Custom Plugins
How to create your own plugins.
Plugins are the building blocks of a Plate editor. Many plugins are provided out of the box, but you may find yourself needing to extend Plate with custom functionality. If that's the situation you're in, it's time to make a custom plugin.
- Getting Started: Components - Instructions for adding plugins to your editor
- PlatePlugin API - The complete API reference for creating plugins
Minimal Plugin
Most plugins you'll create will look something like this:
const KEY_AMAZING = 'amazing';
const createAmazingPlugin = createPluginFactory({
key: KEY_AMAZING,
});
The only required option for createPluginFactory
is key
, which is used to uniquely identify your plugin. We typically store the key in a constant so that it can be referenced later. The naming convention for this constant is KEY_
for behavioral plugins (like Reset Node), ELEMENT_
for elements and MARK_
for marks.
Your plugin won't do anything yet, but let's add it to a Plate editor all the same.
const plugins = createPlugins([
// This is where you would specify options or overrides to the plugin
createAmazingPlugin(),
]);
export default () => {
return <Plate plugins={plugins} />;
};
Now we're ready to start adding functionality to your plugin. The specifics of this will depend on what type of plugin you want to create. See Slate's documentation for explanations of terms like element, block, inline and leaf.
Node Plugins
Elements
To create a new type of element using your plugin, use the isElement: true
option.
const createParagraphPlugin = createPluginFactory({
key: ELEMENT_PARAGRAPH,
isElement: true,
});
Each element should have an associated component, which you can provide either as an option component: ParagraphElement
to createPluginFactory
, or in the components
option of createPlugins
(recommended).
const plugins = createPlugins([
createParagraphPlugin(),
], {
components: {
[ELEMENT_PARAGRAPH]: ParagraphElement,
},
});
A minimum, the component for an element plugin should look like this. The PlateElement
component is also available to provide the standard component boilerplate. See Plugin Components for more information.
const ParagraphElement = ({
attributes,
nodeProps,
children,
}: PlateRenderElementProps) => {
return (
<p {...attributes} {...nodeProps}>
{/* The `children` prop must always be rendered */}
{children}
</p>
);
};
You can also configure a hotkey to toggle your new element. In the following example, the HotkeyPlugin
type argument defines types for the options.hotkey
option. Plugin options will be explained in more detail shortly.
const createParagraphPlugin = createPluginFactory<HotkeyPlugin>({
key: ELEMENT_PARAGRAPH,
isElement: true,
handlers: {
// Check for the hotkey on keydown
onKeyDown: onKeyDownToggleElement,
},
options: {
// Define the hotkeys here
hotkey: ['mod+opt+0', 'mod+shift+0'],
},
});
Inline, Void and Leaf Nodes
You can configure your element to be inline or void using the isInline
and isVoid
options respectively. Please refer to the Slate documentation for explanations of these concepts. If you're unsure which of these options to use, try checking Plate's source code for an example of a plugin that works in a similar way to what you want.
const createLinkPlugin = createPluginFactory({
key: ELEMENT_LINK,
isElement: true,
isInline: true,
// ...
});
const createImagePlugin = createPluginFactory({
key: ELEMENT_IMAGE,
isElement: true,
isVoid: true,
// ...
});
To create a new type of mark, use isLeaf: true
. Similarly to with element plugins, you can use onKeyDownToggleMark
to toggle your mark using a hotkey.
const createSubscriptPlugin = createPluginFactory<ToggleMarkPlugin>({
key: MARK_SUBSCRIPT,
isLeaf: true,
handlers: {
onKeyDown: onKeyDownToggleMark,
},
options: {
hotkey: 'mod+.',
clear: MARK_SUBSCRIPT,
},
});
Deserializing HTML
To enable your element or mark to be deserialized from HTML passed to deserializeHtml
or pasted into the editor, specify deserialization rules using the deserializeHtml
option.
const createBoldPlugin = createPluginFactory({
// ...
deserializeHtml: {
rules: [
{ validNodeName: ['STRONG', 'B'] },
{
validStyle: {
fontWeight: ['600', '700', 'bold'],
},
},
],
},
});
Behavioral Plugins
Rather than render an element or a mark, you may want to customize the behavior of your editor. Various plugin options are available to modify the behavior of Plate.
Event Handlers
The recommended way to respond to user-generated events from inside a plugin is with the handlers
plugin option. A handler should be a function that takes an editor
object and returns another function that takes an event object.
The onChange
handler, which is called when the editor value changes, is an exception to this rule; the inner function of an onChange
handler should accept a Value
object instead of an event.
const createExamplePlugin = createPluginFactory({
key: KEY_EXAMPLE,
handlers: {
onChange: (editor) => (value) => {
console.log(editor, value);
},
onKeyDown: (editor) => (event) => {
console.log(`You pressed ${event.key}`);
},
},
});
Inject Props
You may want to inject a class name or CSS property into any node having a certain property. For example, the following plugin sets the textAlign
CSS property on paragraphs with an align
property.
const KEY_ALIGN = 'align';
const createAlignPlugin = createPluginFactory({
key: KEY_ALIGN,
// Note that we're using `then` to access to the editor
then: (editor) => ({
inject: {
props: {
nodeKey: KEY_ALIGN,
defaultNodeValue: 'left',
styleKey: 'textAlign',
validNodeValues: ['left', 'center', 'right', 'justify'],
validTypes: [getPluginType(editor, ELEMENT_DEFAULT)],
},
},
}),
});
A paragraph node affected by the above plugin would look like this:
{
type: 'p',
align: 'right',
children: [{ text: 'This paragraph is aligned to the right!' }],
}
With Overrides (Advanced)
Occasionally, you'll need to override the built-in editor methods provided by Slate to work around bugs or add complex functionality. To do this, you can use the withOverrides
plugin option to directly mutate properties of the editor
object after its creation.
One common application of this technique is to create custom normalizers.
const withCustomNormalizer = <
V extends Value = Value,
E extends PlateEditor<V> = PlateEditor<V>
>(editor: E) => {
const { normalizeNode } = editor;
editor.normalizeNode = ([node, path]: TNodeEntry) => {
// Normalize the node if necessary
// ...
// Call other normalizers
normalizeNode([node, path]);
};
return editor;
};
const createCustomNormalizerPlugin = createPluginFactory({
key: KEY_CUSTOM_NORMALIZER,
withOverrides: withCustomNormalizer,
});
Custom Plugin Options
Often, you may want to pass custom data to a plugin. We've already seen two examples of this using the HotkeyPlugin
and ToggleMarkPlugin
type arguments to createPluginFactory
.
Let's see a more detailed example of defining a custom options type and using it to customize the plugin behavior.
interface DemoPlugin {
username?: string;
}
const KEY_DEMO = 'demo';
const createDemoPlugin = createPluginFactory<DemoPlugin>({
key: KEY_DEMO,
handlers: {
onKeyDown: (editor) => (event) => {
const { username } = getPluginOptions<DemoPlugin>(editor, KEY_DEMO);
console.log(`${username} pressed ${event.key}`)
},
},
then: (_editor, { options }) => ({
renderAboveEditable: ({ children }) => (
<div>
<p>Editing as {options.username}</p>
{children}
</div>
),
}),
options: {
// Optionally specify a default value
username: 'Anonymous',
},
});
const MyEditor = ({ username }: { username: string }) => {
const plugins = useMemo(() => createPlugins([
createDemoPlugin({
options: { username },
}),
]), [username]);
return <Plate plugins={plugins} />;
};
export default () => <MyEditor username="Marsha P. Johnson" />;
As you can see from the above example, plugin options can be accessed anywhere that you have a reference to editor
using getPluginOptions
. Options are also exposed from a handful of other APIs, such as the second arguments to the then
and withOverrides
functions.
See also
See the PlatePlugin API for more plugin options. Here are some options you may want to pay particular attention to. You can find examples of their usage in Plate's source code.
editor.insertData
- Handles data pasted into the editordecorate
- Used to apply decorations to text ranges without modifying the content of the editorinject.aboveComponent
- Inject a component wrapping the components of other pluginsinject.belowComponent
- Inject a component wrapping the children of other pluginsinject.pluginsByKey
- Modify the options of other pluginsplugins
- Nest multiple plugins inside the same plugin factoryserializeHtml
- Add a custom HTML serializer to a plugin