快速开始
一个快速教程,帮助你快速上手 Plate。
创建项目
你可以选择以下模板之一来开始:
选项 | Plate | Plugins | AI | 后端 |
---|---|---|---|---|
Notion-like template | ✅ | ✅ | ✅ | ✅ |
Plate playground template | ✅ | ✅ | ✅ | |
Plate minimal template | ✅ |
对于现有的 React 项目,请跳到下一步。
添加依赖
首先,安装核心依赖:
npm install @udecode/plate-common slate slate-dom slate-react slate-history
对于本指南中的示例,我们还将使用这些插件:
npm install @udecode/plate-basic-marks @udecode/plate-heading @udecode/plate-block-quote @udecode/cn
@udecode/plate-basic-marks
provides bold, italic, underline, and code formatting.@udecode/plate-heading
adds h1-h6 support.@udecode/plate-block-quote
adds blockquote support.@udecode/cn
helps with component styling (optional).
基本编辑器
让我们从一个最小的编辑器设置开始。
import {
usePlateEditor,
Plate,
PlateContent,
} from '@udecode/plate-common/react';
export default function BasicEditor() {
const editor = usePlateEditor();
return (
<Plate editor={editor}>
<PlateContent placeholder="Type..." />
</Plate>
);
}
Plate
管理编辑器状态,PlateContent
渲染编辑器内容。
'use client';
import React from 'react';
import {
Plate,
PlateContent,
usePlateEditor,
} from '@udecode/plate-common/react';
export default function BasicEditorDefaultDemo() {
const editor = usePlateEditor();
return (
<Plate editor={editor}>
<PlateContent placeholder="Type..." />
</Plate>
);
}
样式
让我们给编辑器添加一些样式:Editor 是 PlateContent
的样式版本。
'use client';
import React from 'react';
import { Plate, usePlateEditor } from '@udecode/plate-common/react';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
export default function BasicEditorStylingDemo() {
const editor = usePlateEditor();
return (
<Plate editor={editor}>
<EditorContainer>
<Editor placeholder="Type..." />
</EditorContainer>
</Plate>
);
}
为了保持简单,我们将在以下代码片段中继续使用 PlateContent
。
注意: Editor
只是一个使用 Tailwind 的样式编辑器的示例,如果你使用它,请确保按照 手动安装
指南中的安装步骤进行操作。你可以创建自己的 PlateContent
样式版本。
'use client';
import React from 'react';
import type { PlateContentProps } from '@udecode/plate-common/react';
import type { VariantProps } from 'class-variance-authority';
import { cn } from '@udecode/cn';
import {
PlateContent,
useEditorContainerRef,
useEditorRef,
} from '@udecode/plate-common/react';
import { cva } from 'class-variance-authority';
const editorContainerVariants = cva(
'relative w-full cursor-text select-text overflow-y-auto caret-primary selection:bg-brand/25 focus-visible:outline-none [&_.slate-selection-area]:border [&_.slate-selection-area]:border-brand/25 [&_.slate-selection-area]:bg-brand/15',
{
defaultVariants: {
variant: 'default',
},
variants: {
variant: {
default: 'h-full',
demo: 'h-[650px]',
select: cn(
'group rounded-md border border-input ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
'has-[[data-readonly]]:w-fit has-[[data-readonly]]:cursor-default has-[[data-readonly]]:border-transparent has-[[data-readonly]]:focus-within:[box-shadow:none]'
),
},
},
}
);
export const EditorContainer = ({
className,
variant,
...props
}: React.HTMLAttributes<HTMLDivElement> &
VariantProps<typeof editorContainerVariants>) => {
const editor = useEditorRef();
const containerRef = useEditorContainerRef();
return (
<div
id={editor.uid}
ref={containerRef}
className={cn(
'ignore-click-outside/toolbar',
editorContainerVariants({ variant }),
className
)}
{...props}
/>
);
};
EditorContainer.displayName = 'EditorContainer';
const editorVariants = cva(
cn(
'group/editor',
'relative w-full cursor-text select-text overflow-x-hidden whitespace-pre-wrap break-words',
'rounded-md ring-offset-background focus-visible:outline-none',
'placeholder:text-muted-foreground/80 [&_[data-slate-placeholder]]:top-[auto_!important] [&_[data-slate-placeholder]]:text-muted-foreground/80 [&_[data-slate-placeholder]]:!opacity-100',
'[&_strong]:font-bold'
),
{
defaultVariants: {
variant: 'default',
},
variants: {
disabled: {
true: 'cursor-not-allowed opacity-50',
},
focused: {
true: 'ring-2 ring-ring ring-offset-2',
},
variant: {
ai: 'w-full px-0 text-base md:text-sm',
aiChat:
'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm',
default:
'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',
demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]',
fullWidth: 'size-full px-16 pb-72 pt-4 text-base sm:px-24',
none: '',
select: 'px-3 py-2 text-base data-[readonly]:w-fit',
},
},
}
);
export type EditorProps = PlateContentProps &
VariantProps<typeof editorVariants>;
export const Editor = React.forwardRef<HTMLDivElement, EditorProps>(
({ className, disabled, focused, variant, ...props }, ref) => {
return (
<PlateContent
ref={ref}
className={cn(
editorVariants({
disabled,
focused,
variant,
}),
className
)}
disabled={disabled}
disableDefaultStyles
{...props}
/>
);
}
);
Editor.displayName = 'Editor';
初始化编辑器内容
让我们指定编辑器的初始内容:一个段落。
// ...
const value = [
{
type: 'p',
children: [
{
text: 'This is editable plain text with react and history plugins, just like a <textarea>!',
},
],
},
];
export default function BasicEditor() {
const editor = usePlateEditor({
value,
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
注意: Plate 使用 type
属性来启用插件按类型渲染节点。
'use client';
import React from 'react';
import { Plate, usePlateEditor } from '@udecode/plate-common/react';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
const value = [
{
children: [
{
text: 'This is editable plain text with react and history plugins, just like a <textarea>!',
},
],
type: 'p',
},
];
export default function BasicEditorValueDemo() {
const editor = usePlateEditor({ value });
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
}
实现 Change Handler
在这一阶段,监控编辑器修改以适当存储值至关重要。onChange
属性将用于此目的。你还可以通过将值保存到本地存储或数据库并根据需要加载它来持久化编辑器状态。
// ...
export default function BasicEditor() {
const localValue =
typeof window !== 'undefined' && localStorage.getItem('editorContent');
const editor = usePlateEditor({
value: localValue ? JSON.parse(localValue) : value,
});
return (
<Plate
editor={editor}
onChange={({ value }) => {
// 为了性能,请对你的保存逻辑进行防抖处理
localStorage.setItem('editorContent', JSON.stringify(value));
}}
>
<PlateContent />
</Plate>
);
}
'use client';
import React, { useState } from 'react';
import type { Value } from '@udecode/plate-common';
import { Plate, usePlateEditor } from '@udecode/plate-common/react';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
const value = [
{
children: [
{
text: 'This is editable plain text with react and history plugins, just like a textarea!',
},
],
type: 'p',
},
];
export default function BasicEditorHandlerDemo() {
const [debugValue, setDebugValue] = useState<Value>(value);
const localValue =
typeof window !== 'undefined' && localStorage.getItem('editorContent');
const editor = usePlateEditor({
value: localValue ? JSON.parse(localValue) : value,
});
return (
<Plate
onChange={({ value }) => {
localStorage.setItem('editorContent', JSON.stringify(value));
setDebugValue(value);
}}
editor={editor}
>
<EditorContainer>
<Editor />
</EditorContainer>
<Accordion type="single" collapsible>
<AccordionItem value="manual-installation">
<AccordionTrigger>Debug Value</AccordionTrigger>
<AccordionContent>{JSON.stringify(debugValue)}</AccordionContent>
</AccordionItem>
</Accordion>
</Plate>
);
}
Plugins
查看 Plugins 部分中的完整插件列表。
让我们使用一些基本的插件。
// ...
import {
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { HeadingPlugin } from '@udecode/plate-heading/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
const value = [
// ...
];
export default function BasicEditor() {
const editor = usePlateEditor({
plugins: [
HeadingPlugin,
BlockquotePlugin,
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
],
value,
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
'use client';
import React, { useState } from 'react';
import type { Value } from '@udecode/plate-common';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import { Plate, usePlateEditor } from '@udecode/plate-common/react';
import { HeadingPlugin } from '@udecode/plate-heading/react';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
import { basicEditorValue } from './basic-plugins-components-demo';
export default function BasicPluginsDefaultDemo() {
const [debugValue, setDebugValue] = useState<Value>(basicEditorValue);
const editor = usePlateEditor({
plugins: [
BlockquotePlugin,
HeadingPlugin,
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin,
],
value: basicEditorValue,
});
return (
<Plate
onChange={({ value }) => {
setDebugValue(value);
// save newValue...
}}
editor={editor}
>
<EditorContainer>
<Editor />
</EditorContainer>
<Accordion type="single" collapsible>
<AccordionItem value="manual-installation">
<AccordionTrigger>Debug Value</AccordionTrigger>
<AccordionContent>{JSON.stringify(debugValue)}</AccordionContent>
</AccordionItem>
</Accordion>
</Plate>
);
}
插件功能正常。然而,由于我们没有为渲染指定任何自定义组件,编辑器使用默认(未样式化)组件。具体来说,默认元素组件是一个 div
,默认叶组件是一个 span
。
注意: 你不需要添加核心插件,如 ReactPlugin
、HistoryPlugin
和 ParagraphPlugin
,因为 usePlateEditor
已经为你做了。
Components
注意: Plate 插件是未样式化的,这意味着你可以完全控制标记和样式,因此你可以集成你自己的设计系统或 Plate UI。
为了在一个地方插件所有组件,使用 override.components
选项在 usePlateEditor
中。我们将使用 withProps
助手传递额外的 Tailwind CSS 类。
// ...
import { withProps } from '@udecode/cn';
import {
Plate,
PlateElement,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate-common/react';
export default function BasicEditor() {
const editor = usePlateEditor({
plugins: [
HeadingPlugin,
BlockquotePlugin,
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
],
override: {
components: {
blockquote: withProps(PlateElement, {
as: 'blockquote',
className: 'mb-4 border-l-4 border-[#d0d7de] pl-4 text-[#636c76]',
}),
bold: withProps(PlateLeaf, { as: 'strong' }),
h1: withProps(PlateElement, {
as: 'h1',
className:
'mb-4 mt-6 text-3xl font-semibold tracking-tight lg:text-4xl',
}),
h2: withProps(PlateElement, {
as: 'h2',
className: 'mb-4 mt-6 text-2xl font-semibold tracking-tight',
}),
h3: withProps(PlateElement, {
as: 'h3',
className: 'mb-4 mt-6 text-xl font-semibold tracking-tight',
}),
italic: withProps(PlateLeaf, { as: 'em' }),
p: withProps(PlateElement, {
as: 'p',
className: 'mb-4',
}),
underline: withProps(PlateLeaf, { as: 'u' }),
},
},
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
'use client';
import { withProps } from '@udecode/cn';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import {
Plate,
PlateElement,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate-common/react';
import { HeadingPlugin } from '@udecode/plate-heading/react';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
export default function BasicPluginsComponentsDemo() {
const editor = usePlateEditor({
override: {
components: {
blockquote: withProps(PlateElement, {
as: 'blockquote',
className: 'mb-4 border-l-4 border-[#d0d7de] pl-4 text-[#636c76]',
}),
bold: withProps(PlateLeaf, { as: 'strong' }),
h1: withProps(PlateElement, {
as: 'h1',
className:
'mb-4 mt-6 text-3xl font-semibold tracking-tight lg:text-4xl',
}),
h2: withProps(PlateElement, {
as: 'h2',
className: 'mb-4 mt-6 text-2xl font-semibold tracking-tight',
}),
h3: withProps(PlateElement, {
as: 'h3',
className: 'mb-4 mt-6 text-xl font-semibold tracking-tight',
}),
italic: withProps(PlateLeaf, { as: 'em' }),
p: withProps(PlateElement, {
as: 'p',
className: 'mb-4',
}),
underline: withProps(PlateLeaf, { as: 'u' }),
},
},
plugins: [
BlockquotePlugin,
HeadingPlugin,
BoldPlugin,
ItalicPlugin,
UnderlinePlugin,
CodePlugin,
],
value: basicEditorValue,
});
return (
<Plate editor={editor}>
<EditorContainer>
<Editor placeholder="Type..." autoFocus={false} spellCheck={false} />
</EditorContainer>
</Plate>
);
}
export const basicEditorValue = [
{
id: '1',
children: [
{
text: '🌳 Blocks',
},
],
type: 'h1',
},
{
id: '2',
children: [
{
text: 'Easily create headings of various levels, from H1 to H6, to structure your content and make it more organized.',
},
],
type: 'p',
},
{
id: '3',
children: [
{
text: 'Create blockquotes to emphasize important information or highlight quotes from external sources.',
},
],
type: 'blockquote',
},
{
id: '1',
children: [
{
text: '🌱 Marks',
},
],
type: 'h1',
},
{
id: '2',
children: [
{
text: 'Add style and emphasis to your text using the mark plugins, which offers a variety of formatting options.',
},
],
type: 'p',
},
{
id: '3',
children: [
{
text: 'Make text ',
},
{
bold: true,
text: 'bold',
},
{
text: ', ',
},
{
italic: true,
text: 'italic',
},
{
text: ', ',
},
{
text: 'underlined',
underline: true,
},
{
text: ', or apply a ',
},
{
bold: true,
italic: true,
text: 'combination',
underline: true,
},
{
text: ' of these styles for a visually striking effect.',
},
],
type: 'p',
},
];
使用 HTML 字符串初始化编辑器内容
你还可以使用 HTML 字符串和相应的插件指定编辑器的初始内容。
// ...
const htmlValue = '<p>This is <b>bold</b> and <i>italic</i> text!</p>';
export default function BasicEditor() {
const editor = usePlateEditor({
// ...
value: htmlValue,
});
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
就这样!
你现在可以玩转 Playground 并开始构建你自己的编辑器。