Skip to content

Lexical

Codox Integration with Lexcial Editor

Overview

  • Codox provides a separate plugin for sync, following a lexical approach for designing external features and plugins. Plugin should be initialized as any other lexical plugin.

  • Codox plugin provides api to control sync start/stop via built-in React ref

  • Codox plugin implements a rollback mechanism after lexical core errors. Rollback is restoring editor state to previous. Plugin catches errors thrown from lexical editor. If client’s callback “onError” was passed into editor config, plugin will allow it to trigger and then will perform rollback.

  • Codox sync supports undo/redo. It relies on a built-in lexical undo/redo implemented with lexical HistoryPlugin provided by the official package “@lexical/react/LexicalHistoryPlugin”. For undo/redo support in editor, should init HistoryPlugin along with Codox plugin. Any other custom client’s implementations of undo/redo can be unstable with respect to sync.

  • Codox sync implements the restrictions of inserted content - the blacklisted content combinations, which are not allowed to be inserted into the editor. Codox plugin prevents inserting such combinations. See Content Blacklisting section for blacklist details.

  • Codox sync supports sync of all lexical nodes types from lexical core and all custom nodes from the official playground. Dynamic support of other custom nodes is in progress.

  • Codox plugin accepts init state during sync start and applies it to editor, then on content changes, plugin will trigger client’s callback passed as part of plugin configuration. Client’s app should not do any direct update ops to editor state. Read ops are not limited.

  • Codox provides an additional helper util to validate editor state json. Checks if state is valid for lexical. Throws error if state is invalid

  • Codox plugin has built-in mechanism to validate and fix lexical tables structures, if lexical core produces any incorrect table structure. This logic is encapsulated and prevents output of broken table structures from lexical

Requirements for Codox sync integration

  • Codox plugin must be initialized in the scope Lexical Composer context. If it’s not, plugin with throw error.

  • Initial state, fetched from client’s backend, should be passed to codox plugin when invoking sync start. Client’s app must delegate applying init state to codox plugin and should not apply it directly to sync - this may cause incorrect sync.

  • Sync must be started only after init state has already been fetched from backend

  • All lexical nodes which are to be registered in lexical, must be passed through codox util “registerNodesWithCodox” before passing it to lexical editor initial configuration. This step is critical for further correct sync work.

Installation of codox sync provider

Install codox provider npm package with codox components into project dependencies:

Terminal window
npm install @codoxhq/lexical-provider

Editor intitial config update for sync integration

Import register nodes util from codox provider

import {registerNodesWithCodox} from “@codoxhq/lexical-provider”

Wrap lexical nodes into registerNodesWithCodox() util and pass wrapped nodes into lexical init configuration for lexical editor:

// all nodes to register in lexical (lexical nodes classes)
let nodes = [HeadingNode, ListNode, ListItemNode, ….]
// critical for sync to wrap nodes
let nodesToRegister = registerNodesWithCodox(nodes)
// init lexical editor config
const initEditorConfig = {
editorState: null,
namespace: “[Your namespace name]”,
nodes: nodesToRegister,
....rest of config
}
.
<LexicalComposer initialConfig={initEditorConfig}>
….plugins
</LexicalComposer>

Codox plugin config reference

Codox plugin accepts config and ref props. Config contains all attributes to configure codox for sync launch and ref is provided to be able to access codox start/stop api to control when to start and stop codox sync

// create react ref for codox provider
const codoxProviderRef = useRef()
<CodoxCollabPlugin
ref={codoxProviderRef}
config={{
docId: "[current document id]",
username: "[current user name]",
apiKey: "[api key provided by codox]",
onEditorStateChange=[callback triggered when editor state changed]
// optional
onBlackListedInsert=[callback triggered when blacklisted content combination was found]
}}
/>
onEditorStateChange

Codox plugin accepts the callback from client’s app, which will be triggered each time, when content state is changed in editor. This callback should be used to listen to state changes and update app’s database.

const onEditorStateChange = ({
docId, // id of current document - passed into CodoxCollabPlugin config
state, // editor content state already converted from lexcial to json obj
isRemoteChange, // boolean flag to identify if change was made locally or not
}) => {
// do your custom logic here
/*
NOTE: state will include "comments" array along with "root"
if comments not used, the array will always be empty
*/
};

It is recommended to update database only on local changes trigger.

onBlacklistedInsert

Optional callback which is triggered each time, when any blacklisted content combination was found and was blocked. Client’s app can listen to such cases and somehow notify users that operation was blacklisted, e.g. show some modal or pop-up.

const onBlackListedInsert = () => {
// do your custom logic here - notify user, etc
};

Client’s app must not stop codox in such cases or do any other operations with editor - all required logic is done internally by codox plugin.

Codox plugin sync start/stop api reference

Codox plugin provides an api to control start/stop of sync via React refs mechanism. Created ref should be passed to codox plugin and later can be used to invoke start/stop of codox sync.

// create react ref
const codoxProviderRef = useRef()
// start codox sync
const startCodox = (initState) => {
codoxProviderRef.current.start(initState)
}
// stop codox sync - usually invoke it in useEffect return callback
const stopCodox = () => {
codoxProviderRef.current.stop()
}
....
<CodoxCollabPlugin
ref={codoxProviderRef} // pass ref to plugin
config={config object}
/>

Codox sync blacklisting policy

Codox plugin defines a set of content combinations which are not allowed. Such content create/insert is blocked and the “onBlacklistedInsert” callback is invoked, if provided. Purpose of blacklisting is to prevent any weird content combinations including those which does not make any sense. Current content combinations blacklist:

Target (child lexical node types)Forbidden to insert into (parent lexical node type)
“table”“table”, “image”, “inline-image”, “sticky”
“image”“image”, “inline-image”, “sticky”
“horizontalrule”“table”, “image”, “inline-image”, “sticky”
“page-break”“table”, “image”, “inline-image”, “sticky”
“collapsible-container”“table”, “image”, “inline-image”, “sticky”, “collapsible-container”
“layout-container”“table”, “image”, “inline-image”, “sticky”, “layout-container”
“inline-image”“image”, “inline-image”, “sticky”
“poll”“image”, “inline-image”, “sticky”
“excalidraw”“image”, “inline-image”, “sticky”

Should read table as following (1st row as example): “Table is forbidden to insert into table, image, inline-image and sticky”

Codox state validation util for editor content state

Codox provides extra helper util to validate json state object and ensure it is valid for lexical. Util can be used at any time and any place of client’s app code.
It is recommended to validate initial state, received from app’s backend.
Validation will throw error if state is not valid - should wrap it into try-catch.
Validation requires the same list of registered nodes that is passed into initial editor config

import {validateStateStructure} from "@codoxhq/lexical-provider"
....
// all nodes to register in lexical (custom and lexical nodes classes)
let nodes = [HeadingNode, ListNode, ListItemNode, ….]
let nodesToRegister = registerNodesWithCodox(nodes) // critical for sync to wrap nodes
....
// fetching initial state
useEffect(() => {
try {
const initState = fetch(....) // fetch init state
validateStateStructure(initState, nodesToRegister)
} catch(err) {
// capture error here
}
}, [])

Codox extra plugins for Lexical Editor

Codox provider lib provides few extra plugins for extra functionality:

CodoxCommentPlugin

Separate plugin for support of comments. Has build-in ui for displaying comments and add/remove comments and threads. Along with plugin, the special editor command is exposed - to invoke comments creation from any place or other plugin

import {CodoxCollabPlugin, CodoxCommentPlugin, INSERT_COMMENT} from "@codoxhq/lexcial-provider"
// init both plugins along with other plugins
<LexicalComposer>
//....other plugins
<CodoxCollabPlugin {codox config}/>
<CodoxCommentPlugin/> // init comments plugin
</LexicalComposer>

Comments plugin has built-in ui:

  • A button in top-right of page, click on it opens sidebar on the right. Sidebar renders all current active comment threads and has functionality to remove threads and add/remove comments.
  • when any range of content is selected, extra button is rendered on the right side of the page, click on it opens a modal to create new comment for selected content range

Extra command is exposed from codox provider lib to allow to trigger comment add from any part of editor ui, e.g. from any floating sidebars, etc

import {INSERT_COMMENT} from "@codoxhq/lexical-provider"
....
editor.dispatchCommand(INSERT_COMMENT) // will trigger modal open for comment add

All comments are exposed in editor state json obj along with root, when state is exposed with “onEditorStateChange” callback:

const {
root, // content tree
comments, // comments array - is empty when no comments or comments not used
} = state;
CodoxFillBGColorPlugin

Extra plugin to change text background fill color.
Plugin comes from built-in ui - button click on which opens a color palette for background color pick. Can be utilized in any other lexical plugin, for example in toolbar plugin.
Plugin has set of optional props for css customization and listen to color changes:

import {CodoxFillBGColorPlugin} from "@codoxhq/lexical-provider"
// somewhere in toolbar plugin
<CodoxFillBGColorPlugin
onColorChange={(latestColor) => {
// invoked when bg color changes,
}}
buttonLable={btn title, defaults to ""}
buttonAriaLabel={btn aria attr name, defaults to ""}
buttonClassName={defaults to "toolbar-item color-picker"}
buttonLabelClassName={defaults to "text dropdown-button-text"}
buttonIconClassName={defaults to "icon bg-color"}
dropdownClassName={defaults to "dropdown"}
dropdownChevronClassName={defaults to "chevron-down"}
/>
CodoxFontColorPlugin

Extra plugin to change text font color.
Plugin comes from built-in ui - button click on which opens a color palette for text color pick. Can be utilized in any other lexical plugin, for example in toolbar plugin.
Plugin has set of optional props for css customization and listen to color changes:

import {CodoxFontColorPlugin} from "@codoxhq/lexical-provider"
// somewhere in toolbar plugin
<CodoxFontColorPlugin
onColorChange={(latestColor) => {
// invoked when bg color changes
}}
buttonLable={btn title, defaults to ""}
buttonAriaLabel={btn aria attr name, defaults to ""}
buttonClassName={defaults to "toolbar-item color-picker"}
buttonLabelClassName={defaults to "text dropdown-button-text"}
buttonIconClassName={defaults to "icon font-color"}
dropdownClassName={defaults to "dropdown"}
dropdownChevronClassName={defaults to "chevron-down"}
/>

Undo/Redo support for Codox sync

Codox sync supports undo/redo out-of-the-box. To enable undo/redo support, should use built-in lexical HistoryPlugin:

import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';

Sync for undo/redo relies on history plugin built-in implementation of undo/redo stacks.

Important: It is not guaranteed to have correct and stable sync for undo/redo in case of using any custom undo/redo implementations

Basic Codox sync intengration example code snippet

Demo playground

// main editor component
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { CodoxCollabPlugin, registerNodesWithCodox, validateStateStructure } from '@codoxhq/lexical-provider';
const nodes = [
/*Put all nodes classes here to register in lexical*/
];
/*
* Wrap all nodes classes with Codox registerNodesWithCodox util.
* This step is critical for correct sync work.
*/
const LEXICAL_NODES_TO_REGISTER = registerNodesWithCodox([...nodes]);
export default function App() {
const currentDocId = 'demo_doc_id';
// flag for codox start
const [codoxStarted, setCodoxStarted] = useState(false);
// codox provider ref for start/stop api
const codoxProviderRef = useRef();
const startCodox = (initState) => {
if (codoxProviderRef.current) {
/**
* Should start codox with initial state for current doc id
* Codox will apply initial state to editor
*/
codoxProviderRef.current.start(initState);
setCodoxStarted(true);
}
};
const stopCodox = () => {
if (codoxProviderRef.current) {
codoxProviderRef.current.stop();
}
setCodoxStarted(false);
};
// fetch init state on mount and start codox sync
useEffect(() => {
(async () => {
// Stop sync if already running
if (codoxStarted) {
stopCodox();
}
let response = await fetch(/*url for fetching init state*/);
let initState = response.json();
// wrap in try-catch: validateStateStructure may throw if state is invalid
try {
// validate state from backend
validateStateStructure(initState, LEXICAL_NODES_TO_REGISTER); // if invalid - will throw
// start codox sync with init state
startCodox(initState);
} catch (err) {
// do custom logic here, e.g. fetch retry or else
}
})();
}, []);
/**
* Initial config for Lexical Composer - init it only once
*/
const initLexicalConfig = useMemo(() => {
return {
editorState: null, // use null as init state, when init state is fetched, it will be applied by codox
namespace: `Demo`, // can use own namespace name, "Demo" is for example here
nodes: LEXICAL_NODES_TO_REGISTER, // should wrap nodes into codox register fn
onError: (error) => {
// custom error handler, can do smth custom here
},
// css theme for styling editor - see lexcial docs for details
// theme: CustomTheme,
};
}, []);
// triggered by CodoxCollabPlugin on content state changes
const onEditorStateChange = ({ docId, state, isRemoteChange = false } = {}) => {
/**
* Should save to database ONLY on local changes - when isRemoteChange flag is false
*/
if (isRemoteChange) return;
/*
do custom logic here to save state to database
*/
};
// triggered by CodoxCollabPlugin when blacklisted content combination rejected
let onBlacklistedInsert = () => {
/*
do custom logic here, e.g. notify user that content change was rejected.
Note: no need to do any other actions, like restart sync or else,
CodoxCollabPlugin handles such cases internally. Content state will
remain same as before blacklisted content insert
*/
};
const Placeholder = () => <div className="Placeholder__root">{'Enter some rich text...'}</div>;
return (
<LexicalComposer initialConfig={initLexicalConfig}>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some rich text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
{/*any other plugins here*/}
{/*init Codox colalboration plugin*/}
<CodoxCollabPlugin
// CodoxCollabPlugin can be inited here or inside EditorPlugins with other plugins
// Important to put it inside LexicalComposer component as other plugins
ref={codoxProviderRef}
config={{
//NOTE: initState should be passed into codoxProviderRef start() call
docId: currentDocId,
apiKey: process.env.REACT_APP_CODOX_API_KEY, // api key provided by codox
username: 'demoUserName', // client user name - use real unique username here instead of demo name
onEditorStateChange: onEditorStateChange, // callback to trigger on state changes
onBlacklistedInsert: onBlacklistedInsert, // callback to trigger when attempt to insert/paste blacklisted content combination
}}
/>
</LexicalComposer>
);
}

Extended Codox sync intengration example code snippet with comments, enabled extra plugins and undo/redo

Demo playground

Main editor component code:

// main editor component
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
// custom toolbar component
import ToolbarPlugin from './ToolbarPlugin';
import {
CodoxCollabPlugin,
registerNodesWithCodox,
validateStateStructure,
CodoxCommentPlugin,
} from '@codoxhq/lexical-provider';
const nodes = [
/*Put all nodes classes here to register in lexical*/
];
/*
* Wrap all nodes classes with Codox registerNodesWithCodox util.
* This step is critical for correct sync work.
*/
const LEXICAL_NODES_TO_REGISTER = registerNodesWithCodox([...nodes]);
export default function App() {
const currentDocId = 'demo_doc_id';
// flag for codox start
const [codoxStarted, setCodoxStarted] = useState(false);
// codox provider ref for start/stop api
const codoxProviderRef = useRef();
const startCodox = (initState) => {
if (codoxProviderRef.current) {
/**
* Should start codox with initial state for current doc id
* Codox will apply initial state to editor
*/
codoxProviderRef.current.start(initState);
setCodoxStarted(true);
}
};
const stopCodox = () => {
if (codoxProviderRef.current) {
codoxProviderRef.current.stop();
}
setCodoxStarted(false);
};
// fetch init state on mount and start codox sync
useEffect(() => {
(async () => {
// Stop sync if already running
if (codoxStarted) {
stopCodox();
}
let response = await fetch(/*url for fetching init state*/);
let initState = response.json();
// wrap in try-catch: validateStateStructure may throw if state is invalid
try {
// validate state from backend
validateStateStructure(initState, LEXICAL_NODES_TO_REGISTER); // if invalid - will throw
// start codox sync with init state
startCodox(initState);
} catch (err) {
// do custom logic here, e.g. fetch retry or else
}
})();
}, []);
/**
* Initial config for Lexical Composer - init it only once
*/
const initLexicalConfig = useMemo(() => {
return {
editorState: null, // use null as init state, when init state is fetched, it will be applied by codox
namespace: `Demo`, // can use own namespace name, "Demo" is for example here
nodes: LEXICAL_NODES_TO_REGISTER, // should wrap nodes into codox register fn
onError: (error) => {
// custom error handler, can do smth custom here
},
// css theme for styling editor - see lexcial docs for details
// theme: CustomTheme,
};
}, []);
// triggered by CodoxCollabPlugin on content state changes
const onEditorStateChange = ({ docId, state, isRemoteChange = false } = {}) => {
/**
* Should save to database ONLY on local changes - when isRemoteChange flag is false
*/
if (isRemoteChange) return;
/*
do custom logic here to save state to database
*/
};
// triggered by CodoxCollabPlugin when blacklisted content combination rejected
let onBlacklistedInsert = () => {
/*
do custom logic here, e.g. notify user that content change was rejected.
Note: no need to do any other actions, like restart sync or else,
CodoxCollabPlugin handles such cases internally. Content state will
remain same as before blacklisted content insert
*/
};
const Placeholder = () => <div className="Placeholder__root">{'Enter some rich text...'}</div>;
return (
<LexicalComposer initialConfig={initLexicalConfig}>
{/*NOTE: plugins can be wrapped into any markup according to app's style*/}
{/*custom toolbar for editor*/}
<ToolbarPlugin />
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some rich text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
{/*init history plugin for undo/redo support - see lexical docs for details*/}
<HistoryPlugin />
{/*any other plugins here*/}
{/*init Codox colalboration plugin*/}
<CodoxCollabPlugin
// CodoxCollabPlugin can be inited here or inside EditorPlugins with other plugins
// Important to put it inside LexicalComposer component as other plugins
ref={codoxProviderRef}
config={{
//NOTE: initState should be passed into codoxProviderRef start() call
docId: currentDocId,
apiKey: process.env.REACT_APP_CODOX_API_KEY, // api key provided by codox
username: 'demoUserName', // client user name - use real unique username here instead of demo name
onEditorStateChange: onEditorStateChange, // callback to trigger on state changes
onBlacklistedInsert: onBlacklistedInsert, // callback to trigger when attempt to insert/paste blacklisted content combination
}}
/>
{/*init Codox comments plugin*/}
<CodoxCommentPlugin />
</LexicalComposer>
);
}

Custom Toolbar component code:

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
CodoxFillBGColorPlugin,
CodoxFontColorPlugin,
INSERT_COMMENT, // special command to invoke new comment insert
} from '@codoxhq/lexical-provider';
// your custom toolbar implementation
export default function ToolbarPlugin() {
// get ref to current editor instance
const [editor] = useLexicalComposerContext();
/*
any custom logic for implementation of ui here
*/
/**
* Example of how to trigger new comment insert from any plugin outside codox comments plugin.
* Note: comment insert can be invoked from any other plugin.
*
* Dispatching the command will open modal for new comment insert on current selection.
* Modal ui is a part of CodoxCommentPlugin implementation.
*/
const insertComment = () => {
/*
simply dispatch command, provided by codox provider, to editor.
See the lexical docs for details of commands concepts.
*/
editor.dispatchCommand(INSERT_COMMENT, undefined);
};
return (
<div>
{/*all other ui elements here*/}
{/*will render btn for text color change - click will open color pallette*/}
<CodoxFontColorPlugin
onColorChange={(latestColor) => {
//Callback is invoked when font color is changed - outputs latest applied color
// console.log('[ToolbarPlugin]: latest font color applied: ', { latestColor });
}}
buttonLabel="" // can pass custom btn name
buttonAriaLabel="Formatting text color" // can use any own name
// Set of css props - can customize elements with own classes
buttonClassName="toolbar-item color-picker"
buttonLabelClassName="text dropdown-button-text"
buttonIconClassName="icon font-color"
dropdownClassName="dropdown"
dropdownChevronClassName="chevron-down"
/>
{/*will render btn for text bg color change - click will open color pallette*/}
<CodoxFillBGColorPlugin
onColorChange={(latestColor) => {
// Callback is invoked when font color is changed - outputs latest applied color
// console.log('[ToolbarPlugin]: latest bg fill color applied: ', { latestColor });
}}
buttonLabel="" // can pass custom btn name
buttonAriaLabel="Formatting background color" // can use any own name
//Set of css props - can customize elements with own classes
buttonClassName="toolbar-item color-picker"
buttonLabelClassName="text dropdown-button-text"
buttonIconClassName="icon bg-color"
dropdownClassName="dropdown"
dropdownChevronClassName="chevron-down"
/>
{/*simple example of button to invoke comment insert*/}
<button onClick={insertComment}>Insert Comment</button>
</div>
);
}