虽然 Plate 通常用作非受控输入,但在某些场景下,您可能希望将编辑器集成到表单库中,比如 react-hook-form 或 shadcn/ui 的 Form 组件。本指南将介绍最佳实践和常见陷阱。
何时将 Plate 与表单集成
- 表单提交:您希望在用户提交表单时,将编辑器内容与其他字段(如
<input>
、<select>
)一起包含。 - 验证:您希望同时验证编辑器内容(例如,检查是否为空)和其他表单字段。
- 表单数据管理:您希望将编辑器内容存储在与表单字段相同的存储中(如
react-hook-form
的状态)。
但是,请注意关于完全控制编辑器值的警告。Plate 强烈推荐使用非受控模型。如果您过于频繁地替换编辑器的内部状态,可能会破坏选择、历史记录或导致性能问题。推荐的模式是将编辑器视为非受控的,但在特定事件上仍然同步表单数据。
方法 1:在 onChange
时同步
这是最直接的方法:每次编辑器更改时,更新表单字段的值。对于小型文档或不频繁的更改,这通常是可以接受的。
React Hook Form 示例
import { useForm } from 'react-hook-form';
import type { Value } from 'platejs';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
type FormData = {
content: Value;
};
export function RHFEditorForm() {
const initialValue = [
{ type: 'p', children: [{ text: '来自 react-hook-form 的问候!' }] },
]
// 设置 react-hook-form
const { register, handleSubmit, setValue } = useForm<FormData>({
defaultValues: {
content: initialValue,
},
});
// 创建/配置 Plate 编辑器
const editor = usePlateEditor({ value: initialValue });
// 为 react-hook-form 注册字段
register('content', { /* 验证规则... */ });
const onSubmit = (data: FormData) => {
// data.content 将包含最终的编辑器内容
console.info('已提交:', data.content);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Plate
editor={editor}
onChange={({ value }) => {
// 将编辑器更改同步到表单
setValue('content', value);
}}
>
<PlateContent placeholder="在此输入..." />
</Plate>
<button type="submit">提交</button>
</form>
);
}
注意事项:
defaultValues.content
:您的初始编辑器内容。register('content')
:向 RHF 表明该字段被跟踪。onChange({ value })
:每次调用setValue('content', value)
。
如果您处理大型文档或快速输入,请考虑使用防抖或切换到 onBlur
方法来减少表单更新。
shadcn/ui 表单示例
shadcn/ui 提供了一个与 react-hook-form 集成的 <Form>
。我们将使用 <FormField>
来处理字段逻辑:
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useForm } from 'react-hook-form';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
type FormValues = {
content: any;
};
export function EditorForm() {
// 1. 创建表单
const form = useForm<FormValues>({
defaultValues: {
content: [
{ type: 'p', children: [{ text: '来自 shadcn/ui Form 的问候!' }] },
],
},
});
// 2. 创建 Plate 编辑器
const editor = usePlateEditor();
const onSubmit = (data: FormValues) => {
console.info('已提交数据:', data.content);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>内容</FormLabel>
<FormControl>
<Plate
editor={editor}
onChange={({ value }) => {
// 同步到表单
field.onChange(value);
}}
>
<PlateContent placeholder="输入..." />
</Plate>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<button type="submit">提交</button>
</form>
</Form>
);
}
这种方法使您的编辑器内容的行为与 shadcn/ui 表单中的其他字段一样。
方法 2:在失焦时同步(或其他触发方式)
您可能只需要在用户以下情况时获取最终值:
- 离开编辑器(
onBlur
) - 点击"保存"按钮
- 达到某些表单提交逻辑
<Plate editor={editor}>
<PlateContent
onBlur={() => {
// 仅在失焦时同步
setValue('content', editor.children);
}}
/>
</Plate>
这减少了开销,但您的表单状态在用户输入时不会反映部分更新。
方法 3:受控替换(高级)
如果您希望表单成为单一数据源(完全受控):
editor.tf.setValue(formStateValue);
但这有已知的缺点:
- 可能会破坏光标位置和撤销/重做功能
- 对于大型文档可能导致频繁的完全重新渲染
建议:如果可能,坚持使用部分非受控模型。
示例:保存和重置
这是一个更完整的表单示例,展示了如何保存和重置编辑器/表单:
import { useForm } from 'react-hook-form';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
function MyForm() {
const form = useForm({
defaultValues: {
content: [
{ type: 'p', children: [{ text: '初始内容...' }] },
],
},
});
const editor = usePlateEditor();
const onSubmit = (data) => {
alert(JSON.stringify(data, null, 2));
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Plate
editor={editor}
onChange={({ value }) => form.setValue('content', value)}
>
<PlateContent />
</Plate>
<button type="submit">保存</button>
<button
type="button"
onClick={() => {
// 重置编辑器
editor.tf.reset();
// 重置表单
form.reset();
}}
>
重置
</button>
</form>
);
}
onChange
-> 更新表单状态- 重置 -> 同时调用
editor.tf.reset()
和form.reset()
以保持一致性
从 shadcn Textarea 迁移到 Plate
如果您有一个标准的 shadcn/ui 文档中的 TextareaForm,并想用 Plate 编辑器替换 <Textarea>
,可以按照以下步骤操作:
// 1. 原始代码 (TextareaForm)
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>个人简介</FormLabel>
<FormControl>
<Textarea
placeholder="告诉我们一些关于你自己的信息"
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
您可以 <span>@提及</span> 其他用户和组织。
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
创建一个新的 EditorField
组件:
// EditorField.tsx
"use client";
import * as React from "react";
import type { Value } from "platejs";
import { Plate, PlateContent, usePlateEditor } from "platejs/react";
/**
* 一个可重用的编辑器组件,工作方式类似于标准的 <Textarea>,
* 接受 `value`、`onChange` 和可选的 placeholder。
*
* 用法:
*
* <FormField
* control={form.control}
* name="bio"
* render={({ field }) => (
* <FormItem>
* <FormLabel>个人简介</FormLabel>
* <FormControl>
* <EditorField
* {...field}
* placeholder="告诉我们一些关于你自己的信息"
* />
* </FormControl>
* <FormDescription>一些有帮助的描述...</FormDescription>
* <FormMessage />
* </FormItem>
* )}
* />
*/
export interface EditorFieldProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* 当前的 Plate 值。应该是一个 Plate 节点数组。
*/
value?: Value;
/**
* 当编辑器值改变时调用。
*/
onChange?: (value: Value) => void;
/**
* 编辑器为空时显示的占位符文本。
*/
placeholder?: string;
}
export function EditorField({
value,
onChange,
placeholder = "在此输入...",
...props
}: EditorFieldProps) {
// 使用提供的初始 `value` 创建编辑器实例。
const editor = usePlateEditor({
value: value ?? [
{ type: "p", children: [{ text: "" }] }, // 默认空段落
],
});
return (
<Plate
editor={editor}
onChange={({ value }) => {
// 通过 onChange prop 将更改同步回调用者
onChange?.(value);
}}
{...props}
>
<PlateContent placeholder={placeholder} />
</Plate>
);
}
- 用
<EditorField>
替换<Textarea>
块:
"use client";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { EditorField } from "./EditorField"; // 导入上面的组件
// 1. 使用 zod 定义验证模式
const FormSchema = z.object({
bio: z
.string()
.min(10, { message: "个人简介至少需要 10 个字符。" })
.max(160, { message: "个人简介不能超过 160 个字符。" }),
});
// 2. 构建主表单组件
export function EditorForm() {
// 3. 设置表单
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
// 这里 "bio" 只是一个字符串,但如果您将其解析为 JSON,
// 我们的编辑器会将其解释为初始内容
bio: "",
},
});
// 4. 提交处理函数
function onSubmit(data: z.infer<typeof FormSchema>) {
alert("已提交: " + JSON.stringify(data, null, 2));
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>个人简介</FormLabel>
<FormControl>
<EditorField
{...field}
placeholder="告诉我们一些关于你自己的信息..."
/>
</FormControl>
<FormDescription>
您可以 <span>@提及</span> 其他用户和组织。
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<button type="submit" className="py-2 px-4 bg-primary text-white">
提交
</button>
</form>
</Form>
);
}
- 任何现有的表单验证或错误消息保持不变。
- 对于默认值,确保
form.defaultValues.bio
是一个有效的 Plate 值(节点数组)而不是字符串。 - 对于受控值,适度使用
editor.tf.setValue(formStateValue)
。
最佳实践
- 使用非受控编辑器:让 Plate 管理自己的状态,仅在必要时更新表单。
- 最小化替换:避免过于频繁地调用
editor.tf.setValue
,这可能会破坏选择、历史记录或降低性能。 - 在适当的时机验证:决定是需要即时验证(输入时)还是在失焦/提交时验证。
- 同时重置:如果重置表单,调用
editor.tf.reset()
以保持同步。