Skip to content

Commit

Permalink
Merge pull request #3512 from udecode/feat/setValue
Browse files Browse the repository at this point in the history
setValue
  • Loading branch information
zbeyens authored Sep 7, 2024
2 parents 2acb53b + 07e428c commit 3ab7bc4
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 73 deletions.
6 changes: 6 additions & 0 deletions .changeset/olive-eggs-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@udecode/plate-core': patch
---

- Add `editor.tf.setValue` to replace the editor value
- Fix: move `editor.api.reset` to `editor.tf.reset`
2 changes: 1 addition & 1 deletion BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ Migration example: https://github.com/udecode/plate/pull/3480
`editor: PlateEditor`:

- Move `redecorate` to `editor.api.redecorate`
- Move `reset` to `editor.api.reset`
- Move `reset` to `editor.tf.reset`
- Move `plate.set` to `editor.setPlateState`
- Move `blockFactory` to `editor.api.create.block`
- Move `childrenFactory` to `editor.api.create.value`
Expand Down
8 changes: 7 additions & 1 deletion apps/www/content/docs/api/core/plate-editor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ A custom editor interface that extends the base **`TEditor`** interface and incl
### ReactPlugin

<APIParameters>
<APIItem name="editor.api.reset" type="() => void">
<APIItem name="editor.tf.reset" type="() => void">
Reset the editor state while maintaining focus if the editor was focused.
</APIItem>
</APIParameters>
Expand All @@ -142,3 +142,9 @@ A custom editor interface that extends the base **`TEditor`** interface and incl
Redecorate the editor. This method should be overridden for proper functionality.
</APIItem>
</APIParameters>

<APIParameters>
<APIItem name="editor.tf.setValue" type="(value: Value) => void">
Replace the editor value. See [Controlled Value](/docs/controlled) for more information.
</APIItem>
</APIParameters>
71 changes: 71 additions & 0 deletions apps/www/content/docs/controlled.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Controlled Editor Value
description: How to control the editor value.
---

Implementing a fully controlled editor value in Plate (and Slate) is complex due to several factors:

1. The editor state includes more than just the content (`editor.children`). It also includes `editor.selection` and `editor.history`.

2. Directly replacing `editor.children` can break the selection and history, leading to unexpected behavior or crashes.

3. All changes to the editor's value should ideally happen through [Transforms](https://docs.slatejs.org/api/transforms) to maintain consistency with selection and history.

Given these challenges, it's generally recommended to use Plate as an uncontrolled input. However, if you need to make external changes to the editor's content, you can use `editor.tf.setValue(value)` function.

<Callout className="my-4">
Using `editor.tf.setValue` will re-render all nodes on each call, so it
should be used carefully and sparingly. It may impact performance if used
frequently or with large documents.
</Callout>

Alternatively, you can use `editor.tf.reset()` to reset the editor state, which will reset the selection and history.

```tsx
function App() {
const editor = usePlateEditor({
value: 'Initial Value',
// Disable the editor if initial value is not yet ready
// enabled: !!value,
});

return (
<div>
<Plate editor={editor}>
<PlateContent />
</Plate>

<button
onClick={() => {
// Replace with HTML string
editor.tf.setValue('Replaced Value');

// Replace with JSON value
editor.tf.setValue([
{
type: 'p',
children: [{ text: 'Replaced Value' }],
},
]);

// Replace with empty value
editor.tf.setValue();
}}
>
Replace Value
</button>

<button
onClick={() => {
editor.tf.reset();
}}
>
Reset Editor
</button>
</div>
);
}
```

<ComponentPreview name="controlled-demo" padding="md" />

9 changes: 9 additions & 0 deletions apps/www/src/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,15 @@ export const Index: Record<string, any> = {
subcategory: "undefined",
component: React.lazy(() => import('@/registry/default/example/basic-editor-default-demo')),
},
'controlled-demo': {
name: 'controlled-demo',
type: 'components:example',
registryDependencies: [],
files: ['registry/default/example/controlled-demo.tsx'],
category: "undefined",
subcategory: "undefined",
component: React.lazy(() => import('@/registry/default/example/controlled-demo')),
},
'basic-editor-styling-demo': {
name: 'basic-editor-styling-demo',
type: 'components:example',
Expand Down
5 changes: 5 additions & 0 deletions apps/www/src/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ export const docsConfig: DocsConfig = {
label: 'New',
title: 'Editor Methods',
},
{
href: '/docs/controlled',
label: 'New',
title: 'Controlled Value',
},
{
href: '/docs/html',
label: 'New',
Expand Down
58 changes: 58 additions & 0 deletions apps/www/src/registry/default/example/controlled-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';

import {
Plate,
focusEditor,
focusEditorEdge,
usePlateEditor,
} from '@udecode/plate-common/react';

import { Button } from '@/registry/default/plate-ui/button';
import { Editor } from '@/registry/default/plate-ui/editor';

export default function ControlledEditorDemo() {
const editor = usePlateEditor({
value: [
{
children: [{ text: 'Initial Value' }],
type: 'p',
},
],
});

return (
<div>
<Plate editor={editor}>
<Editor />
</Plate>

<div className="mt-4 flex flex-col gap-2">
<Button
onClick={() => {
// Replace with HTML string
editor.tf.setValue([
{
children: [{ text: 'Replaced Value' }],
type: 'p',
},
]);

focusEditorEdge(editor, { edge: 'end' });
}}
>
Replace Value
</Button>

<Button
onClick={() => {
editor.tf.reset();

focusEditor(editor);
}}
>
Reset Editor
</Button>
</div>
</div>
);
}
6 changes: 6 additions & 0 deletions apps/www/src/registry/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export const example: Registry = [
registryDependencies: [],
type: 'components:example',
},
{
files: ['example/controlled-demo.tsx'],
name: 'controlled-demo',
registryDependencies: [],
type: 'components:example',
},
{
files: ['example/basic-editor-styling-demo.tsx'],
name: 'basic-editor-styling-demo',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@
`editor: PlateEditor`:

- Move `redecorate` to `editor.api.redecorate`
- Move `reset` to `editor.api.reset`
- Move `reset` to `editor.tf.reset`
- Move `plate.set` to `editor.setPlateState`
- Move `blockFactory` to `editor.api.create.block`
- Move `childrenFactory` to `editor.api.create.value`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,51 @@ describe('delete marked text at block start', () => {
expect(editor.children).toEqual(output.children);
});
});

describe('editor.tf.setValue', () => {
it('should set the editor value correctly', () => {
const input = (
<editor>
<hp>existing content</hp>
</editor>
) as any;

const output = (
<editor>
<hp>new content</hp>
</editor>
) as any;

const editor = createPlateEditor({
editor: input,
});

editor.tf.setValue('<p>new content</p>');

expect(editor.children).toEqual(output.children);
});

it('should set empty value when no argument is provided', () => {
const input = (
<editor>
<hp>existing content</hp>
</editor>
) as any;

const output = (
<editor>
<hp>
<htext />
</hp>
</editor>
) as any;

const editor = createPlateEditor({
editor: input,
});

editor.tf.setValue();

expect(editor.children).toEqual(output.children);
});
});
23 changes: 19 additions & 4 deletions packages/core/src/lib/plugins/editor-protocol/SlateNextPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { TElement, TRange, Value } from '@udecode/slate';
import type { TDescendant, TElement, TRange, Value } from '@udecode/slate';
import type { Path } from 'slate';

import {
isSelectionAtBlockStart,
removeSelectionMark,
replaceNodeChildren,
toggleMark,
} from '@udecode/slate-utils';
import { type OmitFirst, bindFirst } from '@udecode/utils';
Expand Down Expand Up @@ -104,12 +105,26 @@ export const SlateNextPlugin = createTSlatePlugin<SlateNextConfig>({
value: (): Value => [api.create.block()],
},
}))
.extendEditorApi(({ editor }) => ({
.extendEditorTransforms(({ editor }) => ({
reset: () => {
resetEditor(editor);
},
}))
.extendEditorTransforms(({ editor }) => ({
setValue: <V extends Value>(value?: V | string) => {
let children: TDescendant[] = value as any;

if (typeof value === 'string') {
children = editor.api.html.deserialize({
element: value,
});
} else if (!value || value.length === 0) {
children = editor.api.create.value();
}

replaceNodeChildren(editor, {
at: [],
nodes: children,
});
},
toggle: {
block: bindFirst(toggleBlock, editor),
mark: bindFirst(toggleMark, editor),
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/react/plugins/react/ReactPlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('ReactPlugin', () => {
// Mock isEditorFocused to return true
(isEditorFocused as jest.Mock).mockReturnValue(true);

editor.api.reset();
editor.tf.reset();

expect(focusEditorEdge).toHaveBeenCalledWith(editor, { edge: 'start' });
});
Expand All @@ -34,7 +34,7 @@ describe('ReactPlugin', () => {
// Mock isEditorFocused to return false
(isEditorFocused as jest.Mock).mockReturnValue(false);

editor.api.reset();
editor.tf.reset();

expect(focusEditorEdge).not.toHaveBeenCalled();
});
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/react/plugins/react/ReactPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { withPlateReact } from './withPlateReact';
export const ReactPlugin = createSlatePlugin({
extendEditor: withPlateReact,
key: 'dom',
}).extendEditorApi(({ editor }) => {
const { reset } = editor.api;
}).extendEditorTransforms(({ editor }) => {
const { reset } = editor.tf;

return {
reset: () => {
Expand Down
Loading

0 comments on commit 3ab7bc4

Please sign in to comment.