This guide explains how to extend a foundational Lexical node, i.e. nodes found under https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/, to work with Codox for real-time collaboration.
The following key steps will be explained:
- Extension: The recipes are largely similar for most Lexical nodes extensions
- Serialization: Converting the extended Lexical node into a Codox-compatible JSON format.
- Deserialization: Converting a Codox JSON node back into a Lexical-compatible format.
In this example, we consider a custom node, WarningNode that extends the basic TextNode.
A TextNode has the following standard properties: text, format, style, mode, detail ( see https://github.com/facebook/lexical/blob/88382127710e167cbdc608c7119edabfef503f27/packages/lexical/src/nodes/LexicalTextNode.ts#L294)). Among these properties text, format, style are directly mutable by users.
For instance,
textcontains a string that is potentially modified by multiple users concurrently.formatis a comma separated list of format values, e.g.bold,italic. Each of these format values are mutable individually, for instance a user might toggle the bold format where by the previous example would becomeitalic.styleis a colon separate list of css property value pairs, e.g."color: #d0021b;background-color: #f5a623". Simliar toformateach css prop-value pair is potentially modifiable individually via the UX.
In this example, we will extend the original TextNode to save the the font-color and background-color information as individual properties of the node object definition. We also introduce an identifier to identify the node. The semantics of this identifier can be customized to what is appropriate for the application. The point is to illustrate how to handle two types of node properties:
- Immutable custom metadata (e.g.,
__id). - Mutable style properties (e.g.,
__colorand__backgroundColor).
Below we show a subset of the standard approach to extending a Lexical base node with additional properties. This is just to demonstrate that from a node property extension point of view, there is nothing that is particularly different or difficult:
/**
* Assume you want to add the following attributes to an extended node
*/
__id; //some custom id that's immutable
__color; //color attribute that holds a string, and is mutable
__backgroundColor; //background color attribute that holds a string, and is mutable
constructor(id, color, backgroundColor, text, key) {
// invoke parent constructor
super(text, key);
// extension: set custom attributes, with default values
this.__id = id || 'mock_id';
this.__color = color || '#ffffff';
this.__backgroundColor = backgroundColor || '#ff4902';
}
exportJSON() {
return {
// invoke parent text node export
...super.exportJSON(),
version: 1,
/**
* extension: attach custom properties to the exported node
*/
id: this.__id,
color: this.__color,
backgroundColor: this.__backgroundColor,
type: 'warning',
};
}
/*
* Override TextNode createDOM
*/
createDOM(config) {
//Invoke parent createDOM
const dom = super.createDOM(config);
/**
* extension: Compose custom css styles string from custom attributes
*/
dom.style.cssText = `color:${this.__color};background-color:${this.__backgroundColor};`;
dom.className = 'warning-text-frame';
return dom;
}Once a node has been extended, its associated interactions programmed and tested, it is time to consider its relationship to Codox. Intuitively you need to tell Codox which properties can be modified, and therefore need to be merged and synchronized, as well as properties that are pure props that should not be considered for merging and synchronization (i.e. immutable). To express this via code, you need to implement two methods:
The method toCodoxNode transforms a Lexical node into a JSON structure acceptable by Codox. Here are the steps:
-
Extract Base JSON: Start by using the node's existing
exportJSON()method. This retrieves the base text node properties. -
Augment with Extended Properties:
- Mutable Properties: In the WarningNode example,
text,color,backgroundColorare all mutable and therefore Codox must be aware that they are mutable. Codox is already aware that the foundational TextNode contains atextproperty and therefore will handle the merging of text content automatically. That then leavescolorandbackgroundColoras 'new' properties that must be communicated to Codox. To pass these mutable properties into Codox, we can seralize the style propertie,colorandbackground-color, into a single CSS string and assign that string to thestyleproperty of the TextNode. Again, Codox is aware thatstyleexists and will appropriately parse the string into individual properties for merging. - Immutable Metadata: Attach a
codox_metadatacontainer object to house immutable properties, such as the extendedidproperty. Important: you should always include the extended node'stype, in this casetype: "warning"in thecodox_metadatacontainer object - this type ensures Codox is fully aware of the node's identity and that the extended type are preserved during the backward conversion.
- Mutable Properties: In the WarningNode example,
-
Construct the Codox Node: Putting it altogether the final Codox node as a JSON should include:
- Base text attributes (e.g.,
text,format,mode,detail, andversion). - A
stylestring reflecting the current mutable properties. - A
codox_metadatafield containing immutable details.
- Base text attributes (e.g.,
Example Code Snippet:
/**
* Implement the following API methods to interoperate with Codox
* Lexical -> Codox Node
* Converts lexical node to json node, acceptable by Codox.
*/
toCodoxNode() {
/**
* Step 1: Get original text node json. You should/must use .exportJSON()
*/
const originalJsonNode = this.exportJSON();
/**
* Step 2: Extend with additional properties.
*/
const convertedNode = {
type: 'text', // type property must be one of core or playground node types
// preserve default text node attributes
text: originalJsonNode.text,
format: originalJsonNode.format,
mode: originalJsonNode.mode,
detail: originalJsonNode.detail,
version: originalJsonNode.version,
// extension: all mutable style properties should be serialized as a style string, as per TextNode definition
// assumption is the lexical node style is empty, if not, concat the styles.
style: `color:${originalJsonNode.color};background-color:${originalJsonNode.backgroundColor};`,
/**
* extension: codox_metadata should hold immutables:
* - required: your extended node type, e.g. 'warning'
* - optional: any immutable properties of an original node - these fields are not merged and synchronized.
*/
codox_metadata: {
type: originalJsonNode.type, //type == 'warning'
id: originalJsonNode.id, //some custom id an immutable property
},
};
console.log('[DEMO DEBUG][WarningNode][toCodoxNode]: ', { convertedNode, originalJsonNode });
return convertedNode;
}Ensure that immutable properties (like the custom id and node type) are stored in a dedicated metadata object (codox_metadata) to avoid accidental overwrites.
Combine mutable style properties into a single string. This format is expected by Codox and will be subject to synchronization.
Once a node has been synchronized internally, Codox needs to know how to return a node representation that your application understands. To do that you need to provaide a static method that performs the opposite of the marshalling process described in the previous section.
The reverse process involves deserializing a codoxNode back into a Lexical-compatible format. This requires the extraction of style properties and reassigning both base and extended attributes. codoxNode will contain exactly the properties produced by the toCodoxNode function.
-
Extract the Style String Split the CSS
stylestring which reflects the merged and synnchronized font properties (which maybe different to the originalstyle) to retrieve the updated individual style attributes (e.g., color and background color). -
Rebuild the Lexical Node JSON Create a new JSON object that mirrors the Lexical node structure. Include:
- Base attributes:
text,format,mode,detail,version - Extended attributes: custom id, color, and background color (ensuring the type is correctly set)
/**
* Implement the following API methods to interoperate with Codox
* Codox -> Lexical Node
* Converts synchronized codox json node to lexical json node
* must be class 'static' method
*/
static fromCodoxNode(codoxNode) {
/**
* extension: all mutable properties should be deserialized from the style string,
*/
let color = '';
let backgroundColor = '';
codoxNode.style.split(';').forEach((css) => {
if (css.startsWith('color:')) {
color = css.split(':')[1];
}
if (css.startsWith('background-color:')) {
backgroundColor = css.split(':')[1];
}
});
// compose original json node from codox node
const originalJsonNode = {
// set original type - in this example it will be "warning"
type: codoxNode.codox_metadata.type,
// set base attributes
text: codoxNode.text,
format: codoxNode.format,
mode: codoxNode.mode,
detail: codoxNode.detail,
version: codoxNode.version,
style: '', // assumption here is the lexical node style is empty, if not, splice out the extended mutable properties
/**
* set custom attributes
*/
id: codoxNode.codox_metadata.id,
color: color,
backgroundColor: backgroundColor,
};
return originalJsonNode;
}