Closed med8bra closed 1 year ago
Editor is needed due to the fact HTML needs the EditorConfig to apply theming. See https://github.com/facebook/lexical/issues/2586 for some background and why it had to be closed.
You can always create a detached editor to do the rendering, or a headless editor and do the work on the server. Or simply store the HTML in state along with the editor state when you save the editor state.
The reason you can’t use the helper function in read only mode is exactly the same problem as the first above. In read only mode, Lexical doesn’t know if any active editor. It’s an editor to get the theme config to apply to the HTML elements.
Yes, I understood that part @trueadm. But as you said we only need EditorConfig
, not an Editor or an active Editor.
My use case is to generate HTML at form submission outside the editor component (don't want to pass the editor instance to the outer scope). Generating HTML every time is overkill.
If the $generateHtmlFromNodes
requires an Editor just to get theme config, why not accept directly an EditorConfig as a parameter?
A LexicalNode
has createDom(EditorConfig)
which means, a node needs just EditorConfig to generate its DOM representation.
@med8bra Well unfortunately, the exportDOM(editor: LexicalEditor)
method also requires passing of editor
too, as the editor is used to initialize nested editor state – for example image captions. So we making such a big breaking change would still require an editor to be available.
@trueadm Indeed, exportDOM
is taking an EditorInstance
, but I just did a search in the repository and I cannot find a case where the editor was useful to generating DOM, except:
Sorry, I didn't understand the use case for image captions, won't an ImageNode have the image caption in its state?
EditorState
and EditorConfig
(most of the code base respect that)
exportDOM
contract will be clear, with no side effects, taking two inputs (state and config) and returning the DOM element. To benefit from the two strategies, maybe we should define two different use cases:
Let me know what is your take on this
One workaround could be to expose a new core helper from lexical called $getEditor() which returns null | EditorInstance. Then Excalidraw could use that instead and we wouldn’t need to pass it to the html function? Did you want to take a look at making that change in a PR?
That would be a good workaround for now, The only issue I see is that if $getEditor
returns null, we will be putting more responsibility on LexicalNode.exportDOM
, as it should handle both cases.
Actually, I just read the ExcalidrawNode
implementation, it's not using the rendered canvas, but using an utility method to convert Excalidraw data to SVG. So it will be the same performance cost without an editor instance.
But I was thinking maybe we could have different modes:
exportToSvg
from @excalidraw/excalidraw
Document.importNode
I don't want to rush into writing a PR, but if we come up with a better approach I'll be pleased to implement it. Thanks for your time @trueadm
So my proposition is
LexicalNode.exportCurrentDOM
, takes Editor, rendered element, and target document, and optimize DOM generation using input paramtersLexicalNode.exportDOM
so it takes EditorConfig onlyLexicalEditor.exportDOM
: does basically what $generateHTMLfromNodes
but with new exportCurrentDOM
APIEditorState.exportDOM
: exports state (selection) to DOM using updated exportDOM
APIWhat's the status on this? Have you started a PR, @med8bra? I'd like to run "$generateHtmlFromNodes(editor, null)" (inside "editor.registerUpdateListener") whenever the user has not typed for N milliseconds, in order to send the content to a server, but the trouble is, I enable read-only mode (to remove red lines from chromes spelling check, etc.) when the user clicks outside the box. Or is there another way to export the content for syncing with a server while in read-only mode? Or should I rather do a hackish solution, listening on read-only state change and from there add/remove the other listener?
@norregaarden-scalgo I'm waiting for a confirmation from @trueadm to start on my proposition.
For your case, using LexicalOnChangePlugin
with a debounced onChange
callback will be sufficient.
@norregaarden-scalgo I'm waiting for a confirmation from @trueadm to start on my proposition.
For your case, using
LexicalOnChangePlugin
with a debouncedonChange
callback will be sufficient.
@med8bra thanks you for your suggestion, but the same error pertains using OnChangePlugin from @lexical/react/LexicalOnChangePlugin, i.e. $generateHtmlFromNodes can only be called from editor.read or editor.update, and if I wrap it in that, it says " Cannot use method in read-only mode." even when I'm, not in read-only mode, if the editor has been in read-only mode at some point.
is there any working temporary workaround to get an HTML string on change with $generateHtmlFromNodes
?
When the JSON serialization is not an option it seems a quite basic behaviour in order to save the user entered HTML and let them update it later on.
thanks!
@kuus Why can't you use $generateHtmlFromNodes
. That is the correct way to do what you're asking. You can do it in an update listener or using the OnChangePlugin as mentioned above.
$generateHtmlFromNodes can only be called from editor.read or editor.update, and if I wrap it in that, it says " Cannot use method in read-only mode." even when I'm, not in read-only mode, if the editor has been in read-only mode at some point.
Make sure you call $generateHtmlFromNodes
from an editor.update, if you are doing so, please can you share a sandbox with a repro case? It's hard to understand what is going wrong otherwise.
Hi @trueadm , thanks for the quick reply, I was trying to do it from editorState.read()
as such:
https://codesandbox.io/s/lexical-on-change-jervjw
import { useCallback, useEffect } from "react";
import { $getRoot, $getSelection, type EditorState } from "lexical";
import { $generateNodesFromDOM, $generateHtmlFromNodes } from "@lexical/html";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
type HTMLEditorContainerProps = {
defaultValue?: string;
};
export const HTMLEditorContainer = (props: HTMLEditorContainerProps) => {
const { defaultValue } = props;
const [editor] = useLexicalComposerContext();
const onChange = useCallback(
(editorState: EditorState) => {
editorState.read(() => {
const selection = $getSelection();
const html = $generateHtmlFromNodes(editor, selection);
// use `onChange` from props
console.log("onChange html", html); // gives error Cannot use method in read-only mode.
});
},
[editor]
);
// set defaultValue
useEffect(() => {
if (!defaultValue || !editor) {
return;
}
editor.update(() => {
const parser = new DOMParser();
const dom = parser.parseFromString(defaultValue, "text/html");
const nodes = $generateNodesFromDOM(editor, dom);
$getRoot().select();
const selection = $getSelection();
// @ts-expect-error not sure...
selection.insertNodes(nodes);
});
}, [editor]);
return (
<>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder="Placeholder..."
/>
<OnChangePlugin
onChange={onChange}
ignoreHistoryMergeTagChange
ignoreInitialChange
ignoreSelectionChange
/>
</>
);
};
and that was giving me the error Cannot use method in read-only mode.
If I do it from editor.update
it works instead:
https://codesandbox.io/s/lexical-on-change-iqexeo
import { useEffect } from "react";
import { $getRoot, $getSelection } from "lexical";
import { $generateNodesFromDOM, $generateHtmlFromNodes } from "@lexical/html";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
type HTMLEditorContainerProps = {
defaultValue?: string;
};
export const HTMLEditorContainer = (props: HTMLEditorContainerProps) => {
const { defaultValue } = props;
const [editor] = useLexicalComposerContext();
// set defaultValue and register onChange
useEffect(() => {
if (!defaultValue || !editor) {
return;
}
editor.registerUpdateListener(() => {
editor.update(() => {
const html = $generateHtmlFromNodes(editor, null);
// use `onChange` from props
console.log("onChange html", html);
})
});
editor.update(() => {
const parser = new DOMParser();
const dom = parser.parseFromString(defaultValue, "text/html");
const nodes = $generateNodesFromDOM(editor, dom);
$getRoot().select();
const selection = $getSelection();
// @ts-expect-error not sure...
selection.insertNodes(nodes);
});
}, [editor]);
return (
<>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder="Placeholder..."
/>
</>
);
};
is this latter the right way to do it?
a bit unrelated (if you like I can open an issue or ask on SO), is it possible to strip the classNames on the resulted HTML string when using $generateNodesFromDOM
@trueadm ?
@kuus There's no built in way to do so, your best bet is to apply a query selector on all the nodes and manually do element.removeAttribute('class')
. As for using editor.update, that should be fine :)
@kuus There's no built in way to do so, your best bet is to apply a query selector on all the nodes and manually do element.removeAttribute('class'). As for using editor.update, that should be fine :)
@trueadm For now I was able to accomplish that by using a kind of dummy headless editor whose state is in sync with the "real" one, the dummy editor has no theme applied to it so no html classes and the onChange
is bound to that. It works, but do you think is that a good approach?
That's a neat trick too!
@trueadm
I'm storing the json state. I'd like to parse this stored state and send an email (as HTML) on the server?
Right now - my best bet is to use a NodeJS instance on the server to transform JSON to HTML using $generateNodesFromDOM.
There seems to be no way to generate HTML from JSON with zero dependecies. Is this a valid use case for lexical to support?
Generating HTML is contextual for now, so it needs an editor instance to get the node classes from. There’s no way around that
Just to add to this, we still require an editor instance, but the fundamental issue of converting editor state to HTML in non-editable mode has been fixed. You can call editor.getEditorState().read(() => $generateHtmlFromNodes(editor, null))
with editable: false
and it should work fine - no need to call editor.update
any more.
Closing this as the original bug has been fixed, and we don't have any immediate plans to separate the editor instance from converting JSON state to HTML. That would require rewriting a lot of internal editor logic.
@kuus There's no built in way to do so, your best bet is to apply a query selector on all the nodes and manually do element.removeAttribute('class'). As for using editor.update, that should be fine :)
@trueadm For now I was able to accomplish that by using a kind of dummy headless editor whose state is in sync with the "real" one, the dummy editor has no theme applied to it so no html classes and the
onChange
is bound to that. It works, but do you think is that a good approach?
Are you able to use html classes from the original editor it is bound to, with that setup?
@kuus There's no built in way to do so, your best bet is to apply a query selector on all the nodes and manually do element.removeAttribute('class'). As for using editor.update, that should be fine :)
@trueadm For now I was able to accomplish that by using a kind of dummy headless editor whose state is in sync with the "real" one, the dummy editor has no theme applied to it so no html classes and the
onChange
is bound to that. It works, but do you think is that a good approach?Are you able to use html classes from the original editor it is bound to, with that setup?
hi @erikmartinessanches yes html classes are used in the editor shown to the user. I was looking to strip them out from what saved to the database in the "dummy headless editor" because I only needed to save basic html tags like b,i,em,strong,
etc., so there was no need to store html classes too in the database.
I'm storing
EditorState
as the value for a RichTextInput (controlled input), and I want on form submission to:JSON.stringify
does itCurrently, to generate HTML from Lexical, I need to use
Editor. update
with$generateHtmlfromNodes
, which has some issues:read-only
mode,Pleasure to contribute code
I'm open to contributing to this project if this feature is a valid use case.