TL;DR: Do you want to build something like Notion's text editor? Then Lexical is probably the tool for you.
Lexical is a powerful, extensible text editor framework developed by Facebook (now Meta). It's designed to be flexible, performant, and accessible, making it an excellent choice for a wide range of applications. Lexical can be used to build simple text inputs, complex document editors, or even collaborative editing systems.
I found out that the best way to learn about Lexical is to analyze the types of the component. For the initial config for example we have:
export type InitialEditorStateType =
| null
| string
| EditorState
| ((editor: LexicalEditor) => void);
export type InitialConfigType = Readonly<{
editor__DEPRECATED?: LexicalEditor | null;
namespace: string;
nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
onError: (error: Error, editor: LexicalEditor) => void;
editable?: boolean;
theme?: EditorThemeClasses;
editorState?: InitialEditorStateType;
html?: HTMLConfig;
}>;
Do you see that editorState
with the InitialEditorStateType
type? That's the place where we can set the initial state of the editor.
As you can see, it can be a string
, EditorState
, null
, or a function that returns a void
.
Note that string is not a simple string! It's a JSON stringified EditorState. Behind the scenes it calls JSON.parse(editor.setEditorState)
.
In my case I'm building a WhatsApp template builder with Lexical, and users can choose from a list of pre-built templates.
So basically:
bodyContent
property.bodyContent
of the selected template.useSearchParams
hook from Remix to get the template id and set the initial state of the editor.bodyContent
is something like this:In {{1}}, our birthday unfolds,\n\n
A thank you for trust you hold.\n\n
A special surprise awaits,\n\n
Just for you, behind these gates.\n\n
Click below, see deals so rare,\n\n
But hurry, time won't spare.\n\n
Limited offers tick away,\n\n
Seize this gift without delay.
bodyContent
to an EditorState
(i.e a function that imperatively populates editor):export function createInitialConfig(
template?: string | null
): InitialConfigType {
const baseConfig: Omit<InitialConfigType, "editorState"> = {
namespace: "whatsapp-template-editor",
theme: whatsappTemplateEditorTheme,
onError(error: any) {
throw error;
},
nodes: [],
editable: true,
};
if (template) {
return {
...baseConfig,
editorState: () => {
const root = $getRoot();
if (root.getFirstChild() === null) {
const paragraph = $createParagraphNode();
const parts = template.split(/(\{\{.*?\}\}|\*.*?\*)/);
parts.forEach((part) => {
if (part.startsWith("{{") && part.endsWith("}}")) {
// Placeholder, create as code
paragraph.append(createFormattedText(part, ["code"]));
} else if (part.startsWith("*") && part.endsWith("*")) {
// Bold text
paragraph.append(
createFormattedText(part.slice(1, -1), ["bold"])
);
} else {
// Regular text
paragraph.append($createTextNode(part));
}
});
root.append(paragraph);
}
},
};
} else {
return baseConfig;
}
}
function createFormattedText(
text: string,
formats: TextFormatType[] = []
): LexicalNode {
let node = $createTextNode(text);
formats.forEach((format) => {
node = node.toggleFormat(format);
});
return node;
}
And this is how I set up my initial state:
<LexicalComposer initialConfig={createInitialConfig(initialValue)}>
...
</LexicalComposer>