Home > other >  Lexicaljs receive editor state json and text content using debounce in react project
Lexicaljs receive editor state json and text content using debounce in react project

Time:07-28

Requirement

I have a requirement to get the editor state in JSON format as well as the text content of the editor. In addition, I want to receive these values in the debounced way.

I wanted to get these values (as debounced) because I wanted to send them to my server.

Dependencies

"react": "^18.2.0",

"lexical": "^0.3.8",

"@lexical/react": "^0.3.8",

CodePudding user response:

You don't need to touch any of Lexical's internals for this; a custom hook that reads and "stashes" the editor state into a ref and sets up a debounced callback (via use-debounce here, but you can use whatever implementation you like) is enough.

  • getEditorState is in charge of converting the editor state into whichever format you want to send over the wire. It's always called within editorState.read().
function useDebouncedLexicalOnChange<T>(
  getEditorState: (editorState: EditorState) => T,
  callback: (value: T) => void,
  delay: number
) {
  const lastPayloadRef = React.useRef<T | null>(null);
  const callbackRef = React.useRef<(arg: T) => void | null>(callback);
  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);
  const callCallbackWithLastPayload = React.useCallback(() => {
    if (lastPayloadRef.current) {
      callbackRef.current?.(lastPayloadRef.current);
    }
  }, []);
  const call = useDebouncedCallback(callCallbackWithLastPayload, delay);
  const onChange = React.useCallback(
    (editorState) => {
      editorState.read(() => {
        lastPayloadRef.current = getEditorState(editorState);
        call();
      });
    },
    [call, getEditorState]
  );
  return onChange;
}

// ...

const getEditorState = (editorState: EditorState) => ({
  text: $getRoot().getTextContent(false),
  stateJson: JSON.stringify(editorState)
});

function App() {
  const debouncedOnChange = React.useCallback((value) => {
    console.log(new Date(), value);
    // TODO: send to server
  }, []);
  const onChange = useDebouncedLexicalOnChange(
    getEditorState,
    debouncedOnChange,
    1000
  );
  // ...
  <OnChangePlugin onChange={onChange} /> 
}

CodePudding user response:

Code

File: onChangeDebouce.tsx

import {$getRoot} from "lexical";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import React from "react";

const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';

const useLayoutEffectImpl = CAN_USE_DOM ? React.useLayoutEffect : React.useEffect;
var useLayoutEffect = useLayoutEffectImpl;
type onChangeFunction = (editorStateJson: string, editorText: string) => void;

export const OnChangeDebounce: React.FC<{
    ignoreInitialChange?: boolean;
    ignoreSelectionChange?: boolean;
    onChange: onChangeFunction;
    wait?: number
}> = ({ ignoreInitialChange= true, ignoreSelectionChange = false, onChange, wait= 167 }) => {

    const [editor] = useLexicalComposerContext();
    let timerId:  NodeJS.Timeout | null = null;

    useLayoutEffect(() => {
        return editor.registerUpdateListener(({
                                                  editorState,
                                                  dirtyElements,
                                                  dirtyLeaves,
                                                  prevEditorState
                                              }) => {
            if (ignoreSelectionChange && dirtyElements.size === 0 && dirtyLeaves.size === 0) {
                return;
            }

            if (ignoreInitialChange && prevEditorState.isEmpty()) {
                return;
            }
            if(timerId === null) {
              timerId = setTimeout(() => {
                editorState.read(() => {
                  const root = $getRoot();
                  onChange(JSON.stringify(editorState), root.getTextContent());
                })
              }, wait);
            } else {
              clearTimeout(timerId);
              timerId = setTimeout(() => {
                editorState.read(() => {
                  const root = $getRoot();
                  onChange(JSON.stringify(editorState), root.getTextContent());
                });
              }, wait);
            }
        });
    }, [editor, ignoreInitialChange, ignoreSelectionChange, onChange]);

    return null;
}

This is the code for the plugin and it is inspired (or copied) from OnChangePlugin of lexical Since, lexical is in early development the implementation of OnChangePlugin might change. And in fact, there is one more parameter added as of version 0.3.8. You can check the latest code at github.

The only thing I have added is calling onChange function in timer logic.

ie.

if(timerId === null) {
    timerId = setTimeout(() => {
      editorState.read(() => {
        const root = $getRoot();
        onChange(JSON.stringify(editorState), root.getTextContent());
      })
    }, wait);
} else {
  clearTimeout(timerId);
  timerId = setTimeout(() => {
    editorState.read(() => {
      const root = $getRoot();
      onChange(JSON.stringify(editorState), root.getTextContent());
    });
  }, wait);
}

If you are new to lexical, then you have to use declare this plugin as a child of lexical composer, something like this.

File: RichEditor.tsx

<LexicalComposer initialConfig={getRichTextConfig(namespace)}>
    <div className="editor-shell lg:m-2" ref={scrollRef}>
      <div className="editor-container">
        {/* your other plugins */}
        <RichTextPlugin
          contentEditable={<ContentEditable
            className={"ContentEditable__root"} />
          }
          placeholder={<Placeholder text={placeHolderText} />}
        />
        <OnChangeDebounce onChange={onChange} />
      </div>
    </div>
</LexicalComposer>

In this code, as you can see I have passed the onChange function as a prop and you can also pass wait in milliseconds like this.

<OnChangeDebounce onChange={onChange} wait={1000}/>

Now the last bit is the implementation of onChange function, which is pretty straightforward

const onChange = (editorStateJson:string, editorText:string) => {

  console.log("editorStateJson:", editorStateJson);
  console.log("editorText:", editorText);
    // send data to a server or to your data store (eg. redux)
};

Finally

Thanks to Meta and the lexical team for open sourcing this library. And lastly, the code I have provided works for me, I am no expert, feel free to comment to suggest an improvement.

  • Related