SlateJS: Creating a Text Editor
Published: 2021-04-09 15:50:13

Having a text editor customized can enhance your user's experience. [Slate](https://www.slatejs.org/) is a text editor framework that can be customized to fit your needs.

# Installing Slate

We'll need three modules for our Slate implementation.

- `slate`: The core module of Slate
- `slate-react`: The React wrapper for Slate
- `slate-history`: Allows the user to undo their actions

```bash
npm i --save slate slate-react slate-history
```

or

```bash
yarn add slate slate-react slate-history
```

# Setting up Slate

To create a new Editor using `createEditor` in combination with a couple of plugins -- `withReact` and `withHistory`.

```react
...
import { createEditor } from 'slate';
import { withReact } from 'slate-react';
import { withHistory } from 'slate-history';

const Editor = () => {
  const editor = useMemo(() => withReact(withHistory(createEditor())), []);
  ...
}
```

Two components will then be used to render the Slate editor.

```react
import { Slate, Editable, ... } 'slate-react';

const Editor = () => {
  ...
  return (
    <Slate editor={editor}>
      <Editable />
    </Slate>
  )
}
```

# Adding Content

Slate would crash initially and that's because a default value is necessary.

```react
const Editor = () => {
  ...
  const [value, setValue] = useState([
    {
      children: [{ text: 'This is my paragraph!' }]
    }
  ])

  return (
    <Slate ... value={value} setValue={setValue}>
      ...
    </Slate>
  )
}
```

It's important for a default Element to exist and not just an empty array. Having an empty array will cause a crash as Slate has nothing to attach the cursor to.

# Custom Types

By default, the content will be considered as text but Rich Text Editors can have non-text content.

Each Element can have custom properties to help render said Element.

```react
const [value, setValue] = useState([
  {
    type: 'paragraph',
    children: [{ text: 'This is my paragraph' }]
  },
  {
    type: 'image',
    src: 'path/to/image',
    alt: 'This is my image'
    children: [{ text: '' }]
  }
])
```

We can then render these custom elements using the `renderElement` prop of the `Editor` component.

```react
const Paragraph = ({ attributes, children }) => (
  <p {...attributes}>{children}</p>
)

const Image = ({ attributes, element, children }) => (
  <div {...attributes}>
    <div contentEditable={false}>
      <img src={element.src} alt={element.src} />
    </div>
    {children}
  </div>
)

const renderElement = (props) => {
  switch(props.element.type) {
    case 'image': 
      return <Image {...props} />
    default:
      return <Paragraph {...props} />
  }
}

const Editor = () => {
  ...
  return (
    <Slate ...>
      <Editor renderElement={renderElement} />
    </Slate>
  )
}
```

It's important that every Element renders the `children` prop as this is how Slate can keep track of which element currently has focus.

## Voids

Voids are Elements that cannot be edited as if it was text. Since our Image cannot be edited as if it was text, we need to tell Slate that it's a void Element.

# Plugins

The `editor` object that we create has an `isVoid` function which determines whether or not an Element is void or not.

Slate allows us to create plugins that can modify the functionality of existing `editor` functions or add new functionality.

```react
const withImage = (editor) => {
  const { isVoid } = editor;

  editor.isVoid = (element) =>
    element.type === 'image' ? true : isVoid(element);

  return editor;
}

const Editor = () => {
  const editor = useMemo(() => withReact(withHistory(withImages(createEditor()))), []);
  ...
}
```

__TIP:__ Since you can have a lot of plugins especially for more complicated editors, you can use the `pipe` function from `lodash/fp`.

```react
import pipe from 'lodash/fp/pipe'

const createEditorWithPlugins = pipe(
  withReact,
  withHistory,
  withImage
)

const Editor = () => {
  const editor = useMemo(() => createEditorWithPlugins(createEditor()), []);
}
```

# Handling Events

Since Image is now considered as a void element, it loses some keyboard functionality. Luckily, the editor provides us two functions that we can extend.

## `insertBreak`

The `editor.insertBreak` function is called when the user presses the `enter` or `return` for Mac.

```react
const { isVoid, insertBreak, ... } = editor

editor.insertBreak = (...args) => {
  const parentPath = Path.parent(editor.selection.focus.path);
  const parentNode = Node.get(editor, parentPath);

  if (isVoid(parentNode)) {
    const nextPath = Path.next(parentPath);
    Transforms.insertNodes(
      editor,
      {
        type: 'paragraph',
        children: [{ text: '' }]
      }, 
      {
        at: nextPath,
        select: true // Focus on this node once inserted
      }
    );
  } else {
    insertBreak(...args);
  }
}
```

## `deleteBackward`

The `editor.deleteBackward` function is called when the user presses the `backspace` or `delete` for Mac.

```react
const { isVoid, deleteBackward, ... } = editor

editor.deleteBackward = (...args) => {
  const parentPath = Path.parent(editor.selection.focus.path);
  const parentNode = Node.get(editor, parentPath);

  if (isVoid(parentNode) || !Node.string(parentNode).length) {
    Transforms.removeNodes(editor, { at: parentPath });
  } else {
    deleteBackward(...args);
  }
}
```

# Conclusion

As you can see, Slate can be heavily customizable as it gives you the necessary tools to add your own functionality.

# Demo

{% codesandbox 2pd0d %}