Skip to content

Commit

Permalink
Remove EditorReadOptions and first commit pending updates
Browse files Browse the repository at this point in the history
  • Loading branch information
etrepum committed Jul 1, 2024
1 parent 004ad87 commit c5bd932
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 85 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Editor States are also fully serializable to JSON and can easily be serialized b
### Reading and Updating Editor State

When you want to read and/or update the Lexical node tree, you must do it via `editor.update(() => {...})`. You may also do
read-only operations with the editor state via `editor.getEditorState().read(() => {...})` or `editor.read(() => {...})`.
read-only operations with the editor state via `editor.read(() => {...})` or `editor.getEditorState().read(() => {...})`.
The closure passed to the update or read call is important, and must be synchronous. It's the only place where you have full
"lexical" context of the active editor state, and providing you with access to the Editor State's node tree. We promote using
the convention of using `$` prefixed functions (such as `$getRoot()`) to convey that these functions must be called in this
Expand All @@ -170,8 +170,10 @@ For those familiar with React Hooks, you can think of these $functions as having

Node Transforms and Command Listeners are called with an implicit `editor.update(() => {...})` context.

It is permitted to do nested reads and updates, but an update should not be nested in a read.
For example, `editor.update(() => editor.update(() => {...}))` is allowed.
It is permitted to do nested updates, or nested reads, but an update should not be nested in a read
or vice versa. For example, `editor.update(() => editor.update(() => {...}))` is allowed. It is permitted
to nest nest an `editor.read` at the end of an `editor.update`, but this will immediately flush the update
and any additional update in that callback will throw an error.

All Lexical Nodes are dependent on the associated Editor State. With few exceptions, you should only call methods
and access properties of a Lexical Node while in a read or update call (just like `$` functions). Methods
Expand All @@ -188,12 +190,11 @@ having Lexical Nodes always refer to the editor state allows for a simpler and l

:::tip

`editor.getEditorState().read()` and `editor.read()` will use the latest
reconciled `EditorState` (after any node transforms, DOM reconciliation,
etc. have already run). Any pending `editor.update` calls that were not
scheduled with `discrete: true` will not yet be visible unless you call
`editor.read(() => { /* callback */ }, { pending: true })`. When you are
in an `editor.update`, you will always see the pending state.
If you use `editor.read(() => { /* callback */ })` it will first flush any pending updates, so you will
always see a consistent state. When you are in an `editor.update`, you will always be working with the
pending state, where node transforms and DOM reconciliation may not have run yet.
`editor.getEditorState().read()` will use the latest reconciled `EditorState` (after any node transforms,
DOM reconciliation, etc. have already run), any pending `editor.update` mutations will not yet be visible.

:::

Expand Down
19 changes: 10 additions & 9 deletions packages/lexical-website/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Editor States are also fully serializable to JSON and can easily be serialized b
### Reading and Updating Editor State

When you want to read and/or update the Lexical node tree, you must do it via `editor.update(() => {...})`. You may also do
read-only operations with the editor state via `editor.getEditorState().read(() => {...})` or `editor.read(() => {...})`.
read-only operations with the editor state via `editor.read(() => {...})` or `editor.getEditorState().read(() => {...})`.
The closure passed to the update or read call is important, and must be synchronous. It's the only place where you have full
"lexical" context of the active editor state, and providing you with access to the Editor State's node tree. We promote using
the convention of using `$` prefixed functions (such as `$getRoot()`) to convey that these functions must be called in this
Expand All @@ -79,8 +79,10 @@ For those familiar with React Hooks, you can think of these $functions as having

Node Transforms and Command Listeners are called with an implicit `editor.update(() => {...})` context.

It is permitted to do nested reads and updates, but an update should not be nested in a read.
For example, `editor.update(() => editor.update(() => {...}))` is allowed.
It is permitted to do nested updates, or nested reads, but an update should not be nested in a read
or vice versa. For example, `editor.update(() => editor.update(() => {...}))` is allowed. It is permitted
to nest nest an `editor.read` at the end of an `editor.update`, but this will immediately flush the update
and any additional update in that callback will throw an error.

All Lexical Nodes are dependent on the associated Editor State. With few exceptions, you should only call methods
and access properties of a Lexical Node while in a read or update call (just like `$` functions). Methods
Expand All @@ -97,12 +99,11 @@ having Lexical Nodes always refer to the editor state allows for a simpler and l

:::tip

`editor.getEditorState().read()` and `editor.read()` will use the latest
reconciled `EditorState` (after any node transforms, DOM reconciliation,
etc. have already run). Any pending `editor.update` calls that were not
scheduled with `discrete: true` will not yet be visible unless you call
`editor.read(() => { /* callback */ }, { pending: true })`. When you are
in an `editor.update`, you will always see the pending state.
If you use `editor.read(() => { /* callback */ })` it will first flush any pending updates, so you will
always see a consistent state. When you are in an `editor.update`, you will always be working with the
pending state, where node transforms and DOM reconciliation may not have run yet.
`editor.getEditorState().read()` will use the latest reconciled `EditorState` (after any node transforms,
DOM reconciliation, etc. have already run), any pending `editor.update` mutations will not yet be visible.

:::

Expand Down
19 changes: 5 additions & 14 deletions packages/lexical/src/LexicalEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ export type EditorUpdateOptions = {
discrete?: true;
};

export interface EditorReadOptions {
pending?: boolean;
}

export type EditorSetOptions = {
tag?: string;
};
Expand Down Expand Up @@ -1121,18 +1117,13 @@ export class LexicalEditor {
* Executes a read of the editor's state, with the
* editor context available (useful for exporting and read-only DOM
* operations). Much like update, but prevents any mutation of the
* editor's state.
* editor's state. Any pending updates will be flushed immediately before
* the read.
* @param callbackFn - A function that has access to read-only editor state.
* @param options - A bag of options to control the behavior of the read.
* @param options.pending - Use the pending editorState. Use this only when
* it is necessary to read the state that has not yet been reconciled (this
* is the state that you would be working with from editor.update).
*/
read<T>(callbackFn: () => T, options?: EditorReadOptions): T {
const editorState =
(options && options.pending && this._pendingEditorState) ||
this.getEditorState();
return editorState.read(callbackFn, {editor: this});
read<T>(callbackFn: () => T): T {
$commitPendingUpdates(this);
return this.getEditorState().read(callbackFn, {editor: this});
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/lexical/src/LexicalUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function getActiveEditorState(): EditorState {
'Unable to find an active editor state. ' +
'State helpers or node methods can only be used ' +
'synchronously during the callback of ' +
'editor.update() or editorState.read().',
'editor.update(), editor.read(), or editorState.read().',
);
}

Expand All @@ -110,7 +110,7 @@ export function getActiveEditor(): LexicalEditor {
'Unable to find an active editor. ' +
'This method can only be used ' +
'synchronously during the callback of ' +
'editor.update().',
'editor.update() or editor.read().',
);
}

Expand Down
121 changes: 71 additions & 50 deletions packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ describe('LexicalEditor tests', () => {

let editor: LexicalEditor;

function init(onError?: () => void) {
function init(onError?: (error: Error) => void) {
const ref = createRef<HTMLDivElement>();

function TestBase() {
Expand All @@ -138,34 +138,31 @@ describe('LexicalEditor tests', () => {

describe('read()', () => {
it('Can read the editor state', async () => {
init();
expect(editor.read(() => $getRoot().getTextContent())).toEqual('');
expect(editor.read(() => $getEditor())).toBe(editor);
editor.update(() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
const text = $createTextNode('This works!');
root.append(paragraph);
paragraph.append(text);
init(function onError(err) {
throw err;
});
expect(editor.read(() => $getRoot().getTextContent())).toEqual('');
expect(editor.read(() => $getRoot().getTextContent(), {})).toEqual('');
expect(
editor.read(() => $getRoot().getTextContent(), {pending: false}),
).toEqual('');
expect(
editor.read(() => $getRoot().getTextContent(), {pending: true}),
).toEqual('This works!');
editor.read(() => {
const rootElement = editor.getRootElement();
expect(rootElement).toBeDefined();
const paragraphDom = rootElement!.querySelector('p');
// Not reconciled yet
expect(paragraphDom).toBeNull();
// The root never works for this call (is that a bug?)
expect($getNearestNodeFromDOMNode(rootElement!)).toBe(null);
});
expect(editor.read(() => $getEditor())).toBe(editor);
const onUpdate = jest.fn();
editor.update(
() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
const text = $createTextNode('This works!');
root.append(paragraph);
paragraph.append(text);
},
{onUpdate},
);
expect(onUpdate).toHaveBeenCalledTimes(0);
// This read will flush pending updates
expect(editor.read(() => $getRoot().getTextContent())).toEqual(
'This works!',
);
expect(onUpdate).toHaveBeenCalledTimes(1);
// Check to make sure there is not an unexpected reconciliation
await Promise.resolve().then();
expect(onUpdate).toHaveBeenCalledTimes(1);
editor.read(() => {
const rootElement = editor.getRootElement();
expect(rootElement).toBeDefined();
Expand All @@ -189,48 +186,72 @@ describe('LexicalEditor tests', () => {
$getNearestNodeFromDOMNode(textDom!.firstChild!)!.getTextContent(),
).toBe('This works!');
});
expect(onUpdate).toHaveBeenCalledTimes(1);
});
it('runs transforms the editor state', async () => {
init(function onError(err) {
throw err;
});
expect(editor.read(() => $getRoot().getTextContent())).toEqual('');
expect(editor.read(() => $getEditor())).toBe(editor);
editor.registerNodeTransform(TextNode, (node) => {
if (node.getTextContent() === 'This works!') {
node.replace($createTextNode('Transforms work!'));
}
});
const onUpdate = jest.fn();
editor.update(
() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
const text = $createTextNode('This works!');
root.append(paragraph);
paragraph.append(text);
},
{onUpdate},
);
expect(onUpdate).toHaveBeenCalledTimes(0);
// This read will flush pending updates
expect(editor.read(() => $getRoot().getTextContent())).toEqual(
'This works!',
'Transforms work!',
);
expect(editor.getRootElement().textContent).toEqual('Transforms work!');

Check failure on line 218 in packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx

View workflow job for this annotation

GitHub Actions / core-tests / integrity (20.11.0)

Object is possibly 'null'.
expect(onUpdate).toHaveBeenCalledTimes(1);
// Check to make sure there is not an unexpected reconciliation
await Promise.resolve().then();
expect(onUpdate).toHaveBeenCalledTimes(1);
expect(editor.read(() => $getRoot().getTextContent())).toEqual(
'Transforms work!',
);
expect(
editor.read(() => $getRoot().getTextContent(), {pending: true}),
).toEqual('This works!');
});

it('Can be nested in an update or read', async () => {
init();
it('can be nested in an update or read', async () => {
init(function onError(err) {
throw err;
});
editor.update(() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
const text = $createTextNode('This works!');
root.append(paragraph);
paragraph.append(text);
editor.read(() => {
expect($getRoot().getTextContent()).toBe('');
expect($getRoot().getTextContent()).toBe('This works!');
});
editor.read(
() => {
expect($getRoot().getTextContent()).toBe('This works!');
},
{pending: true},
);
editor.read(() => {
// Nesting update in read works, although it is discouraged in the documentation.
editor.update(() => {
expect($getRoot().getTextContent()).toBe('This works!');
});
// The state still has not yet been reconciled
expect($getRoot().getTextContent()).toBe('');
// The pending state can be read
editor.read(
() => {
expect($getRoot().getTextContent()).toBe('This works!');
},
{pending: true},
);
});
// Updating after a nested read will fail as it has already been committed
expect(() => {
root.append(
$createParagraphNode().append(
$createTextNode('update-read-update'),
),
);
}).not.toThrow();
});
await Promise.resolve().then();
editor.read(() => {
editor.read(() => {
expect($getRoot().getTextContent()).toBe('This works!');
Expand Down
1 change: 0 additions & 1 deletion packages/lexical/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export type {
CreateEditorArgs,
EditableListener,
EditorConfig,
EditorReadOptions,
EditorSetOptions,
EditorThemeClasses,
EditorThemeClassName,
Expand Down

0 comments on commit c5bd932

Please sign in to comment.