DEV Community

Karl Castillo
Karl Castillo

Posted on • Edited on

SlateJS: Creating a Text Editor

Having a text editor customized can enhance your user's experience. Slate 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
npm i --save slate slate-react slate-history
Enter fullscreen mode Exit fullscreen mode

or

yarn add slate slate-react slate-history
Enter fullscreen mode Exit fullscreen mode

Setting up Slate

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

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

const Editor = () => {
  const editor = useMemo(() => withReact(withHistory(createEditor())), []);
  ...
}
Enter fullscreen mode Exit fullscreen mode

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

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

const Editor = () => {
  ...
  return (
    <Slate editor={editor}>
      <Editable />
    </Slate>
  )
}
Enter fullscreen mode Exit fullscreen mode

Adding Content

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

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

  return (
    <Slate ... value={value} setValue={setValue}>
      ...
    </Slate>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

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: '' }]
  }
])
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

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()))), []);
  ...
}
Enter fullscreen mode Exit fullscreen mode

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

import pipe from 'lodash/fp/pipe'

const createEditorWithPlugins = pipe(
  withReact,
  withHistory,
  withImage
)

const Editor = () => {
  const editor = useMemo(() => createEditorWithPlugins(createEditor()), []);
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

deleteBackward

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

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

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

Demo

Top comments (0)