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.
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.