[lexical][lexical-website] Feature: Document and export common update tags (#7441)

Co-authored-by: Bob Ippolito <bob@ippoli.to>
This commit is contained in:
Kiran Dash
2025-04-09 12:23:06 +08:00
committed by GitHub
parent 039afde04f
commit cb74b8c281
5 changed files with 325 additions and 0 deletions

View File

@ -0,0 +1,132 @@
# Updates
Updates in Lexical are synchronous operations that mutate the editor state (except in nested update scenarios which should be deprecated). The reconciliation process (DOM updates) is batched for performance reasons. This batching of DOM updates means we can avoid unnecessary re-renders and optimize the rendering process.
## Update Tags
Update tags are string identifiers that can be attached to an update to indicate its type or purpose. They can be used to control how updates are processed, merged, or handled by listeners. Multiple tags can be used in a single update.
You can add tags in two ways:
1. Using the `tag` option in `editor.update()`:
```js
import {HISTORY_PUSH_TAG, PASTE_TAG} from 'lexical';
editor.update(() => {
// Your update code
}, {
tag: HISTORY_PUSH_TAG // Single tag
});
editor.update(() => {
// Your update code
}, {
tag: [HISTORY_PUSH_TAG, PASTE_TAG] // Multiple tags
});
```
2. Using the `$addUpdateTag()` function within an update:
```js
import {HISTORY_PUSH_TAG} from 'lexical';
editor.update(() => {
$addUpdateTag(HISTORY_PUSH_TAG);
// Your update code
});
```
You can check if a tag is present using `$hasUpdateTag()`:
```js
import {HISTORIC_TAG} from 'lexical';
editor.update(() => {
$addUpdateTag(HISTORIC_TAG);
console.log($hasUpdateTag(HISTORIC_TAG)); // true
});
```
Note: While update tags can be checked within the same update using `$hasUpdateTag()`, they are typically accessed in update and mutation listeners through the `tags` and `updateTags` properties in their respective payloads. Here's the more common usage pattern:
```js
import {HISTORIC_TAG} from 'lexical';
editor.registerUpdateListener(({tags}) => {
if (tags.has(HISTORIC_TAG)) {
// Handle updates with historic tag
}
});
editor.registerMutationListener(MyNode, (mutations) => {
// updateTags contains tags from the current update
if (mutations.updateTags.has(HISTORIC_TAG)) {
// Handle mutations with historic tag
}
});
```
### Common Update Tags
Lexical provides several built-in update tags that are exported as constants:
- `HISTORIC_TAG`: Indicates that the update is related to history operations (undo/redo)
- `HISTORY_PUSH_TAG`: Forces a new history entry to be created
- `HISTORY_MERGE_TAG`: Merges the current update with the previous history entry
- `PASTE_TAG`: Indicates that the update is related to a paste operation
- `COLLABORATION_TAG`: Indicates that the update is related to collaborative editing
- `SKIP_COLLAB_TAG`: Indicates that the update should skip collaborative sync
- `SKIP_SCROLL_INTO_VIEW_TAG`: Prevents scrolling the selection into view
- `SKIP_DOM_SELECTION_TAG`: Prevents updating the DOM selection (useful for updates that shouldn't affect focus)
### Tag Validation
To prevent typos and ensure type safety when using update tags, Lexical exports constants for all built-in tags. It's recommended to always use these constants instead of string literals:
```js
import {
HISTORIC_TAG,
HISTORY_PUSH_TAG,
COLLABORATION_TAG,
} from 'lexical';
editor.update(() => {
// Using constants ensures type safety and prevents typos
$addUpdateTag(HISTORIC_TAG);
// These constants can be used in update options
editor.update(() => {
// Your update code
}, {
tag: HISTORY_PUSH_TAG
});
// And in listener checks
editor.registerUpdateListener(({tags}) => {
if (tags.has(COLLABORATION_TAG)) {
// Handle collaborative updates
}
});
});
```
### Custom Tags
While Lexical provides common tags as constants, you can also define your own constants for custom tags to maintain consistency and type safety:
```js
// Define your custom tags as constants
const MY_FEATURE_TAG = 'my-custom-feature';
const MY_UPDATE_TAG = 'my-custom-update';
editor.update(() => {
$addUpdateTag(MY_FEATURE_TAG);
}, {
tag: MY_UPDATE_TAG
});
// Listen for updates with specific tags
editor.registerUpdateListener(({tags}) => {
if (tags.has(MY_FEATURE_TAG)) {
// Handle updates from your custom feature
}
});
```

View File

@ -51,6 +51,7 @@ const sidebars = {
'concepts/serialization',
'concepts/dom-events',
'concepts/traversals',
'concepts/updates',
],
label: 'Concepts',
type: 'category',

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/**
* Common update tags used in Lexical. These tags can be used with editor.update() or $addUpdateTag()
* to indicate the type/purpose of an update. Multiple tags can be used in a single update.
*/
/**
* Indicates that the update is related to history operations (undo/redo)
*/
export const HISTORIC_TAG = 'historic';
/**
* Indicates that a new history entry should be pushed to the history stack
*/
export const HISTORY_PUSH_TAG = 'history-push';
/**
* Indicates that the current update should be merged with the previous history entry
*/
export const HISTORY_MERGE_TAG = 'history-merge';
/**
* Indicates that the update is related to a paste operation
*/
export const PASTE_TAG = 'paste';
/**
* Indicates that the update is related to collaborative editing
*/
export const COLLABORATION_TAG = 'collaboration';
/**
* Indicates that the update should skip collaborative sync
*/
export const SKIP_COLLAB_TAG = 'skip-collab';
/**
* Indicates that the update should skip scrolling the selection into view
*/
export const SKIP_SCROLL_INTO_VIEW_TAG = 'skip-scroll-into-view';
/**
* Indicates that the update should skip updating the DOM selection
* This is useful when you want to make updates without changing the selection or focus
*/
export const SKIP_DOM_SELECTION_TAG = 'skip-dom-selection';

View File

@ -0,0 +1,127 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {LexicalEditor} from 'lexical';
import {
$addUpdateTag,
$createParagraphNode,
$getRoot,
$hasUpdateTag,
COLLABORATION_TAG,
HISTORIC_TAG,
HISTORY_MERGE_TAG,
HISTORY_PUSH_TAG,
SKIP_DOM_SELECTION_TAG,
SKIP_SCROLL_INTO_VIEW_TAG,
} from 'lexical';
import {initializeUnitTest} from '../utils';
type TestEnv = {
editor: LexicalEditor;
};
describe('LexicalUpdateTags tests', () => {
initializeUnitTest((testEnv: TestEnv) => {
test('Built-in update tags work correctly', async () => {
const {editor} = testEnv;
await editor.update(() => {
const builtInTags = [
HISTORIC_TAG,
HISTORY_PUSH_TAG,
HISTORY_MERGE_TAG,
COLLABORATION_TAG,
SKIP_DOM_SELECTION_TAG,
SKIP_SCROLL_INTO_VIEW_TAG,
];
for (const tag of builtInTags) {
$addUpdateTag(tag);
expect($hasUpdateTag(tag)).toBe(true);
}
});
});
test('$addUpdateTag and $hasUpdateTag work correctly', async () => {
const {editor} = testEnv;
await editor.update(() => {
const tag = 'test-tag';
expect($hasUpdateTag(tag)).toBe(false);
$addUpdateTag(tag);
expect($hasUpdateTag(tag)).toBe(true);
});
});
test('Multiple update tags can be added', async () => {
const {editor} = testEnv;
await editor.update(() => {
const tags = ['tag1', 'tag2', 'tag3'];
for (const tag of tags) {
$addUpdateTag(tag);
}
for (const tag of tags) {
expect($hasUpdateTag(tag)).toBe(true);
}
});
});
test('Update tags via editor.update() options work', async () => {
const {editor} = testEnv;
const tag = 'test-tag';
let hasTag = false;
await editor.update(
() => {
hasTag = $hasUpdateTag(tag);
},
{tag},
);
expect(hasTag).toBe(true);
});
test('Update tags are cleared after update', async () => {
const {editor} = testEnv;
const tag = HISTORIC_TAG;
await editor.update(() => {
$addUpdateTag(tag);
expect($hasUpdateTag(tag)).toBe(true);
});
let hasTag = false;
await editor.update(() => {
hasTag = $hasUpdateTag(tag);
});
expect(hasTag).toBe(false);
});
test('Update tags affect editor behavior', async () => {
const {editor} = testEnv;
// Test that skip-dom-selection prevents selection updates
const updateListener = jest.fn();
editor.registerUpdateListener(({tags}: {tags: Set<string>}) => {
updateListener(Array.from(tags));
});
await editor.update(
() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
root.append(paragraph);
},
{
tag: SKIP_DOM_SELECTION_TAG,
},
);
expect(updateListener).toHaveBeenCalledWith(
expect.arrayContaining([SKIP_DOM_SELECTION_TAG]),
);
});
});
});

View File

@ -296,3 +296,15 @@ export type {
TextModeType,
} from './nodes/LexicalTextNode';
export {$createTextNode, $isTextNode, TextNode} from './nodes/LexicalTextNode';
// Update Tags
export {
COLLABORATION_TAG,
HISTORIC_TAG,
HISTORY_MERGE_TAG,
HISTORY_PUSH_TAG,
PASTE_TAG,
SKIP_COLLAB_TAG,
SKIP_DOM_SELECTION_TAG,
SKIP_SCROLL_INTO_VIEW_TAG,
} from './LexicalUpdateTags';