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)