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
or
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.
...
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.
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.
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.
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.
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.
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.
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.
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.
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
Top comments (0)