mirror of
https://github.com/facebook/lexical.git
synced 2025-05-17 23:26:16 +08:00
[lexical][lexical-website] Feature: Document and export common update tags (#7441)
Co-authored-by: Bob Ippolito <bob@ippoli.to>
This commit is contained in:
132
packages/lexical-website/docs/concepts/updates.md
Normal file
132
packages/lexical-website/docs/concepts/updates.md
Normal 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
|
||||
}
|
||||
});
|
||||
```
|
@ -51,6 +51,7 @@ const sidebars = {
|
||||
'concepts/serialization',
|
||||
'concepts/dom-events',
|
||||
'concepts/traversals',
|
||||
'concepts/updates',
|
||||
],
|
||||
label: 'Concepts',
|
||||
type: 'category',
|
||||
|
53
packages/lexical/src/LexicalUpdateTags.ts
Normal file
53
packages/lexical/src/LexicalUpdateTags.ts
Normal 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';
|
127
packages/lexical/src/__tests__/unit/LexicalUpdateTags.test.ts
Normal file
127
packages/lexical/src/__tests__/unit/LexicalUpdateTags.test.ts
Normal 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]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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';
|
||||
|
Reference in New Issue
Block a user