Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plain/RichTextPlugin to handle initialEditorState + editor.isReadOnly #1358

Merged
merged 10 commits into from
Feb 24, 2022

Conversation

zurfyx
Copy link
Member

@zurfyx zurfyx commented Feb 24, 2022

Same problem as #1335, different approach


Problem:

Potential race condition between initializing the content, event handlers (through commands) and the user typing on the editable, currently handled by BootstrapPlugin, Rich/PlainTextPlugin and LexicalContentEditable

Solution:

Have the Rich/PlainTextPlugin handle the content editable creation and events. Additionally, add a _isReadOnly property to be able to orchestrate React's lifecycle with the contenteditable. (readonly also serves its most obvious purpose, be able to mark the editor as readonly)

How does this work?

React SSR does not load useLayoutEffect nor useEffect. Technically, useLayoutEffect does run but we do replace it for useEffect in useLayoutEffect.js. (We are particularly interested in useLayoutEffect because of its synchronous behavior before render)

At the same, the editor is unusable before hydration so we have to make sure that the editor is contenteditable="false" before hydration.

So we can leverage useLayoutEffect to perform the swap on the client-side. LexicalComposer will always set readonly to true (meaning contenteditable = false) after editor creation and that's the value that LexicalContentEditable will see. LexicalComposer will then have a useLayoutEffect that will swap this readonly value to false.

What if the user lazy loads Rich/PlainTextPlugin?

That's no problem! RichTextPlugin is now the responsible for both the event handling through commands and the rendering of the LexicalContentEditable, so even if readonly is false the user won't be able to interact without a <div contenteditable on the DOM.

For the same reason, we can always ensure that the Rich/PlainTextPlugin event handling through commands will always happen first before rendering.


Kudos to @trueadm and @acywatson for the proposal and @trueadm for helping me understand how this all connects together through React

@vercel
Copy link

vercel bot commented Feb 24, 2022

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/fbopensource/lexical/Ei8Tcoczik1aX7Rwj3yxqZ4SEDmf
✅ Preview: https://lexical-git-bootstrap3-fbopensource.vercel.app

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 24, 2022
@@ -98,6 +100,7 @@ export default function Editor(): React$Node {
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={placeholder}
initialEditorState={isCollab ? emptyFn : null}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why an empty function? Surely null should mean "do not initialize any editor state"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what if it's not initialEditorState? But instead, it can take a given editorState and update if it changes.

@zurfyx zurfyx changed the title rfc richtext initial + readonly wip rfc richtext initial + readonly Feb 24, 2022
break;
}
case 'function': {
editor.update(initialEditorState);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without history tag?

@@ -544,15 +539,16 @@ export function addRootElementEvents(

for (let i = 0; i < rootElementEvents.length; i++) {
const [eventName, onEvent] = rootElementEvents[i];
const isReadOnly = editor.getReadOnly();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be inside the event listener itself? Otherwise it will be stale.

switch (typeof initialEditorState) {
case 'string': {
const parsedEditorState = editor.parseEditorState(initialEditorState);
editor.setEditorState(parsedEditorState, {tag: 'history-merge'});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean you can have const options = {tag: 'history-merge'} at the top of the module, and pass it in. Saves bytes and is faster.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screen Shot 2022-02-24 at 1 42 37 pm

@@ -74,6 +74,7 @@ export default function Editor(): React$Node {
!isRichText ? 'plain-text' : ''
}`}>
<AutoFocusPlugin />
<LexicalClearEditorPlugin />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future consideration: I'm wondering if this really needs to be a separate plugin. If so, can it be returned from the RichText or PlainText plugin? We allow composing plugins like that, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@acywatson - Good point, this has always been a hot topic. It used to live in PlainText/Plugin but does it really make to have it there? At the end of the day this is a custom command that we happen to use in the playground and some WWW applications but you don't necessarily have to support clear. For example, you could just destroy and reinstantiate the editor.

At the same time, the reason why we merged BootstrapPlugin and RichTextPlugin together was mostly to solve initialization race conditions. Clearing the editor now sounds like an addon that doesn't conflict with the lifecycle.

Can we bring this up in our next sync? It might be worth a quick debate but personally I wouldn't like to clutter RichTextPlugin again.

@zurfyx zurfyx changed the title wip rfc richtext initial + readonly Plain/RichTextPlugin to handle initialEditorState + editor.isReadOnly Feb 24, 2022
@zurfyx zurfyx merged commit 6eb5439 into main Feb 24, 2022
@trueadm trueadm deleted the bootstrap3 branch April 6, 2022 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants