@standhigher/react-email-editor 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +24 -104
- package/README.zh-CN.md +24 -104
- package/dist/index.d.ts +641 -120
- package/dist/index.js +4797 -4221
- package/dist/styles.css +216 -39
- package/docs/advanced-examples.md +265 -0
- package/docs/advanced-examples.zh-CN.md +265 -0
- package/docs/api.md +275 -0
- package/docs/api.zh-CN.md +275 -0
- package/docs/assets/architecture.png +0 -0
- package/docs/assets/architecture.svg +84 -0
- package/docs/assets/architecture.zh-CN.png +0 -0
- package/docs/assets/architecture.zh-CN.svg +84 -0
- package/docs/basic-examples.md +113 -0
- package/docs/basic-examples.zh-CN.md +113 -0
- package/docs/custom-blocks.md +487 -0
- package/docs/custom-blocks.zh-CN.md +486 -0
- package/package.json +3 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# 复杂场景示例
|
|
2
|
+
|
|
3
|
+
[English](./advanced-examples.md) | [API 参考](./api.zh-CN.md) | [基础场景](./basic-examples.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
每段标注文件名的代码保存为独立文件;引入一次包样式。业务请求通过 Props 注入,示例不会假设你的后端路径。
|
|
6
|
+
|
|
7
|
+
## 1. 校验后异步保存
|
|
8
|
+
|
|
9
|
+
校验可提前展示控件错误,导出仍需捕获插件渲染异常。调用方实现 `saveDocument(data)`,返回保存完成的 Promise;失败会显示可重试状态。
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// ValidatedEditor.tsx
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
EmailEditor, useEmailEditor, type EmailEditorData,
|
|
16
|
+
} from "@standhigher/react-email-editor";
|
|
17
|
+
|
|
18
|
+
interface ValidatedEditorProps {
|
|
19
|
+
saveDocument: (data: EmailEditorData) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ValidatedEditor({ saveDocument }: ValidatedEditorProps) {
|
|
23
|
+
const editor = useEmailEditor();
|
|
24
|
+
const [status, setStatus] = useState("尚未保存");
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
|
|
27
|
+
async function save() {
|
|
28
|
+
setSaving(true);
|
|
29
|
+
try {
|
|
30
|
+
const issues = editor.validate();
|
|
31
|
+
if (issues.length) {
|
|
32
|
+
setStatus(issues.map((issue) => `${issue.controlPath}: ${issue.message}`).join(";"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const data = editor.getData();
|
|
36
|
+
await saveDocument(data);
|
|
37
|
+
setStatus("本次快照已保存;保存期间的后续编辑需再次保存");
|
|
38
|
+
} catch (error) {
|
|
39
|
+
setStatus(error instanceof Error ? error.message : "保存失败,请重试");
|
|
40
|
+
} finally {
|
|
41
|
+
setSaving(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<EmailEditor editor={editor} height={720} />
|
|
48
|
+
<button type="button" disabled={saving} onClick={save}>{saving ? "保存中…" : "保存 / 重试"}</button>
|
|
49
|
+
<output aria-live="polite">{status}</output>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`getData()` 同时生成模板与预览 HTML;只需要草稿 JSON 时使用 `getDocument()`,不会因无效控件或模板渲染阻止草稿持久化。保存期间编辑器仍可编辑,保存结果对应点击时的独立快照。
|
|
56
|
+
|
|
57
|
+
## 2. 多实例共享配置,局部订阅文档
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// DocumentSummary.tsx
|
|
61
|
+
import { useEmailEditorState, type EmailEditorInstance } from "@standhigher/react-email-editor";
|
|
62
|
+
|
|
63
|
+
interface DocumentSummaryProps {
|
|
64
|
+
editor: EmailEditorInstance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function DocumentSummary({ editor }: DocumentSummaryProps) {
|
|
68
|
+
const { document } = useEmailEditorState(editor);
|
|
69
|
+
return <output>{document.blocks.length} 个区块</output>;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// ComparisonEditors.tsx
|
|
75
|
+
import { EmailEditor, useEmailEditor, useEmailEditorConfig } from "@standhigher/react-email-editor";
|
|
76
|
+
import { DocumentSummary } from "./DocumentSummary";
|
|
77
|
+
|
|
78
|
+
const builtins = ["heading", "text", "button", "image"];
|
|
79
|
+
|
|
80
|
+
export function ComparisonEditors() {
|
|
81
|
+
const config = useEmailEditorConfig({ builtins });
|
|
82
|
+
const left = useEmailEditor({ config, body: { contentWidth: 640 } });
|
|
83
|
+
const right = useEmailEditor({ config, body: { contentWidth: 480 } });
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<DocumentSummary editor={left} /><EmailEditor editor={left} height={600} />
|
|
87
|
+
<DocumentSummary editor={right} /><EmailEditor editor={right} height={600} />
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
两个实例的文档、选择、重置互相独立。仅摘要组件订阅文档变化。共享配置定义区块能力,不共享可变文档。
|
|
94
|
+
|
|
95
|
+
## 3. 业务区块、多区域富文本与列表
|
|
96
|
+
|
|
97
|
+
以下商品推荐区块同时包含两个富文本编辑区和一个商品列表。控件声明是类型和默认值的唯一来源;Canvas 负责编辑布局,`renderEmail` 负责邮件 HTML。
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// recommendation-controls.ts
|
|
101
|
+
import { EmailControls, type ControlValues } from "@standhigher/react-email-editor";
|
|
102
|
+
|
|
103
|
+
export const recommendationControls = {
|
|
104
|
+
intro: EmailControls.richText({ label: "介绍", defaultValue: "<p>本周推荐</p>" }),
|
|
105
|
+
footer: EmailControls.richText({ label: "结语", defaultValue: "<p>谢谢订阅</p>" }),
|
|
106
|
+
products: EmailControls.list({
|
|
107
|
+
label: "商品", min: 1, max: 8,
|
|
108
|
+
controls: {
|
|
109
|
+
name: EmailControls.text({ label: "名称", validate: (value) => value.trim() ? undefined : "请填写名称" }),
|
|
110
|
+
image: EmailControls.image({ label: "图片" }),
|
|
111
|
+
url: EmailControls.url({ label: "链接" }),
|
|
112
|
+
},
|
|
113
|
+
defaultValue: [{ name: "示例商品", image: "", url: "https://example.com/product" }],
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
export type RecommendationValues = ControlValues<typeof recommendationControls>;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// RecommendationCanvas.tsx
|
|
121
|
+
import { EditableRichText, type PluginEditorProps } from "@standhigher/react-email-editor";
|
|
122
|
+
import type { RecommendationValues } from "./recommendation-controls";
|
|
123
|
+
|
|
124
|
+
export function RecommendationCanvas({ controls }: PluginEditorProps<RecommendationValues>) {
|
|
125
|
+
return (
|
|
126
|
+
<div>
|
|
127
|
+
<EditableRichText control="intro" />
|
|
128
|
+
<ul>{controls.products.map((product, index) => <li key={index}>{product.name}</li>)}</ul>
|
|
129
|
+
<EditableRichText control="footer" />
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// RecommendationInspector.tsx
|
|
137
|
+
import { ControlEditor } from "@standhigher/react-email-editor";
|
|
138
|
+
|
|
139
|
+
export function RecommendationInspector() {
|
|
140
|
+
return <ControlEditor control="products" />;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// recommendation.ts
|
|
146
|
+
import { defineBlock } from "@standhigher/react-email-editor";
|
|
147
|
+
import { recommendationControls } from "./recommendation-controls";
|
|
148
|
+
import { RecommendationCanvas } from "./RecommendationCanvas";
|
|
149
|
+
import { RecommendationInspector } from "./RecommendationInspector";
|
|
150
|
+
|
|
151
|
+
export const recommendation = defineBlock({
|
|
152
|
+
type: "shop.recommendation",
|
|
153
|
+
title: "商品推荐",
|
|
154
|
+
controls: recommendationControls,
|
|
155
|
+
Canvas: RecommendationCanvas,
|
|
156
|
+
Inspector: RecommendationInspector,
|
|
157
|
+
defaultStyle: { dimension: { padding: "16px" } },
|
|
158
|
+
renderEmail: ({ controls, email }) => email.html`
|
|
159
|
+
${email.richText(controls.intro)}
|
|
160
|
+
${controls.products.map((product) => email.html`
|
|
161
|
+
<p><strong>${product.name}</strong></p>
|
|
162
|
+
${product.image ? email.image({ src: product.image, alt: product.name }) : email.raw("")}
|
|
163
|
+
${email.button({ href: product.url, label: "查看商品" })}
|
|
164
|
+
`)}
|
|
165
|
+
${email.richText(controls.footer)}
|
|
166
|
+
`,
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// RecommendationEditor.tsx
|
|
172
|
+
import { useMemo, useState } from "react";
|
|
173
|
+
import {
|
|
174
|
+
createEmailDocument, EmailEditor, useEmailEditor, useEmailEditorConfig,
|
|
175
|
+
} from "@standhigher/react-email-editor";
|
|
176
|
+
import { recommendation } from "./recommendation";
|
|
177
|
+
|
|
178
|
+
const plugins = [recommendation];
|
|
179
|
+
|
|
180
|
+
export function RecommendationEditor() {
|
|
181
|
+
const config = useEmailEditorConfig({ plugins });
|
|
182
|
+
const initialDocument = useMemo(
|
|
183
|
+
() => createEmailDocument([config.createBlock(recommendation.type)]),
|
|
184
|
+
[config],
|
|
185
|
+
);
|
|
186
|
+
const editor = useEmailEditor({ config, initialDocument });
|
|
187
|
+
const [html, setHtml] = useState("");
|
|
188
|
+
const [error, setError] = useState("");
|
|
189
|
+
|
|
190
|
+
function exportHtml() {
|
|
191
|
+
try {
|
|
192
|
+
// 同一配置也可供不挂载 UI 的导出流程使用。
|
|
193
|
+
setHtml(config.toHtml(editor.getDocument()));
|
|
194
|
+
setError("");
|
|
195
|
+
} catch (cause) {
|
|
196
|
+
setError(cause instanceof Error ? cause.message : "导出失败");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return (
|
|
200
|
+
<>
|
|
201
|
+
<EmailEditor editor={editor} height={720} />
|
|
202
|
+
<button type="button" onClick={exportHtml}>导出</button>
|
|
203
|
+
{error ? <p role="alert">{error}</p> : null}
|
|
204
|
+
<pre>{html}</pre>
|
|
205
|
+
</>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`config.toHtml()` 和编辑器共用同一注册表,保存 JSON 后再加载也必须注册该插件。列表编辑由 `ControlEditor` 提供增加、删除及子控件编辑;Canvas 的列表只展示商品名称。业务选择器一次填充多个关联字段可使用 `PluginEditorProps.setControls(patch)`,嵌套列表提交完整数组。
|
|
211
|
+
|
|
212
|
+
## 4. 接入已有图片资源选择器
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// ResourceEditor.tsx
|
|
216
|
+
import { useCallback } from "react";
|
|
217
|
+
import {
|
|
218
|
+
EmailEditor, useEmailEditor, useEmailEditorConfig, type ImagePickerContext,
|
|
219
|
+
} from "@standhigher/react-email-editor";
|
|
220
|
+
|
|
221
|
+
interface ResourceEditorProps {
|
|
222
|
+
openLibrary: (request: { currentUrl: string; signal: AbortSignal }) => Promise<string | null>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function ResourceEditor({ openLibrary }: ResourceEditorProps) {
|
|
226
|
+
const pickImage = useCallback(
|
|
227
|
+
({ value, signal }: ImagePickerContext) => openLibrary({ currentUrl: value, signal }),
|
|
228
|
+
[openLibrary],
|
|
229
|
+
);
|
|
230
|
+
const config = useEmailEditorConfig({ pickImage });
|
|
231
|
+
const editor = useEmailEditor({ config });
|
|
232
|
+
return <EmailEditor editor={editor} height={720} />;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
调用方的资源库应处理弹窗、搜索、空结果、上传与取消;传入 signal 给网络请求。返回允许协议的地址,返回 null 表示取消,抛错由编辑器显示重试。编辑器对图片请求提供 loading/error/retry,忽略过期结果,卸载时取消请求。
|
|
237
|
+
|
|
238
|
+
## 5. 批量模板数据与单区块预览
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
// template-preview.ts
|
|
242
|
+
import {
|
|
243
|
+
BLOCK_TYPES, blockToHtml, createBlockNode, createEmailDocument, documentToHtml,
|
|
244
|
+
mergeTemplateData, type Variable,
|
|
245
|
+
} from "@standhigher/react-email-editor";
|
|
246
|
+
|
|
247
|
+
const block = createBlockNode(BLOCK_TYPES.html);
|
|
248
|
+
if (!block) throw new Error("HTML 区块未注册");
|
|
249
|
+
block.controls.html = `<p>你好,{{.customer}}</p>
|
|
250
|
+
{{range .products}}<p>{{.name}}</p>{{end}}`;
|
|
251
|
+
const variables: Variable[] = [{ name: "{{.customer}}", value: "李小明" }];
|
|
252
|
+
const templateData = { products: [{ name: "茶叶" }, { name: "咖啡" }] };
|
|
253
|
+
const document = createEmailDocument([block]);
|
|
254
|
+
|
|
255
|
+
export const template = documentToHtml(document, undefined, { variables });
|
|
256
|
+
export const preview = documentToHtml(document, undefined, {
|
|
257
|
+
variables, templateData, resolveHtmlTemplates: true,
|
|
258
|
+
});
|
|
259
|
+
export const fragment = blockToHtml(block, undefined, "email", {
|
|
260
|
+
resolveHtmlTemplate: true,
|
|
261
|
+
templateData: mergeTemplateData(variables, { ...templateData, ...block.defaultData }),
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
完整文档自动按顶层浅合并 `variables → templateData → block.defaultData`,后者优先。单区块不会自动合并变量;示例显式合并,并使用单数 `resolveHtmlTemplate`。真实发送端的模板引擎由应用决定,预览仅支持部分 Go template 语法。
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
[简体中文](./api.zh-CN.md) | [Back to README](../README.md)
|
|
4
|
+
|
|
5
|
+
Import all APIs below from `@standhigher/react-email-editor`, and import `@standhigher/react-email-editor/styles.css` once. The published `dist/index.d.ts` includes English JSDoc for externally accessible fields, available through IDE hover information. See [basic examples](./basic-examples.md) and [advanced examples](./advanced-examples.md) for integration code.
|
|
6
|
+
|
|
7
|
+
## Editor component and Hooks
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { EmailEditor, useEmailEditor } from "@standhigher/react-email-editor";
|
|
11
|
+
import "@standhigher/react-email-editor/styles.css";
|
|
12
|
+
|
|
13
|
+
export function EmailPage() {
|
|
14
|
+
const editor = useEmailEditor();
|
|
15
|
+
return <EmailEditor editor={editor} height={720} />;
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### EmailEditor
|
|
20
|
+
|
|
21
|
+
| Parameter | Type | Meaning and default |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `editor` | `EmailEditorInstance` | Required instance returned by `useEmailEditor()`. |
|
|
24
|
+
| `height` | `number \| string` | Default `"100%"`; numbers use px; percentages require a parent with an explicit height. |
|
|
25
|
+
|
|
26
|
+
The component provides UI; the instance works independently of mounting it. Component refs, legacy configuration factories and document props are unsupported.
|
|
27
|
+
|
|
28
|
+
### useEmailEditor(options?) → EmailEditorInstance
|
|
29
|
+
|
|
30
|
+
All options are optional. Defaults to an empty document and all builtin blocks. Follow React Hook rules.
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Meaning |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `initialDocument` | `EmailDocument` | Read only during initialization; use `setDocument()` for later replacements. |
|
|
35
|
+
| `body` | `Partial<EmailBodySettings>` | Initial overrides; later changes merge into the current document. Omitted fields retain their values. |
|
|
36
|
+
| `config` | `EmailEditorConfig` | Result of `useEmailEditorConfig()`; updates preserve the document. |
|
|
37
|
+
| `variables` | `Variable[]` | Insertable preview variables, default `[]`; updates affect preview and export. |
|
|
38
|
+
| `onChange` | `(document) => void` | Detached snapshot after edits, replacements, resets and effective external body updates; no initialization call. |
|
|
39
|
+
| `onBodyChange` | `(body) => void` | Detached body snapshot after body edits inside the editor only. |
|
|
40
|
+
|
|
41
|
+
The instance reference is stable; instances are isolated. Configuration, variables, body and callbacks synchronize during React commit. Interrupted renders do not change the committed instance. Child layout effects can read updated options; subscriptions and external body `onChange` notifications run during layout. A component using only this Hook does not rerender on document edits.
|
|
42
|
+
|
|
43
|
+
### Instance methods
|
|
44
|
+
|
|
45
|
+
| Method | Arguments | Return and behavior |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `getData()` | None | `EmailEditorData`: `document`, formatted `json`, template `html`, and preview `renderedHtml`; rendering errors throw. |
|
|
48
|
+
| `getDocument()` | None | Detached `EmailDocument` snapshot to persist or modify before passing to `setDocument()`. |
|
|
49
|
+
| `getJson()` | None | Pretty-printed document JSON string. |
|
|
50
|
+
| `getHtml()` | None | Complete email HTML preserving templates. |
|
|
51
|
+
| `getRenderedHtml()` | None | Complete HTML using current variables and block `defaultData`. |
|
|
52
|
+
| `setDocument(document)` | v4 `EmailDocument` | `void`; copies and replaces the document, clears selection/focus/control state and calls `onChange`. |
|
|
53
|
+
| `reset()` | None | `void`; restores the initialized document, including initial body overrides, clears interaction state and calls `onChange`. |
|
|
54
|
+
| `validate()` | None | `ValidationIssue[]`; validates controls and missing plugins without rendering HTML. Rendering errors still need handling. |
|
|
55
|
+
|
|
56
|
+
Exports and callback arguments do not expose mutable internal state. Missing plugins, invalid controls and rendering failures throw during export; catch these in your application.
|
|
57
|
+
|
|
58
|
+
### useEmailEditorState(editor) → EmailEditorState
|
|
59
|
+
|
|
60
|
+
Requires an instance from `useEmailEditor()`. Returns cached `{ document }`, a deeply readonly snapshot, and notifies only on document changes. Use it in components that need reactive document display; update through instance methods.
|
|
61
|
+
|
|
62
|
+
## Shared configuration and plugins
|
|
63
|
+
|
|
64
|
+
### useEmailEditorConfig(options?) → EmailEditorConfig
|
|
65
|
+
|
|
66
|
+
| Parameter | Type | Default and meaning |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `plugins` | `BlockPlugin[]` | `[]`; plugins created by `defineBlock()`. |
|
|
69
|
+
| `builtins` | `false \| string[]` | All by default; false hides all builtins; arrays filter and order them. Hidden blocks still render existing nodes. Custom types are appended. |
|
|
70
|
+
| `pickImage` | `(context: ImagePickerContext) => Promise<string \| null>` | Resolve a URL; null cancels; throwing shows retry. Context contains `value/label/signal`. |
|
|
71
|
+
|
|
72
|
+
Declare plugins at module scope, or use `useMemo` when they depend on application state. Stable option references reuse configuration; inline arrays do not recreate the editor or reset its document. The returned object contains:
|
|
73
|
+
|
|
74
|
+
| Member | Arguments and meaning |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `definitions` | Readonly list of all registered plugins, including hidden builtins. |
|
|
77
|
+
| `library` | Readonly list of insertable blocks in display order. |
|
|
78
|
+
| `pickImage` | Registered resource picker. |
|
|
79
|
+
| `createBlock(type)` | Registered type string → detached default `EmailBlockNode`; unknown types throw. |
|
|
80
|
+
| `toHtml(document, options?)` | v4 document and `DocumentHtmlOptions` → complete HTML using this registry. |
|
|
81
|
+
| `validate(document)` | Document → `ValidationIssue[]`, without rendering HTML. |
|
|
82
|
+
|
|
83
|
+
Duplicate custom types, duplicate builtin entries and unknown builtin names throw. Replacing a builtin requires `override: true`.
|
|
84
|
+
|
|
85
|
+
### defineBlock(input) → BlockPlugin<Schema, Type>
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { defineBlock, EmailControls } from "@standhigher/react-email-editor";
|
|
89
|
+
|
|
90
|
+
export const coupon = defineBlock({
|
|
91
|
+
type: "shop.coupon",
|
|
92
|
+
title: "Coupon",
|
|
93
|
+
controls: { code: EmailControls.text({ label: "Code", defaultValue: "SAVE20" }) },
|
|
94
|
+
renderEmail: ({ controls, email }) => email.html`<p>${controls.code}</p>`,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Input member | Meaning |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `type` / `title` | Required nonempty unique type and library title. |
|
|
101
|
+
| `renderEmail(ctx)` | Required; returns inner HTML or `EmailMarkup`; ctx contains `block/controls/style/body/mode/email`. |
|
|
102
|
+
| `controls` | Map of names to EmailControls declarations, default `{}`; values and defaults are inferred. |
|
|
103
|
+
| `icon` / `description` | Optional ReactNode/string icon and description. |
|
|
104
|
+
| `override` | Must be true to replace a builtin with the same type. |
|
|
105
|
+
| `containerStyles` | Spacing and appearance enabled by default; false disables all; `{ spacing?: boolean, appearance?: boolean }` controls each group. |
|
|
106
|
+
| `styleControls` | Additional styles; matching IDs replace default container controls. |
|
|
107
|
+
| `inspectorSections` | Ordered inspector sections and content/style controls. |
|
|
108
|
+
| `defaultStyle` | New-node styles, taking precedence over style control defaultValue. |
|
|
109
|
+
| `defaultData` | Template preview data for new nodes. |
|
|
110
|
+
| `Canvas` / `Inspector` | Optional React editing components receiving `PluginEditorProps<ControlValues<Schema>>`. |
|
|
111
|
+
|
|
112
|
+
`Schema` infers control values; `Type` preserves the type literal. Declarations are copied and frozen. Multiple inline rich-text regions require a Canvas. See the [custom block guide](./custom-blocks.md) for detailed behavior and inherited runtime members.
|
|
113
|
+
|
|
114
|
+
### EmailControls
|
|
115
|
+
|
|
116
|
+
Each factory takes an options object and returns `ControlDefinition<T>`. Common options apply only to relevant control kinds.
|
|
117
|
+
|
|
118
|
+
| Parameter | Meaning |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `label` | Required display label. |
|
|
121
|
+
| `defaultValue` | Fills only persisted undefined; does not replace `""`, `0` or false. |
|
|
122
|
+
| `placeholder` / `rows` | Empty hint / multiline editor rows. |
|
|
123
|
+
| `options` | Allowed `ControlOption[]`, each with label and value. |
|
|
124
|
+
| `min` / `max` / `step` / `unit` | Numeric limits, increment and unit label; list min/max limit row counts. |
|
|
125
|
+
| `profile` / `inline` | Rich-text feature profile / direct editing on the default canvas. |
|
|
126
|
+
| `component` | Custom React control receiving `ControlComponentProps<T>`. |
|
|
127
|
+
| `validate(value)` | Normalized value → error string, or undefined when valid. |
|
|
128
|
+
|
|
129
|
+
| Factory | Value and fallback | Additional behavior |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| `text/textarea/html/url/image/color/select` | string, default `""` | URL/image protocol validation; select options validation. |
|
|
132
|
+
| `richText` | HTML string, default `"<p></p>"` | profile text/heading/button, inline editing. |
|
|
133
|
+
| `number` | number, default 0 | Invalid input becomes NaN and fails validation. |
|
|
134
|
+
| `boolean` | boolean, default false | Only actual booleans are accepted; other values normalize to false. |
|
|
135
|
+
| `list` | Inferred row array, default [] | Required `controls: ControlSchema`; recursive parsing and validation. |
|
|
136
|
+
| `custom<T>` | Application-defined | Required defaultValue, pure `read(value: unknown): T`, and component; values must be JSON-persistable. |
|
|
137
|
+
|
|
138
|
+
### Plugin React components
|
|
139
|
+
|
|
140
|
+
`PluginEditorProps` provides `block/controls/style/body/variables` snapshots, selected, renderedHtml, select(), setControl(name, value), setControls(patch), and setStyle(id, value). Batch control updates shallow-merge once and notify once; replace nested objects entirely. renderedHtml contains resolved inner HTML in Canvas and is empty in Inspector.
|
|
141
|
+
|
|
142
|
+
| Component | Parameters |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `ControlEditor` | Required control string, reusing its declaration, validation and resource adapter. Unknown names throw. |
|
|
145
|
+
| `EditableRichText` | Required control string referencing richText; optional profile and placeholder overrides. |
|
|
146
|
+
|
|
147
|
+
Use these components only within a plugin Canvas or Inspector.
|
|
148
|
+
|
|
149
|
+
### Style control factories
|
|
150
|
+
|
|
151
|
+
`createDimensionControls(overrides?)`, `createDecorationControls(overrides?)`, and `createTypographyControls(overrides?)` return independent StyleControlConfig objects keyed by capability name. Overrides are `Partial<Record<Name, Partial<StyleControlConfig>>>`, modifying labels, defaults, units and ranges. Add selected entries to plugin styleControls.
|
|
152
|
+
|
|
153
|
+
| Factory / name type | Capability names |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `createDimensionControls` / `DimensionControlName` | padding/margin/height. |
|
|
156
|
+
| `createDecorationControls` / `DecorationControlName` | backgroundColor/borderWidth/borderStyle/borderColor/borderRadius. |
|
|
157
|
+
| `createTypographyControls` / `TypographyControlName` | fontFamily/fontSize/fontWeight/lineHeight/color/textAlign/paragraphSpacing/listSpacing/listIndent/linkColor/linkDecoration. |
|
|
158
|
+
|
|
159
|
+
## Document model and creation
|
|
160
|
+
|
|
161
|
+
| API | Arguments | Return and limitations |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `createEmailDocument(blocks = [])` | Ordered EmailBlockNode array | v4 document with full default body. Retains the supplied array; does not deep-clone it. |
|
|
164
|
+
| `resolveEmailBody(body?)` | `Partial<EmailBodySettings> \| null` | Complete normalized body; invalid values fall back; width is clamped to 320–900px. |
|
|
165
|
+
| `createBlockNode(type, definitions?)` | Type and optional registry, default builtins | Detached default node; unknown type returns null. Prefer config.createBlock for application plugins. |
|
|
166
|
+
| `getBlockDefinition(type, definitions?)` | Type and optional registry, default builtins | Matching runtime definition or undefined. Do not mutate frozen configuration definitions. |
|
|
167
|
+
| `generateId(prefix = "block")` | String ID prefix | String with a random suffix; applications must still ensure IDs are unique within a document. |
|
|
168
|
+
|
|
169
|
+
EmailDocument contains version/body/blocks/subject?; EmailBlockNode contains id/type/controls/style?/defaultData?. Only DOCUMENT_VERSION = 4 is supported; older documents are not migrated. JSON stores no components or render functions; register matching plugins again when loading it.
|
|
170
|
+
|
|
171
|
+
Canvas, preview and email HTML share body settings:
|
|
172
|
+
|
|
173
|
+
| EmailBodySettings member | Default and meaning |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| textColor | #000000; default text color. |
|
|
176
|
+
| backgroundColor | #ffffff; centered content background; outside remains white. |
|
|
177
|
+
| contentWidth | 600; maximum content width in px. |
|
|
178
|
+
| fontFamily | Arial, Helvetica, sans-serif. |
|
|
179
|
+
| fontWeight | normal; also supports bold. |
|
|
180
|
+
| linkColor / linkDecoration | #005bd3 / underline. |
|
|
181
|
+
| contentPadding | 0px; CSS padding inside the border. |
|
|
182
|
+
| blockGap | 4px; spacing between adjacent blocks. |
|
|
183
|
+
| borderWidth / borderStyle / borderColor | 0px / solid / transparent. |
|
|
184
|
+
|
|
185
|
+
The body is centered without body radius or extra outer padding. Explicit rich-text formats override body defaults; internal defaults provide 16px font size and 1.5 line height. HTML blocks retain their internal links/lists. Block container styles live in dimension/decorations, inner elements in element, and custom blocks can enable typography. Only enabled styles are read.
|
|
186
|
+
|
|
187
|
+
## HTML rendering and templates
|
|
188
|
+
|
|
189
|
+
### documentToHtml(document, definitions?, options?) → string
|
|
190
|
+
|
|
191
|
+
The required document must use v4. Definitions default to DEFAULT_BLOCKS; for custom plugins pass config.definitions or use config.toHtml. Returns a complete email document with one doctype/html/head/body, tables and inline styles.
|
|
192
|
+
|
|
193
|
+
| DocumentHtmlOptions member | Default and meaning |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| resolveHtmlTemplates | false preserves templates; true resolves preview data. |
|
|
196
|
+
| variables | [] of exact inserted tokens and preview values. |
|
|
197
|
+
| templateData | Optional TemplateData for global objects and arrays. |
|
|
198
|
+
|
|
199
|
+
Data is shallow-merged at the top level: node defaultData > templateData > variables. Default export only adds missing dots to tokens exactly matching variables[].name, preserving spaces: `{{ store_name }}` → `{{ .store_name }}`. Unlisted Go templates stay unchanged. Empty variable descriptions are hidden; value is only preview data.
|
|
200
|
+
|
|
201
|
+
### Single-block and low-level helpers
|
|
202
|
+
|
|
203
|
+
| API | Arguments and defaults | Output |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `renderBlock(node, definitions = DEFAULT_BLOCKS, mode = "email", body = DEFAULT_EMAIL_BODY)` | Node, registry, RenderMode, full body | Plugin inner HTML without template resolution or dot normalization. |
|
|
206
|
+
| `renderBlockContent(node, definitions = DEFAULT_BLOCKS, mode = "email", options = {})` | Final argument is BlockRenderOptions | Inner HTML with dot normalization or template resolution. |
|
|
207
|
+
| `blockToHtml(node, definitions = DEFAULT_BLOCKS, mode = "email", options = {})` | Same | Email table fragment with container styles and spacing, not a complete document. |
|
|
208
|
+
| `buildRenderContext(node, mode, body = DEFAULT_EMAIL_BODY, definition = builtin lookup)` | Required mode; explicitly pass a custom definition | Detached normalized BlockRenderContext without rendering. |
|
|
209
|
+
| `variablesToTemplateData(variables)` | Required Variable[] | TemplateData keyed by token paths with value as preview data; ignores absent values. |
|
|
210
|
+
| `mergeTemplateData(variables, defaultData?)` | Variables and optional object | Shallow merge, defaultData wins; undefined if both contain no data. |
|
|
211
|
+
| `renderTemplatePreview(html, defaultData?)` | HTML string and optional data | Supports a subset of Go paths, range, if and with; preserves input when data is omitted. |
|
|
212
|
+
|
|
213
|
+
BlockRenderOptions contains resolveHtmlTemplate?: boolean (singular), templateData?: TemplateData, variables?: Variable[], body?: EmailBodySettings. Body defaults to DEFAULT_EMAIL_BODY; templates are preserved by default. Single-block preview uses templateData ?? node.defaultData and does not merge variables automatically; call mergeTemplateData explicitly when needed. The fourth argument must be an object; boolean or null throws TypeError.
|
|
214
|
+
|
|
215
|
+
Mode selects the consumer: canvas for editing, email for control validation. It does not enable template resolution. Preview is a subset of Go templates, not a complete Go engine, HTML sanitizer or security sandbox.
|
|
216
|
+
|
|
217
|
+
### email helpers in renderEmail
|
|
218
|
+
|
|
219
|
+
| Method | Arguments and output |
|
|
220
|
+
| --- | --- |
|
|
221
|
+
| ``email.html`…${value}…` `` | Escapes ordinary interpolations; composes EmailMarkup and arrays; preserves template tokens. |
|
|
222
|
+
| `email.raw(html)` | Raw HTML → EmailMarkup, without sanitization. |
|
|
223
|
+
| `email.richText(html)` | Rich-text HTML → EmailMarkup with body defaults. |
|
|
224
|
+
| `email.table(rows)` | EmailMarkup[][] → email table fragment. |
|
|
225
|
+
| `email.image({ src, alt?, width? })` | Required src; alt defaults empty, width to 100%; validates image protocols. |
|
|
226
|
+
| `email.button({ href, label })` | Required URL and plain-text label; empty/invalid URL produces no link. |
|
|
227
|
+
| `email.url(value, image = false)` | Allowed URL or empty string; true selects image protocol rules. |
|
|
228
|
+
|
|
229
|
+
Links allow http/https/mailto/tel, anchors or templates; images allow http/https/cid or templates. Validate protocols before escaping attributes; plugins returning plain strings handle escaping themselves.
|
|
230
|
+
|
|
231
|
+
## Base UI components
|
|
232
|
+
|
|
233
|
+
Use these in business inspectors. They provide UI without automatically binding document state. Callbacks receive values, except TextInput which uses native React events.
|
|
234
|
+
|
|
235
|
+
| Component | Parameters and defaults |
|
|
236
|
+
| --- | --- |
|
|
237
|
+
| TextInput | Native React input attributes and ref; type=text, allowing text/url/email/search/tel/password/number. |
|
|
238
|
+
| SelectControl | Required value/options/onChange(value); optional id, accessible label, emptyLabel, compact=false, disabled. |
|
|
239
|
+
| CheckboxControl | Required label/checked/onChange(checked); optional disabled, className, variant=checkbox or switch. |
|
|
240
|
+
| ColorControl | Required value/onChange(value); fallback=#000000, label=Color, resetTo=fallback. |
|
|
241
|
+
| RangeControl | Required value/onChange(value); optional id/label; min=0, max=100, step=1. |
|
|
242
|
+
| Button | Native React button attributes, type=button; variant=default/primary/solid/icon; optional tooltip. |
|
|
243
|
+
| RichText | Required initialHtml, variables, selected, toolbarId, toolbarCollapsed, onCommit(html), onSelect(); optional profile=text, placeholder, inherited. toolbarId must be unique. Prefer EditableRichText for plugin binding. |
|
|
244
|
+
|
|
245
|
+
Props and `RichText.inherited` fields are documented in the published `.d.ts`; use IDE hover information or go to definition. Native attributes follow React types; props interfaces are not added as named package exports.
|
|
246
|
+
|
|
247
|
+
## Constants and public types
|
|
248
|
+
|
|
249
|
+
| Export | Purpose |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| BLOCK_TYPES / BuiltinBlockType | heading/text/button/image/divider/spacer/html. |
|
|
252
|
+
| DEFAULT_BLOCKS | All builtin BlockPlugin definitions; custom registries use config.definitions. |
|
|
253
|
+
| DEFAULT_EMAIL_BODY / DOCUMENT_VERSION | Default body / document version 4. Do not mutate the default object. |
|
|
254
|
+
| FONT_FAMILIES | Readonly label/value font options. |
|
|
255
|
+
| STYLE_GROUPS / StyleGroup | general/dimension/typography/decorations identifiers and display names. |
|
|
256
|
+
| BORDER_STYLES / BorderStyle | none/solid/dashed/dotted. |
|
|
257
|
+
| FONT_WEIGHTS / FontWeight | normal/bold. |
|
|
258
|
+
| HORIZONTAL_ALIGNS / HorizontalAlign | left/center/right. |
|
|
259
|
+
| TEXT_ALIGNS / TextAlign | left/center/right/justify. |
|
|
260
|
+
| LINK_DECORATIONS / LinkDecoration | underline/none. |
|
|
261
|
+
|
|
262
|
+
Fields in editor, document, control, plugin, style, rendering and template types are documented through JSDoc in the published `.d.ts`. BlockDefinition is internal and not exported from the package entry; use BlockPlugin for public plugin definitions.
|
|
263
|
+
|
|
264
|
+
## Development and local linking
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
pnpm typecheck
|
|
268
|
+
pnpm lint
|
|
269
|
+
pnpm build
|
|
270
|
+
node --test tests/*.test.mjs tests/*.test.ts
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Build before tests that depend on dist. Internal TypeScript tests require Node 22.18+. The package ships ESM, declarations and flattened CSS. React/React DOM are peer dependencies; TipTap, dnd-kit, CodeMirror and Lezer are dependencies, externalized during builds rather than bundled.
|
|
274
|
+
|
|
275
|
+
Local consumers can use link:../react-email-editor. Run pnpm build first and pnpm build:watch to update dist. Both projects must share one React instance: Vite can use resolve.dedupe: ["react", "react-dom"]; Next.js can use transpilePackages: ["@standhigher/react-email-editor"], with Turbopack root covering the linked directory.
|