Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Lexical Building Custom Features

Before you begin building custom features for Lexical, it is crucial to familiarize yourself with the Lexical docs, particularly the "Concepts" section. This foundation is necessary for understanding Lexical's core principles, such as nodes, editor state, and commands.

Lexical features are designed to be modular, meaning each piece of functionality is encapsulated within just two specific interfaces: one for server-side code and one for client-side code.

By convention, these are named feature.server.ts for server-side functionality and feature.client.ts for client-side functionality. The primary functionality is housed within feature.server.ts, which users will import into their projects. The client-side feature, although defined separately, is integrated and rendered server-side through the server feature. That way, we still maintain a clear boundary between server and client code, while also centralizing the code needed for a feature in basically one place. This approach is beneficial for managing all the bits and pieces which make up your feature as a whole, such as toolbar entries, buttons, or new nodes, allowing each feature to be neatly contained and managed independently.

Server Feature

To start building new features, you should start with the server feature, which is the entry-point.

Example myFeature/feature.server.ts:

1
import { createServerFeature } from '@payloadcms/richtext-lexical';
2
3
export const MyFeature = createServerFeature({
4
feature: {
5
},
6
key: 'myFeature',
7
})

createServerFeature is a helper function which lets you create new features without boilerplate code.

Now, the feature is ready to be used in the editor:

1
import { MyFeature } from './myFeature/feature.server';
2
import { lexicalEditor } from '@payloadcms/richtext-lexical';
3
4
//...
5
{
6
name: 'richText',
7
type: 'richText',
8
editor: lexicalEditor({
9
features: [
10
MyFeature(),
11
],
12
}),
13
},

By default, this server feature does nothing - you haven't added any functionality yet. Depending on what you want your feature to do, the ServerFeature type exposes various properties you can set to inject custom functionality into the lexical editor.

i18n

Each feature can register their own translations, which are automatically scoped to the feature key:

1
import { createServerFeature } from '@payloadcms/richtext-lexical';
2
3
4
export const MyFeature = createServerFeature({
5
feature: {
6
i18n: {
7
en: {
8
label: 'My Feature',
9
},
10
de: {
11
label: 'Mein Feature',
12
},
13
},
14
},
15
key: 'myFeature',
16
})

This allows you to add i18n translations scoped to your feature. This specific example translation will be available under lexical:myFeature:label - myFeature being your feature key.

Markdown Transformers

The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when converting the editor from or to markdown.

1
import { createServerFeature } from '@payloadcms/richtext-lexical';
2
import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown'
3
import {
4
$createMyNode,
5
$isMyNode,
6
MyNode
7
} from './nodes/MyNode'
8
9
const MyMarkdownTransformer: ElementTransformer = {
10
type: 'element',
11
dependencies: [MyNode],
12
export: (node, exportChildren) => {
13
if (!$isMyNode(node)) {
14
return null
15
}
16
return '+++'
17
},
18
// match ---
19
regExp: /^+++\s*$/,
20
replace: (parentNode) => {
21
const node = $createMyNode()
22
if (node) {
23
parentNode.replace(node)
24
}
25
},
26
}
27
28
29
export const MyFeature = createServerFeature({
30
feature: {
31
markdownTransformers: [MyMarkdownTransformer],
32
},
33
key: 'myFeature',
34
})

In this example, the node will be outputted as +++ in Markdown, and the markdown +++ will be converted to a MyNode node in the editor.

Nodes

While nodes added to the server feature do not control how the node is rendered in the editor, they control other aspects of the node:

  • HTML conversion
  • Node Hooks
  • Sub fields
  • Behavior in a headless editor

The createNode helper function is used to create nodes with proper typing. It is recommended to use this function to create nodes.

1
import { createServerFeature, createNode } from '@payloadcms/richtext-lexical';
2
import {
3
MyNode
4
} from './nodes/MyNode'
5
6
export const MyFeature = createServerFeature({
7
feature: {
8
9
nodes: [
10
// Use the createNode helper function to more easily create nodes with proper typing
11
createNode({
12
converters: {
13
html: {
14
converter: () => {
15
return `<hr/>`
16
},
17
nodeTypes: [MyNode.getType()],
18
},
19
},
20
// Here you can add your actual node. On the server, they will be
21
// used to initialize a headless editor which can be used to perform
22
// operations on the editor, like markdown / html conversion.
23
node: MyNode,
24
}),
25
],
26
},
27
key: 'myFeature',
28
})

While nodes in the client feature are added by themselves to the nodes array, nodes in the server feature can be added together with the following sibling options:

OptionDescription
getSubFieldsIf a node includes sub-fields (e.g. block and link nodes), passing the subFields schema here will make Payload automatically populate & run hooks for them.
getSubFieldsDataIf a node includes sub-fields, the sub-fields data needs to be returned here, alongside getSubFields which returns their schema.
graphQLPopulationPromisesAllows you to run population logic when a node's data was requested from GraphQL. While getSubFields and getSubFieldsData automatically handle populating sub-fields (since they run hooks on them), those are only populated in the Rest API. This is because the Rest API hooks do not have access to the 'depth' property provided by GraphQL. In order for them to be populated correctly in GraphQL, the population logic needs to be provided here.
nodeThe actual lexical node needs to be provided here. This also supports lexical node replacements.
validationsThis allows you to provide node validations, which are run when your document is being validated, alongside other Payload fields. You can use it to throw a validation error for a specific node in case its data is incorrect.
convertersAllows you to define how a node can be serialized into different formats. Currently, only supports HTML. Markdown converters are defined in markdownTransformers and not here.
hooksJust like Payload fields, you can provide hooks which are run for this specific node. These are called Node Hooks.

Feature load order

Server features can also accept a function as the feature property (useful for sanitizing props, as mentioned below). This function will be called when the feature is loaded during the Payload sanitization process:

1
import { createServerFeature } from '@payloadcms/richtext-lexical';
2
3
createServerFeature({
4
//...
5
feature: async ({ config, isRoot, props, resolvedFeatures, unSanitizedEditorConfig, featureProviderMap }) => {
6
7
return {
8
//Actual server feature here...
9
}
10
}
11
})

"Loading" here means the process of calling this feature function. By default, features are called in the order in which they are added to the editor. However, sometimes you might want to load a feature after another feature has been loaded, or require a different feature to be loaded, throwing an error if this is not the case.

Within lexical, one example where this is done are our list features. Both UnorderedListFeature and OrderedListFeature register the same ListItem node. Within UnorderedListFeature we register it normally, but within OrderedListFeature we want to only register the ListItem node if the UnorderedListFeature is not present - otherwise, we would have two features registering the same node.

Here is how we do it:

1
import { createServerFeature, createNode } from '@payloadcms/richtext-lexical';
2
3
export const OrderedListFeature = createServerFeature({
4
feature: ({ featureProviderMap }) => {
5
return {
6
// ...
7
nodes: featureProviderMap.has('unorderedList')
8
? []
9
: [
10
createNode({
11
// ...
12
}),
13
],
14
}
15
},
16
key: 'orderedList',
17
})

featureProviderMap will always be available and contain all the features, even yet-to-be-loaded ones, so we can check if a feature is loaded by checking if its key present in the map.

If you wanted to make sure a feature is loaded before another feature, you can use the dependenciesPriority property:

1
import { createServerFeature } from '@payloadcms/richtext-lexical';
2
3
export const MyFeature = createServerFeature({
4
feature: ({ featureProviderMap }) => {
5
return {
6
// ...
7
}
8
},
9
key: 'myFeature',
10
dependenciesPriority: ['otherFeature'],
11
})
OptionDescription
dependenciesSoftKeys of soft-dependencies needed for this feature. These are optional. Payload will attempt to load them before this feature, but doesn't throw an error if that's not possible.
dependenciesKeys of dependencies needed for this feature. These dependencies do not have to be loaded first, but they have to exist, otherwise an error will be thrown.
dependenciesPriorityKeys of priority dependencies needed for this feature. These dependencies have to be loaded first AND have to exist, otherwise an error will be thrown. They will be available in the feature property.

Client Feature

Most of the functionality which the user actually sees and interacts with, like toolbar items and React components for nodes, resides on the client-side.

To set up your client-side feature, follow these three steps:

  1. Create a Separate File: Start by creating a new file specifically for your client feature, such as myFeature/feature.client.ts. It's important to keep client and server features in separate files to maintain a clean boundary between server and client code.
  2. 'use client': Mark that file with a 'use client' directive at the top of the file
  3. Register the Client Feature: Register the client feature within your server feature, by passing it to the ClientFeature prop. This is needed because the server feature is the sole entry-point of your feature. This also means you are not able to create a client feature without a server feature, as you will not be able to register it otherwise.

Example myFeature/feature.client.ts:

1
'use client'
2
3
import { createClientFeature } from '@payloadcms/richtext-lexical/client';
4
5
export const MyClientFeature = createClientFeature({
6
7
})

Explore the APIs available through ClientFeature to add the specific functionality you need. Remember, do not import directly from '@payloadcms/richtext-lexical' when working on the client-side, as it will cause errors with webpack or turbopack. Instead, use '@payloadcms/richtext-lexical/client' for all client-side imports. Type-imports are excluded from this rule and can always be imported.

Nodes

Add nodes to the nodes array in both your client & server feature. On the server side, nodes are utilized for backend operations like HTML conversion in a headless editor. On the client side, these nodes are integral to how content is displayed and managed in the editor, influencing how they are rendered, behave, and saved in the database.

Example:

myFeature/feature.client.ts:

1
'use client'
2
3
import { createClientFeature } from '@payloadcms/richtext-lexical/client';
4
import { MyNode } from './nodes/MyNode';
5
6
export const MyClientFeature = createClientFeature({
7
nodes: [MyNode]
8
})

This also supports lexical node replacements.

myFeature/nodes/MyNode.tsx:

Here is a basic DecoratorNode example:

1
import type {
2
DOMConversionMap,
3
DOMConversionOutput,
4
DOMExportOutput,
5
EditorConfig,
6
LexicalNode,
7
SerializedLexicalNode,
8
} from '@payloadcms/richtext-lexical/lexical'
9
10
import { $applyNodeReplacement, DecoratorNode } from '@payloadcms/richtext-lexical/lexical'
11
12
// SerializedLexicalNode is the default lexical node.
13
// By setting your SerializedMyNode type to SerializedLexicalNode,
14
// you are basically saying that this node does not save any additional data.
15
// If you want your node to save data, feel free to extend it
16
export type SerializedMyNode = SerializedLexicalNode
17
18
// Lazy-import the React component to your node here
19
const MyNodeComponent = React.lazy(() =>
20
import('../component/index.js').then((module) => ({
21
default: module.MyNodeComponent,
22
})),
23
)
24
25
/**
26
* This node is a DecoratorNode. DecoratorNodes allow you to render React components in the editor.
27
*
28
* They need both createDom and decorate functions. createDom => outside of the html. decorate => React Component inside of the html.
29
*
30
* If we used DecoratorBlockNode instead, we would only need a decorate method
31
*/
32
export class MyNode extends DecoratorNode<React.ReactElement> {
33
static clone(node: MyNode): MyNode {
34
return new MyNode(node.__key)
35
}
36
37
static getType(): string {
38
return 'myNode'
39
}
40
41
/**
42
* Defines what happens if you copy a div element from another page and paste it into the lexical editor
43
*
44
* This also determines the behavior of lexical's internal HTML -> Lexical converter
45
*/
46
static importDOM(): DOMConversionMap | null {
47
return {
48
div: () => ({
49
conversion: $yourConversionMethod,
50
priority: 0,
51
}),
52
}
53
}
54
55
/**
56
* The data for this node is stored serialized as JSON. This is the "load function" of that node: it takes the saved data and converts it into a node.
57
*/
58
static importJSON(serializedNode: SerializedMyNode): MyNode {
59
return $createMyNode()
60
}
61
62
/**
63
* Determines how the hr element is rendered in the lexical editor. This is only the "initial" / "outer" HTML element.
64
*/
65
createDOM(config: EditorConfig): HTMLElement {
66
const element = document.createElement('div')
67
return element
68
}
69
70
/**
71
* Allows you to render a React component within whatever createDOM returns.
72
*/
73
decorate(): React.ReactElement {
74
return <MyNodeComponent nodeKey={this.__key} />
75
}
76
77
/**
78
* Opposite of importDOM, this function defines what happens when you copy a div element from the lexical editor and paste it into another page.
79
*
80
* This also determines the behavior of lexical's internal Lexical -> HTML converter
81
*/
82
exportDOM(): DOMExportOutput {
83
return { element: document.createElement('div') }
84
}
85
/**
86
* Opposite of importJSON. This determines what data is saved in the database / in the lexical editor state.
87
*/
88
exportJSON(): SerializedLexicalNode {
89
return {
90
type: 'myNode',
91
version: 1,
92
}
93
}
94
95
getTextContent(): string {
96
return '\n'
97
}
98
99
isInline(): false {
100
return false
101
}
102
103
updateDOM(): boolean {
104
return false
105
}
106
}
107
108
// This is used in the importDOM method. Totally optional if you do not want your node to be created automatically when copy & pasting certain dom elements
109
// into your editor.
110
function $yourConversionMethod(): DOMConversionOutput {
111
return { node: $createMyNode() }
112
}
113
114
// This is a utility method to create a new MyNode. Utility methods prefixed with $ make it explicit that this should only be used within lexical
115
export function $createMyNode(): MyNode {
116
return $applyNodeReplacement(new MyNode())
117
}
118
119
// This is just a utility method you can use to check if a node is a MyNode. This also ensures correct typing.
120
export function $isMyNode(
121
node: LexicalNode | null | undefined,
122
): node is MyNode {
123
return node instanceof MyNode
124
}

Please do not add any 'use client' directives to your nodes, as the node class can be used on the server.

Plugins

One small part of a feature are plugins. The name stems from the lexical playground plugins and is just a small part of a lexical feature. Plugins are simply React components which are added to the editor, within all the lexical context providers. They can be used to add any functionality to the editor, by utilizing the lexical API.

Most commonly, they are used to register lexical listeners, node transforms or commands. For example, you could add a drawer to your plugin and register a command which opens it. That command can then be called from anywhere within lexical, e.g. from within your custom lexical node.

To add a plugin, simply add it to the plugins array in your client feature:

1
'use client'
2
3
import { createClientFeature } from '@payloadcms/richtext-lexical/client';
4
import { MyPlugin } from './plugin';
5
6
export const MyClientFeature = createClientFeature({
7
plugins: [MyPlugin]
8
})

Example plugin.tsx:

1
'use client'
2
import type {
3
LexicalCommand,
4
} from '@payloadcms/richtext-lexical/lexical'
5
6
import {
7
createCommand,
8
$getSelection,
9
$isRangeSelection,
10
COMMAND_PRIORITY_EDITOR
11
} from '@payloadcms/richtext-lexical/lexical'
12
13
import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext.js'
14
import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'
15
import { useEffect } from 'react'
16
17
import type { PluginComponent } from '@payloadcms/richtext-lexical' // type imports can be imported from @payloadcms/richtext-lexical - even on the client
18
19
import {
20
$createMyNode,
21
} from '../nodes/MyNode'
22
import './index.scss'
23
24
export const INSERT_MYNODE_COMMAND: LexicalCommand<void> = createCommand(
25
'INSERT_MYNODE_COMMAND',
26
)
27
28
/**
29
* Plugin which registers a lexical command to insert a new MyNode into the editor
30
*/
31
export const MyNodePlugin: PluginComponent= () => {
32
// The useLexicalComposerContext hook can be used to access the lexical editor instance
33
const [editor] = useLexicalComposerContext()
34
35
useEffect(() => {
36
return editor.registerCommand(
37
INSERT_MYNODE_COMMAND,
38
(type) => {
39
const selection = $getSelection()
40
41
if (!$isRangeSelection(selection)) {
42
return false
43
}
44
45
const focusNode = selection.focus.getNode()
46
47
if (focusNode !== null) {
48
const newMyNode = $createMyNode()
49
$insertNodeToNearestRoot(newMyNode)
50
}
51
52
return true
53
},
54
COMMAND_PRIORITY_EDITOR,
55
)
56
}, [editor])
57
58
return null
59
}

In this example, we register a lexical command, which simply inserts a new MyNode into the editor. This command can be called from anywhere within lexical, e.g. from within a custom node.

Toolbar groups

Toolbar groups are visual containers which hold toolbar items. There are different toolbar group types which determine how a toolbar item is displayed: dropdown and buttons.

All the default toolbar groups are exported from @payloadcms/richtext-lexical/client. You can use them to add your own toolbar items to the editor:

  • Dropdown: toolbarAddDropdownGroupWithItems
  • Dropdown: toolbarTextDropdownGroupWithItems
  • Buttons: toolbarFormatGroupWithItems
  • Buttons: toolbarFeatureButtonsGroupWithItems

Within dropdown groups, items are positioned vertically when the dropdown is opened and include the icon & label. Within button groups, items are positioned horizontally and only include the icon. If a toolbar group with the same key is declared twice, all its items will be merged into one group.

Custom buttons toolbar group

OptionDescription
itemsAll toolbar items part of this toolbar group need to be added here.
keyEach toolbar group needs to have a unique key. Groups with the same keys will have their items merged together.
orderDetermines where the toolbar group will be.
typeControls the toolbar group type. Set to buttons to create a buttons toolbar group, which displays toolbar items horizontally using only their icons.

Example:

1
import type { ToolbarGroup, ToolbarGroupItem } from '@payloadcms/richtext-lexical'
2
3
export const toolbarFormatGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => {
4
return {
5
type: 'buttons',
6
items,
7
key: 'myButtonsToolbar',
8
order: 10,
9
}
10
}

Custom dropdown toolbar group

OptionDescription
itemsAll toolbar items part of this toolbar group need to be added here.
keyEach toolbar group needs to have a unique key. Groups with the same keys will have their items merged together.
orderDetermines where the toolbar group will be.
typeControls the toolbar group type. Set to dropdown to create a buttons toolbar group, which displays toolbar items vertically using their icons and labels, if the dropdown is open.
ChildComponentThe dropdown toolbar ChildComponent allows you to pass in a React Component which will be displayed within the dropdown button.

Example:

1
import type { ToolbarGroup, ToolbarGroupItem } from '@payloadcms/richtext-lexical'
2
3
import { MyIcon } from './icons/MyIcon'
4
5
export const toolbarAddDropdownGroupWithItems = (items: ToolbarGroupItem[]): ToolbarGroup => {
6
return {
7
type: 'dropdown',
8
ChildComponent: MyIcon,
9
items,
10
key: 'myDropdownToolbar',
11
order: 10,
12
}
13
}

Toolbar items

Custom nodes and features on its own are pointless, if they can't be added to the editor. You will need to hook in one of our interfaces which allow the user to interact with the editor:

  • Fixed toolbar which stays fixed at the top of the editor
  • Inline, floating toolbar which appears when selecting text
  • Slash menu which appears when typing / in the editor
  • Markdown transformers, which are triggered when a certain text pattern is typed in the editor
  • Or any other interfaces which can be added via your own plugins. Our toolbars are a prime example of this - they are just plugins.

To add a toolbar item to either the floating or the inline toolbar, you can add a ToolbarGroup with a ToolbarItem to the toolbarFixed or toolbarInline props of your client feature:

1
'use client'
2
3
import { createClientFeature, toolbarAddDropdownGroupWithItems } from '@payloadcms/richtext-lexical/client';
4
import { IconComponent } from './icon';
5
import { $isHorizontalRuleNode } from './nodes/MyNode';
6
import { INSERT_MYNODE_COMMAND } from './plugin';
7
import { $isNodeSelection } from '@payloadcms/richtext-lexical/lexical'
8
9
export const MyClientFeature = createClientFeature({
10
toolbarFixed: {
11
groups: [
12
toolbarAddDropdownGroupWithItems([
13
{
14
ChildComponent: IconComponent,
15
isActive: ({ selection }) => {
16
if (!$isNodeSelection(selection) || !selection.getNodes().length) {
17
return false
18
}
19
20
const firstNode = selection.getNodes()[0]
21
return $isHorizontalRuleNode(firstNode)
22
},
23
key: 'myNode',
24
label: ({ i18n }) => {
25
return i18n.t('lexical:myFeature:label')
26
},
27
onSelect: ({ editor }) => {
28
editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined)
29
},
30
},
31
]),
32
],
33
},
34
})

You will have to provide a toolbar group first, and then the items for that toolbar group (more on that above).

A ToolbarItem various props you can use to customize its behavior:

OptionDescription
ChildComponentA React component which is rendered within your toolbar item's default button component. Usually, you want this to be an icon.
ComponentA React component which is rendered in place of the toolbar item's default button component, thus completely replacing it. The ChildComponent and onSelect properties will be ignored.
labelThe label will be displayed in your toolbar item, if it's within a dropdown group. To make use of i18n, this can be a function.
keyEach toolbar item needs to have a unique key.
onSelectA function which is called when the toolbar item is clicked.
isEnabledThis is optional and controls if the toolbar item is clickable or not. If false is returned here, it will be grayed out and unclickable.
isActiveThis is optional and controls if the toolbar item is highlighted or not

The API for adding an item to the floating inline toolbar (toolbarInline) is identical. If you wanted to add an item to both the fixed and inline toolbar, you can extract it into its own variable (typed as ToolbarGroup[]) and add it to both the toolbarFixed and toolbarInline props.

Slash Menu groups

We're exporting slashMenuBasicGroupWithItems from @payloadcms/richtext-lexical/client which you can use to add items to the slash menu labelled "Basic". If you want to create your own slash menu group, here is an example:

1
import type {
2
SlashMenuGroup,
3
SlashMenuItem,
4
} from '@payloadcms/richtext-lexical'
5
6
export function mwnSlashMenuGroupWithItems(items: SlashMenuItem[]): SlashMenuGroup {
7
return {
8
items,
9
key: 'myGroup',
10
label: 'My Group' // <= This can be a function to make use of i18n
11
}
12
}

By creating a helper function like this, you can easily re-use it and add items to it. All Slash Menu groups with the same keys will have their items merged together.

OptionDescription
itemsAn array of SlashMenuItem's which will be displayed in the slash menu.
labelThe label will be displayed before your Slash Menu group. In order to make use of i18n, this can be a function.
keyUsed for class names and, if label is not provided, for display. Slash menus with the same key will have their items merged together.

Slash Menu items

The API for adding items to the slash menu is similar. There are slash menu groups, and each slash menu groups has items. Here is an example:

1
'use client'
2
3
import { createClientFeature, slashMenuBasicGroupWithItems } from '@payloadcms/richtext-lexical/client';
4
import { INSERT_MYNODE_COMMAND } from './plugin';
5
import { IconComponent } from './icon';
6
7
export const MyClientFeature = createClientFeature({
8
slashMenu: {
9
groups: [
10
slashMenuBasicGroupWithItems([
11
{
12
Icon: IconComponent,
13
key: 'myNode',
14
keywords: ['myNode', 'myFeature', 'someOtherKeyword'],
15
label: ({ i18n }) => {
16
return i18n.t('lexical:myFeature:label')
17
},
18
onSelect: ({ editor }) => {
19
editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined)
20
},
21
},
22
]),
23
],
24
},
25
})
OptionDescription
IconThe icon which is rendered in your slash menu item.
labelThe label will be displayed in your slash menu item. In order to make use of i18n, this can be a function.
keyEach slash menu item needs to have a unique key. The key will be matched when typing, displayed if no label property is set, and used for classNames.
onSelectA function which is called when the slash menu item is selected.
keywordsKeywords are used to match the item for different texts typed after the '/'. E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc. In addition to the keywords, the label and key will be used to find the right slash menu item.

Markdown Transformers

The Client Feature, just like the Server Feature, allows you to add markdown transformers. Markdown transformers on the client are used to create new nodes when a certain markdown pattern is typed in the editor.

1
import { createClientFeature } from '@payloadcms/richtext-lexical/client';
2
import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown'
3
import {
4
$createMyNode,
5
$isMyNode,
6
MyNode
7
} from './nodes/MyNode'
8
9
const MyMarkdownTransformer: ElementTransformer = {
10
type: 'element',
11
dependencies: [MyNode],
12
export: (node, exportChildren) => {
13
if (!$isMyNode(node)) {
14
return null
15
}
16
return '+++'
17
},
18
// match ---
19
regExp: /^+++\s*$/,
20
replace: (parentNode) => {
21
const node = $createMyNode()
22
if (node) {
23
parentNode.replace(node)
24
}
25
},
26
}
27
28
29
export const MyFeature = createClientFeature({
30
markdownTransformers: [MyMarkdownTransformer],
31
})

In this example, a new MyNode will be inserted into the editor when +++ is typed.

Providers

You can add providers to your client feature, which will be nested below the EditorConfigProvider. This can be useful if you want to provide some context to your nodes or other parts of your feature.

1
'use client'
2
3
import { createClientFeature } from '@payloadcms/richtext-lexical/client';
4
import { TableContext } from './context';
5
6
export const MyClientFeature = createClientFeature({
7
providers: [TableContext],
8
})

Props

To accept props in your feature, type them as a generic.

Server Feature:

1
createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
2
//...
3
})

Client Feature:

1
createClientFeature<UnSanitizedClientProps, SanitizedClientProps>({
2
//...
3
})

The unSanitized props are what the user will pass to the feature when they call its provider function and add it to their editor config. You then have an option to sanitize those props. To sanitize those in the server feature, you can pass a function to feature instead of an object:

1
createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
2
//...
3
feature: async ({ config, isRoot, props, resolvedFeatures, unSanitizedEditorConfig, featureProviderMap }) => {
4
const sanitizedProps = doSomethingWithProps(props)
5
6
return {
7
sanitizedServerFeatureProps: sanitizedProps,
8
//Actual server feature here...
9
}
10
}
11
})

Keep in mind that any sanitized props then have to be returned in the sanitizedServerFeatureProps property.

In the client feature, it works similarly:

1
createClientFeature<UnSanitizedClientProps, SanitizedClientProps>(
2
({ clientFunctions, featureProviderMap, props, resolvedFeatures, unSanitizedEditorConfig }) => {
3
const sanitizedProps = doSomethingWithProps(props)
4
return {
5
sanitizedClientFeatureProps: sanitizedProps,
6
//Actual client feature here...
7
}
8
},
9
)

Bringing props from the server to the client

By default, the client feature will never receive any props from the server feature. In order to pass props from the server to the client, you can need to return those props in the server feature:

1
type UnSanitizedClientProps = {
2
test: string
3
}
4
5
createServerFeature<UnSanitizedProps, SanitizedProps, UnSanitizedClientProps>({
6
//...
7
feature: {
8
clientFeatureProps: {
9
test: 'myValue'
10
}
11
}
12
})

The reason the client feature does not have the same props available as the server by default is because all client props need to be serializable. You can totally accept things like functions or Maps as props in your server feature, but you will not be able to send those to the client. In the end, those props are sent from the server to the client over the network, so they need to be serializable.

More information

Have a look at the features we've already built - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of Payload or not!

Next

Lexical Migration