With Lexical, how do I set default initial text?

13.4k Views Asked by At

With the Lexical text editor framework, what's the easiest way to initialize the editor in React with a default text string?

I could, for instance, create an instance, manually save the JSON state, and then copy that JSON blob to my code, to set as initialEditorState to PlainTextPlugin, but this seems like I have to be missing something.

Thanks

3

There are 3 best solutions below

0
On

The way to achieve it found in lexical codebase is like so

//...
<RichTextPlugin
   initialEditorState={ prepopulatedRichText}/>
//...

function prepopulatedRichText() {
 const root = $getRoot();
 if (root.getFirstChild() === null) {
   const heading = $createHeadingNode('h1');
   heading.append($createTextNode('Welcome to the playground'));
   root.append(heading);
   const quote = $createQuoteNode();
   quote.append(
     $createTextNode(
       `In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of editor. ` +
         `You can hide it by pressing on the settings control in the bottom-right of your screen and toggling the debug view setting.`,
     ),
   );
   root.append(quote);
   const paragraph = $createParagraphNode();
   paragraph.append(
     $createTextNode('The playground is a demo environment built with '),
     $createTextNode('@lexical/react').toggleFormat('code'),
     $createTextNode('.'),
     $createTextNode(' Try typing in '),
     $createTextNode('some text').toggleFormat('bold'),
     $createTextNode(' with '),
     $createTextNode('different').toggleFormat('italic'),
     $createTextNode(' formats.'),
   );
   root.append(paragraph);
   const paragraph2 = $createParagraphNode();
   paragraph2.append(
     $createTextNode(
       'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!',
     ),
   );
   root.append(paragraph2);
   const paragraph3 = $createParagraphNode();
   paragraph3.append(
     $createTextNode(`If you'd like to find out more about Lexical, you can:`),
   );
   root.append(paragraph3);
   const list = $createListNode('bullet');
   list.append(
     $createListItemNode().append(
       $createTextNode(`Visit the `),
       $createLinkNode('https://lexical.dev/').append(
         $createTextNode('Lexical website'),
       ),
       $createTextNode(` for documentation and more information.`),
     ),
     $createListItemNode().append(
       $createTextNode(`Check out the code on our `),
       $createLinkNode('https://github.com/facebook/lexical').append(
         $createTextNode('GitHub repository'),
       ),
       $createTextNode(`.`),
     ),
     $createListItemNode().append(
       $createTextNode(`Playground code can be found `),
       $createLinkNode(
         'https://github.com/facebook/lexical/tree/main/packages/lexical-playground',
       ).append($createTextNode('here')),
       $createTextNode(`.`),
     ),
     $createListItemNode().append(
       $createTextNode(`Join our `),
       $createLinkNode('https://discord.com/invite/KmG4wQnnD9').append(
         $createTextNode('Discord Server'),
       ),
       $createTextNode(` and chat with the team.`),
     ),
   );
   root.append(list);
   const paragraph4 = $createParagraphNode();
   paragraph4.append(
     $createTextNode(
       `Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).`,
     ),
   );
   root.append(paragraph4);
 }
}

but I prefer to use stringified editorState then parse it to editorState by parseEditorState function owned by instance.

Here is another problem I'm confused which also seems I have to be missed something.

ParseEditorState is a function only used for instance, but I can't use useLexicalComposerContext under the init component which will return LexicalComposer(unexpected error will cause), so I need to write an extra plugin to achieve it. Something like that:

import { useSelector } from "react-redux";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useEffect } from "react";

export default function EditorStatePlugin() {
  const activeNote = useSelector((state) => state.note.activeNote);
  const [editor] = useLexicalComposerContext();
  const state = editor.parseEditorState(
    activeNote?.content ||
      '{"_nodeMap":[["root",{"__children":["1"],"__dir":null,"__format":0,"__indent":0,"__key":"root","__parent":null,"__type":"root"}],["1",{"__type":"paragraph","__parent":"root","__key":"1","__children":[],"__format":0,"__indent":0,"__dir":null}]],"_selection":{"anchor":{"key":"1","offset":0,"type":"element"},"focus":{"key":"1","offset":0,"type":"element"},"type":"range"}}'
  );
  useEffect(() => {
    editor.setEditorState(state);
  }, [activeNote]);

  return null;
}

It seems not a good way to write, is there a better way to do it?

3
On

Your intuition is correct. Avoid touching the EditorState directly, even when it's serialized as a JSON. Manipulating internals (inc. node private properties) can potentially lead to unexpected behavior/errors in future releases.

The initialEditorState can take many shapes:

export type InitialEditorStateType = null | string | EditorState | (() => void);

  • null -> empty EditorState (just a RootNode)
  • string -> a JSON stringified EditorState. Behind the scenes it calls JSON.parse(editor.setEditorState)
  • EditorState -> an EditorState. Behind the scenes it calls - editor.setEditorState() (the undo-redo/History plugin uses this) (() => void) -> an editor.update function

The one you're interested in is (() => void) -> an editor update.

You can run an editor.update like follows:

<LexicalPlainTextPlugin initialEditorState={() => {
  const paragraph = $createParagraphNode();
  const text = $createTextNode('foo');
  paragraph.append(text);
  $getRoot().append(paragraph);
  $getRoot().selectEnd();
}} />

No need to cache (useCallback) initialEditorState as it's only processed once

Side note: we're planning to move initialEditorState (that currently lives in LexicalPlainTextPlugin and LexicalRichTextPlugin) to LexicalComposer but it will work the same way.


We recommend avoiding manually handcrafted solutions too:

// PrepopulatePlugin.js
useLayoutEffect(() => {
  editor.update(() => {
    // Prepopulate
  });
}, [editor]);

We built LexicalContentEditable to work well with SSR and handle the contenteditable appropriately. If you were to build your own custom solution you would have to repeat this process.

1
On

You can set default text by setting editorState to either a stringified JSON state, or a function that imperatively populates the editor.

Approach #1 - Initializing editorState to stringified JSON state

You can set the initial content by setting editorState to a stringified JSON state.

const EMPTY_CONTENT =
  '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';

const initialConfig = {
  ...
  editorState: EMPTY_CONTENT,
  ...
}

const Editor = () => {

  return (
    <LexicalComposer initialConfig={initialConfig}>
      ...
    </LexicalComposer>
  );
}
  

Note that you can retrieve the JSON state via something like the onChangePlugin:

import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
...

  const onChange = (editorState) => {
    editorState.read(() => {
      const json = editorState.toJSON();
      console.log(JSON.stringify(json));
    })
  }
  ...
  
  return (
    <LexicalComposer initialConfig={initialConfig}>
      ...
      <OnChangePlugin onChange={onChange} />
    </LexicalComposer>

Approach #2 - Initializing editorState to function that imperatively populates editor

Or alternatively you can set the editorState to a function like this that imperatively initializes the editor (this is how it's done in the example below):

function prepopulatedRichText() {
  const root = $getRoot();
  if (root.getFirstChild() === null) {
    const paragraph = $createParagraphNode();
    paragraph.append(
      $createTextNode("The playground is a demo environment built with "),
      $createTextNode("@lexical/react").toggleFormat("code"),
      $createTextNode("."),
      $createTextNode(" Try typing in "),
      $createTextNode("some text").toggleFormat("bold"),
      $createTextNode(" with "),
      $createTextNode("different").toggleFormat("italic"),
      $createTextNode(" formats.")
    );
    root.append(paragraph);
  }
}

const initialConfig = {
  ...
  editorState: prepopulatedRichText,
  ...
}

Here are the relevant pages in the documentation:

Here is an example of a Lexical editor defaulting with initial text: