Search code examples
reactjslexicaljs

Lexical Custom Node not working with Enter Key


Need to create a custom node to create heading1, formatted and normal fonts on choosing each button and created a custom node for it.

NewNode.js

import { ElementNode } from 'lexical';

export class NewNode extends ElementNode {

    constructor(payload, key) {
        super(key);
        this._payload = payload;
    }

    createDOM(_config) {
        const element = document.createElement(this._payload.type);
        const keys = this._payload.theme.split('.');
        // Use reduce to traverse the theme object dynamically
        const className = keys.reduce((obj, key) => obj && obj[key] !== undefined ? obj[key] : undefined, _config.theme);
        element.className = className;
        return element;
    }

    static clone(node) {
        return new NewNode(node._payload, node.__key);
    }

 
    updateDOM() {
        return false;
    }

    exportJSON() {
        return {
            type: 'NewNode',
            version: 1,
            children: [],
            format: '',
            indent: 1,
            direction: null
        };
    }
}

Plugin.js

import { useEffect } from 'react';

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $setBlocksType } from '@lexical/selection';
import {
    $getSelection,
    createCommand
} from 'lexical';

import { NewNode } from '../NewNode';

export const NEW_FORMAT_COMMAND = createCommand('NEW_FORMAT_COMMAND');

export const $createNewNode = (payload) => new NewNode(payload);

export const StylePlugin = () => {
    const [editor] = useLexicalComposerContext();

    if (!editor.hasNode(NewNode)) {
        throw new Error('"NewNode" does not register on editor!');
    }

    useEffect(() => {
        return editor.registerCommand(
            NEW_FORMAT_COMMAND,
            (payload) => {
                const selection = $getSelection();
                $setBlocksType(selection, () => $createNewNode(payload));
                return true;
            },
           1,
        );
    }, []);

    return null;
};

Its working as expected when $createNewNode(payload) is replaced by $createHeadingNode('h1'). But I want to use the newNode to display the changes. However the enter key doesn't seem to be working with the newNode

I can't figure out why the issue is being caused. On pressing enter key, a new paragraph is supposed to be added.


Solution

  • To my understanding, the issue you're facing is that after the dispatchCommand to convert the selection into a CustomNode, pressing on the Enter key doesn't work.

    This is because your CustomNode extends the ElementNode which by default has a dummy implementation of the insertNewAfter method, which is called when a newline is inserted (when the Enter key is pressed). The default implementation of insertNewAfter from ElementNode class returns null and doesn't do anything, so it must be overridden.

    Other Nodes that extend ElementNode like HeadingNode and ParagraphNode both override the insertNewAfter with node specific heuristics, hence resulting in different behaviors when the Enter is pressed on them.

    With all that said, CustomNode should look something like this, with a couple of corrections to the the original implementation included with descriptive comments:

    import { $createParagraphNode, EditorConfig, ElementNode, LexicalNode, NodeKey, RangeSelection } from "lexical";
    import { $createCustomNode } from "./StackOverNodePlugin";
    
    export class CustomNode extends ElementNode {
      _payload: { type: string; theme: string };
      constructor(payload: { type: string; theme: string }, key?: NodeKey) {
        super(key);
        this._payload = payload;
      }
    
      createDOM(_config: EditorConfig): HTMLElement {
        const element = document.createElement(this._payload.type);
        const keys = this._payload.theme.split(".");
        // Use reduce to traverse the theme object dynamically
        // FIX!!! Type 'EditorThemeClasses' is not assignable to type 'string' !!!FIX
        // const className = keys.reduce((obj, key) => (obj && obj[key] !== undefined ? obj[key] : undefined), _config.theme);
        // only strings should be assigned to className
        element.className = "";
        return element;
      }
    
      static clone(node: CustomNode) {
        return new CustomNode(node._payload, node.__key);
      }
    
      static getType() {
        // FIX!!! Must be the same as type used in exportJSON !!!FIX
        return "customNode";
      }
    
      /**
       * Returning false tells Lexical that this node does not need its
       * DOM element replacing with a new copy from createDOM.
       */
      updateDOM() {
        return false;
      }
    
      /**
       * `insertNewAfter` override
       *
       * @param selection
       * @param restoreSelection
       * @returns {null | LexicalNode}
       */
      insertNewAfter(selection: RangeSelection, restoreSelection?: boolean): null | LexicalNode {
        const anchorOffet = selection ? selection.anchor.offset : 0;
        const lastDesc = this.getLastDescendant();
        const isAtEnd =
          !lastDesc ||
          (selection && selection.anchor.key === lastDesc.getKey() && anchorOffet === lastDesc.getTextContentSize());
    
        // This implementation returns a new `ParagraphNode` if the cursor (selection) is at the end of the `CustomNode` or there is no selection else it returns a `CustomNode` at the new line.
        const newElement = isAtEnd || !selection ? $createParagraphNode() : $createCustomNode(this._payload);
    
        // This implementation always returns a `CustomNode`
        // const newElement = $createCustomNode(this._payload);
    
        const direction = this.getDirection();
        newElement.setDirection(direction);
        this.insertAfter(newElement, restoreSelection);
        if (anchorOffet === 0 && !this.isEmpty() && selection) {
          const paragraph = $createParagraphNode();
          paragraph.select();
          this.replace(paragraph, true);
        }
        return newElement;
      }
    
      exportJSON() {
        return {
          ...super.exportJSON(),
          type: "customNode",
          version: 1,
          // FIX!!! `ElementNode.exportJSON` must(should) be called to avoid serialization errors !!!FIX
          //   children: [],
          //   customValue: "custom value",
          //   format: "",
          //   indent: 1,
          //   direction: null,
        };
      }
    }
    
    

    Lexical's documentation is not the best, sometimes you just have to go through the GitHub codebase, to better understand implementations and resolve bugs.