Executing Commands
Up until now, everything we've learned has been about how to write one-off logic for your specific Slate editor. But one of the most powerful things about Slate is that it lets you model your specific rich text "domain" however you'd like, and write less one-off code.
In the previous guides we've written some useful code to handle formatting code blocks and bold marks. And we've hooked up the onKeyDown handler to invoke that code. But we've always done it using the built-in Editor helpers directly, instead of using "commands".
Slate lets you augment the built-in editor object to handle your own custom rich text commands. And you can even use pre-packaged "plugins" which add a given set of functionality.
Let's see how this works.
We'll start with our app from earlier:
1
const initialValue = [
2
{
3
type: 'paragraph',
4
children: [{ text: 'A line of text in a paragraph.' }],
5
},
6
]
7
8
const App = () => {
9
const editor = useMemo(() => withReact(createEditor()), [])
10
11
const renderElement = useCallback(props => {
12
switch (props.element.type) {
13
case 'code':
14
return <CodeElement {...props} />
15
default:
16
return <DefaultElement {...props} />
17
}
18
}, [])
19
20
const renderLeaf = useCallback(props => {
21
return <Leaf {...props} />
22
}, [])
23
24
return (
25
<Slate editor={editor} value={initialValue}>
26
<Editable
27
renderElement={renderElement}
28
renderLeaf={renderLeaf}
29
onKeyDown={event => {
30
if (!event.ctrlKey) {
31
return
32
}
33
34
switch (event.key) {
35
case '`': {
36
event.preventDefault()
37
const [match] = Editor.nodes(editor, {
38
match: n => n.type === 'code',
39
})
40
Transforms.setNodes(
41
editor,
42
{ type: match ? null : 'code' },
43
{ match: n => Editor.isBlock(editor, n) }
44
)
45
break
46
}
47
48
case 'b': {
49
event.preventDefault()
50
Transforms.setNodes(
51
editor,
52
{ bold: true },
53
{ match: n => Text.isText(n), split: true }
54
)
55
break
56
}
57
}
58
}}
59
/>
60
</Slate>
61
)
62
}
Copied!
It has the concept of "code blocks" and "bold formatting". But these things are all defined in one-off cases inside the onKeyDown handler. If you wanted to reuse that logic elsewhere you'd need to extract it.
We can instead implement these domain-specific concepts by creating custom helper functions:
1
// Define our own custom set of helpers.
2
const CustomEditor = {
3
isBoldMarkActive(editor) {
4
const [match] = Editor.nodes(editor, {
5
match: n => n.bold === true,
6
universal: true,
7
})
8
9
return !!match
10
},
11
12
isCodeBlockActive(editor) {
13
const [match] = Editor.nodes(editor, {
14
match: n => n.type === 'code',
15
})
16
17
return !!match
18
},
19
20
toggleBoldMark(editor) {
21
const isActive = CustomEditor.isBoldMarkActive(editor)
22
Transforms.setNodes(
23
editor,
24
{ bold: isActive ? null : true },
25
{ match: n => Text.isText(n), split: true }
26
)
27
},
28
29
toggleCodeBlock(editor) {
30
const isActive = CustomEditor.isCodeBlockActive(editor)
31
Transforms.setNodes(
32
editor,
33
{ type: isActive ? null : 'code' },
34
{ match: n => Editor.isBlock(editor, n) }
35
)
36
},
37
}
38
39
const initialValue = [
40
{
41
type: 'paragraph',
42
children: [{ text: 'A line of text in a paragraph.' }],
43
},
44
]
45
46
const App = () => {
47
const editor = useMemo(() => withReact(createEditor()), [])
48
49
const renderElement = useCallback(props => {
50
switch (props.element.type) {
51
case 'code':
52
return <CodeElement {...props} />
53
default:
54
return <DefaultElement {...props} />
55
}
56
}, [])
57
58
const renderLeaf = useCallback(props => {
59
return <Leaf {...props} />
60
}, [])
61
62
return (
63
<Slate editor={editor} value={initialValue}>
64
<Editable
65
renderElement={renderElement}
66
renderLeaf={renderLeaf}
67
onKeyDown={event => {
68
if (!event.ctrlKey) {
69
return
70
}
71
72
// Replace the `onKeyDown` logic with our new commands.
73
switch (event.key) {
74
case '`': {
75
event.preventDefault()
76
CustomEditor.toggleCodeBlock(editor)
77
break
78
}
79
80
case 'b': {
81
event.preventDefault()
82
CustomEditor.toggleBoldMark(editor)
83
break
84
}
85
}
86
}}
87
/>
88
</Slate>
89
)
90
}
Copied!
Now our commands are clearly defined and you can invoke them from anywhere we have access to our editor object. For example, from hypothetical toolbar buttons:
1
const initialValue = [
2
{
3
type: 'paragraph',
4
children: [{ text: 'A line of text in a paragraph.' }],
5
},
6
]
7
8
const App = () => {
9
const editor = useMemo(() => withReact(createEditor()), [])
10
11
const renderElement = useCallback(props => {
12
switch (props.element.type) {
13
case 'code':
14
return <CodeElement {...props} />
15
default:
16
return <DefaultElement {...props} />
17
}
18
}, [])
19
20
const renderLeaf = useCallback(props => {
21
return <Leaf {...props} />
22
}, [])
23
24
return (
25
// Add a toolbar with buttons that call the same methods.
26
<Slate editor={editor} value={initialValue}>
27
<div>
28
<button
29
onMouseDown={event => {
30
event.preventDefault()
31
CustomEditor.toggleBoldMark(editor)
32
}}
33
>
34
Bold
35
</button>
36
<button
37
onMouseDown={event => {
38
event.preventDefault()
39
CustomEditor.toggleCodeBlock(editor)
40
}}
41
>
42
Code Block
43
</button>
44
</div>
45
<Editable
46
editor={editor}
47
renderElement={renderElement}
48
renderLeaf={renderLeaf}
49
onKeyDown={event => {
50
if (!event.ctrlKey) {
51
return
52
}
53
54
switch (event.key) {
55
case '`': {
56
event.preventDefault()
57
CustomEditor.toggleCodeBlock(editor)
58
break
59
}
60
61
case 'b': {
62
event.preventDefault()
63
CustomEditor.toggleBoldMark(editor)
64
break
65
}
66
}
67
}}
68
/>
69
</Slate>
70
)
71
}
Copied!
That's the benefit of extracting the logic.
And there you have it! We just added a ton of functionality to the editor with very little work. And we can keep all of our command logic tested and isolated in a single place, making the code easier to maintain.
Copy link