Issues with usage of Rich Text Editor as custom widget in A12 Client (Form Engine)

Hi,

Has anybody used the rich text editor as a custom widget in their project or knows how to integrate it correctly with A12 client?

I have integrated it into the Form Engine by using FormEngineViews.FormEngineTpl and customizing the TextAreaStateless in WidgetMap. However, I am facing at least two problems:

  1. The editor is first initialized when the model is loaded. At that time, props.value is still undefined. Therefore, providing the value via initialConfig.editorState has no effect. When the actual document data is loaded, it’s too late — because the editor is already initialized.
<DefaultRichTextEditor
    initialConfig={{
        namespace: "Rich-Text-Editor"
        editorState: prepopulatedRichText(props.value ?? "")
    }}
    ...
/>
  1. activeEditorState is undefined when I try to call $getRoot on Init, but not only. So this config
<DefaultRichTextEditor
    initialConfig={{
        namespace: "Rich-Text-Editor",
        editorState: (() => {
            const root = $getRoot();

            if (root.getFirstChild() === null) {
                const paragraph = $createParagraphNode();
                paragraph.append($createTextNode("text"));
                root.append(paragraph);
            }
        })
    }}
    ...
/>

leads to JS error:

Error: Unable to find an active editor state. State helpers or node methods can only be used synchronously during the callback of editor.update(), editor.read(), or editorState.read(). Detected on the page: 0 compatible editor(s) with version 0.31.2+dev.cjs

I also tried to integrate the native rich text editor from lexical without any customization of A12 widget – similar issues:

<LexicalComposer initialConfig={initialConfig}>
    <RichTextPlugin
        contentEditable={
            <ContentEditable
                aria-placeholder={'Enter some text...'}
                placeholder={<div>Enter some text...</div>}
            />
        }
        ErrorBoundary={LexicalErrorBoundary}
    />
    <HistoryPlugin />
    <AutoFocusPlugin />
</LexicalComposer>

So it looks like this is actually an A12 Client or Engines issue and not a problem with the custom widget itself.

Regards,
Foued

In the STBK project, we’ve integrated Rich text editor using a similar idea.
It might be helpful to check that implementation and compare it with your

<StyledWrapperRichTextEditor $disabled={disabled} $readonly={readonly}>
    <DefaultRichTextEditor
       {...(value && {
          initialConfig: {
             editorState: initialEditorStateFromHTMLString(value)
          }
       })}
       minHeight={80}
       maxHeight={500}
       autoExpand
       spellCheck
       staticToolbarButtons={TOOLBAR_BUTTONS}
       {...toEditorProps(inputProps)}
       {...props}
    >
       <HandleChangeOnBlurPlugin onChange={handleValueChanged} />
    </DefaultRichTextEditor>
</StyledWrapperRichTextEditor>


const handleValueChanged = (value?: string) => {
		if (value) {
			const textContent = getTextContentFromHtmlString(value).trim();
			eventHandlers.onValueChange(path, textContent ? value : null, elementPath);
		}
	};

/**
 * This function is used to initialize an HTML string to a LexicalEditor state.
 * @see https://lexical.dev/docs/concepts/serialization#html---lexical
 */
export const initialEditorStateFromHTMLString = (htmlString: string) => (editor: LexicalEditor) => {
	const sanitizedHtml = DOMPurify.sanitize(htmlString);
	// In the browser you can use the native DOMParser API to parse the HTML string.
	const parser = new DOMParser();
	const dom = parser.parseFromString(sanitizedHtml, "text/html");
	// Once you have the DOM instance it's easy to generate LexicalNodes.
	const nodes = $generateNodesFromDOM(editor, dom);
	$getRoot().append(...nodes);
};

export const getTextContentFromHtmlString = (htmlString: string): string => {
	const parser = new DOMParser();
	const dom = parser.parseFromString(htmlString, "text/html");
	const editor = createEditor();

	let textContent = "";

	editor.update(() => {
		const nodes = $generateNodesFromDOM(editor, dom);
		$getRoot()
			.clear()
			.append(...nodes);
		textContent = $getRoot().getTextContent();
	});

	return textContent;
};

Thanks @thiem-free-cairn for your answer.

Your code example is interesting, but it hasn’t resolved all issues for me – at least not yet.

I’m posting my current progress here as a basis for further discussion. This is not a final solution yet.

One issue was that the RichTextEditor was rendered and initialized too early, before the document was loaded. My current workaround is to only render the component after I’ve confirmed all activity dataLoaders have finished (i.e., none are in “loading” state). This ensures that props.value is set correctly when the RichTextEditor is rendered.

The other issue with “no active editor” was more persistent. I decided to use the native Lexical editor instead of the A12 widget, which is working so far. In further tests, I realized that the InlineStyleTextNode (which is passed as an array item in nodes for the initialConfig) might actually be the root cause. Maybe it tries to access the editor before it is initialized?

Here’s the error I still face when I include InlineStyleTextNode as it is defined in defaultConfig of A12 RichTextEditor:

Error: Unable to find an active editor. This method can only be used synchronously during the callback of editor.update() or editor.read(). Detected on the page: 0 compatible editor(s) with version 0.31.2+dev.esm
Stacktrace
at LexicalComposer (webpack-internal:///./node_modules/@lexical/react/LexicalComposer.dev.js:52:3)

If you or anyone else has any further suggestions, I’d really appreciate your input!

Hi @foued-soft-queue,

i think i know the root cause of your problem since i stumbled over recently.

My guess
Its a problem with the module imports of your JS project.

On my side the problem was that there was a clash between .js and .esm.js.

My project did a common js build but a12 libs itself used esm modules on their imports.

So the project import did use common js implementation of import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext" and widgets itself the esm impl.

So in fact two contexts were created one from widgets and one from my implementation.

My solution and learnings:

  1. for lexical packages add webpack resolve alias for each importable file and the package root itself a little node script can ease this

    module.exports = {
       //...,
       resolve: {
          //...,
          alias: {
             // lexical main entry point
             lexical: Path.join(__dirname, "node_modules", "lexical", "Lexical.mjs"),
             // each file entry point resolved by custom nodescript resolveAliases for the package my project needed
             ...resolveAliases("lexical"),
             ...resolveAliases("@lexical/react"),
             ...resolveAliases("@lexical/html")
          }
       }
       //...
    }
    
  2. furthermore its important to call the lexical $… methods only in callbacks ot the editor functions like editor.update or editorstate.read

Maybe this will help on your case :slight_smile:

I would recommend to use the FormEngineViews.FormEngine component in your case because the component contains already the loading logic that you need. This will prevent your initialization issue.

If you want to support updates of values by middleware or saga, then you need to write your own lexical plugin that takes your value and tracks external changes and sets the value to the editor.

export const ExternalValueChangePlugin: ComponentType<{ value: string }> = function ExternalValueChangePlugin({ value }) {
	const [editor] = useLexicalComposerContext();

	useEffect(() => {
		// IMPORTANT: Here is a check missing that the change is triggered externally and not by the richtext editor itself!
		const parser = new DOMParser();
		const dom = parser.parseFromString(DOMPurify.sanitize(value), "text/html");

		root.clear();
		const nodes = $generateNodesFromDOM(editor, dom);
		nodes.forEach(node => root.append(node));
	}, [editor, value]);

	return null;
};

In order to prevent further issue it is also critical to prevent unnecessary updates to the component by creating new objects. You can prevent this by using useMemo for example.

Thanks @ mschmahl and @stefan-cold-haze for your responses.

I’ve paused work on the RTE for now since the lexical‑native alternative worked well. But I still want to try your suggestions and give feedback, so I’ll come back to it later.