@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,113 @@
|
|
|
1
|
+
# 基础场景示例
|
|
2
|
+
|
|
3
|
+
[English](./basic-examples.md) | [API 参考](./api.zh-CN.md) | [复杂场景](./advanced-examples.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
在应用全局样式入口引入一次 `@standhigher/react-email-editor/styles.css`。以下每段组件示例可以独立放入一个 `.tsx` 文件;Next.js App Router 中添加 `"use client"`。
|
|
6
|
+
|
|
7
|
+
## 1. 空编辑器与保存 JSON
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { EmailEditor, useEmailEditor } from "@standhigher/react-email-editor";
|
|
12
|
+
|
|
13
|
+
export function EmailComposer() {
|
|
14
|
+
const editor = useEmailEditor();
|
|
15
|
+
const [savedJson, setSavedJson] = useState("");
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<EmailEditor editor={editor} height={720} />
|
|
20
|
+
<button type="button" onClick={() => setSavedJson(editor.getJson())}>保存 JSON</button>
|
|
21
|
+
<output>{savedJson ? "已保存到当前页面状态" : "尚未保存"}</output>
|
|
22
|
+
</>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
该示例保存到 React 状态,刷新后不会保留;数据库或文件持久化由应用实现。`getDocument()` 返回 JSON 对象快照,`getJson()` 返回格式化字符串,`getHtml()` 返回可交给发送系统的模板 HTML。
|
|
28
|
+
|
|
29
|
+
## 2. 初始内容、切换文档与重置
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import {
|
|
33
|
+
BLOCK_TYPES, createBlockNode, createEmailDocument,
|
|
34
|
+
EmailEditor, useEmailEditor, type EmailDocument,
|
|
35
|
+
} from "@standhigher/react-email-editor";
|
|
36
|
+
|
|
37
|
+
const heading = createBlockNode(BLOCK_TYPES.heading);
|
|
38
|
+
if (heading) heading.controls.html = "<h1>欢迎订阅</h1>";
|
|
39
|
+
const initialDocument = createEmailDocument(heading ? [heading] : []);
|
|
40
|
+
|
|
41
|
+
interface RestorableEditorProps {
|
|
42
|
+
savedDocument: EmailDocument;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function RestorableEditor({ savedDocument }: RestorableEditorProps) {
|
|
46
|
+
const editor = useEmailEditor({ initialDocument });
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<EmailEditor editor={editor} height={640} />
|
|
50
|
+
<button type="button" onClick={() => editor.setDocument(savedDocument)}>载入已保存文档</button>
|
|
51
|
+
<button type="button" onClick={() => editor.reset()}>恢复初始内容</button>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`savedDocument` 应由应用读取并校验为当前 v4 文档;TypeScript 类型不替代运行时 JSON 校验。`initialDocument` 只读取一次;后续切换用 `setDocument()`。`reset()` 恢复首次初始化内容,不是最近载入的文档。
|
|
58
|
+
|
|
59
|
+
## 3. 主体样式、变量与预览导出
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { useState } from "react";
|
|
63
|
+
import {
|
|
64
|
+
BLOCK_TYPES, createBlockNode, createEmailDocument, EmailEditor, useEmailEditor,
|
|
65
|
+
} from "@standhigher/react-email-editor";
|
|
66
|
+
|
|
67
|
+
const text = createBlockNode(BLOCK_TYPES.text);
|
|
68
|
+
if (text) text.controls.html = "<p>你好,{{ customer_name }},欢迎来到 {{ store_name }}</p>";
|
|
69
|
+
const initialDocument = createEmailDocument(text ? [text] : []);
|
|
70
|
+
|
|
71
|
+
export function PersonalizedEditor() {
|
|
72
|
+
const [html, setHtml] = useState("");
|
|
73
|
+
const editor = useEmailEditor({
|
|
74
|
+
initialDocument,
|
|
75
|
+
body: { contentWidth: 640, contentPadding: "24px", backgroundColor: "#f7f7f7" },
|
|
76
|
+
variables: [
|
|
77
|
+
{ name: "{{ store_name }}", description: "店铺名称", value: "示例店铺" },
|
|
78
|
+
{ name: "{{ customer_name }}", value: "李小明" },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
return (
|
|
82
|
+
<>
|
|
83
|
+
<EmailEditor editor={editor} height={720} />
|
|
84
|
+
<button type="button" onClick={() => setHtml(editor.getHtml())}>读取模板 HTML</button>
|
|
85
|
+
<button type="button" onClick={() => setHtml(editor.getRenderedHtml())}>读取预览 HTML</button>
|
|
86
|
+
<pre>{html}</pre>
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
模板导出保留变量,并把列出的 `{{ store_name }}` 补成 `{{ .store_name }}`;预览导出替换为示例店铺等文本。编辑器自带桌面与移动端预览。这里用 `<pre>` 展示导出源码;应用可自行下载或上传。生产导出错误处理见[校验后保存](./advanced-examples.zh-CN.md#1-校验后异步保存)。
|
|
93
|
+
|
|
94
|
+
## 4. 不挂载编辑器直接导出
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import {
|
|
98
|
+
BLOCK_TYPES, createBlockNode, createEmailDocument, documentToHtml,
|
|
99
|
+
} from "@standhigher/react-email-editor";
|
|
100
|
+
|
|
101
|
+
const block = createBlockNode(BLOCK_TYPES.html);
|
|
102
|
+
if (!block) throw new Error("HTML 区块未注册");
|
|
103
|
+
block.controls.html = "<p>你好,{{.customer.name}}</p>";
|
|
104
|
+
const document = createEmailDocument([block]);
|
|
105
|
+
|
|
106
|
+
export const templateHtml = documentToHtml(document);
|
|
107
|
+
export const previewHtml = documentToHtml(document, undefined, {
|
|
108
|
+
resolveHtmlTemplates: true,
|
|
109
|
+
templateData: { customer: { name: "李小明" } },
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
每份导出只有一套 doctype、html、head 和 body。自定义区块需共享注册表,见[复杂场景](./advanced-examples.zh-CN.md#3-业务区块多区域富文本与列表)。
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# Custom block API
|
|
2
|
+
|
|
3
|
+
English | [简体中文](./custom-blocks.zh-CN.md) · [Back to README](../README.md)
|
|
4
|
+
|
|
5
|
+
This guide follows “example → parameters → return values → behavior” for the three extension entry points:
|
|
6
|
+
|
|
7
|
+
| Entry point | Responsibility |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `EmailControls` | Declare controls, value types, defaults, validation, and editing components. |
|
|
10
|
+
| `defineBlock()` | Combine controls, styles, editing UI, and email output into a block plugin. |
|
|
11
|
+
| `useEmailEditorConfig()` | Register plugins and provide block creation, document validation, and HTML export. |
|
|
12
|
+
|
|
13
|
+
Built-in blocks use the same mechanism. Configuration holds no document or selection state and can be shared by multiple editors. `EmailDocument` is the source of truth for document state; the React editing session handles selection, dragging, and updates.
|
|
14
|
+
|
|
15
|
+
## Complete integration example
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import {
|
|
20
|
+
EmailEditor,
|
|
21
|
+
EmailControls,
|
|
22
|
+
createEmailDocument,
|
|
23
|
+
useEmailEditorConfig,
|
|
24
|
+
useEmailEditor,
|
|
25
|
+
defineBlock,
|
|
26
|
+
} from "@standhigher/react-email-editor";
|
|
27
|
+
import "@standhigher/react-email-editor/styles.css";
|
|
28
|
+
|
|
29
|
+
const coupon = defineBlock({
|
|
30
|
+
type: "my-app.coupon",
|
|
31
|
+
title: "Coupon",
|
|
32
|
+
controls: {
|
|
33
|
+
code: EmailControls.text({ label: "Code", defaultValue: "SAVE20" }),
|
|
34
|
+
message: EmailControls.richText({
|
|
35
|
+
label: "Message",
|
|
36
|
+
defaultValue: "<p>Thanks for your support.</p>",
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
renderEmail: ({ controls, email }) => email.html`
|
|
40
|
+
${email.richText(controls.message)}
|
|
41
|
+
<p>Coupon code: ${controls.code}</p>
|
|
42
|
+
`,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export function EmailPage() {
|
|
46
|
+
const config = useEmailEditorConfig({ plugins: [coupon] });
|
|
47
|
+
const [initialDocument] = useState(() => createEmailDocument([config.createBlock(coupon.type)]));
|
|
48
|
+
const editor = useEmailEditor({ config, initialDocument });
|
|
49
|
+
return <EmailEditor editor={editor} />;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
At declaration time, `controls` contains control definitions. In nodes, Canvas/Inspector props, and render contexts, it contains their values:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// defineBlock declaration
|
|
57
|
+
controls: { code: EmailControls.text({ label: "Code", defaultValue: "SAVE20" }) }
|
|
58
|
+
|
|
59
|
+
// Node data returned by config.createBlock()
|
|
60
|
+
{ id: "…", type: "my-app.coupon", controls: { code: "SAVE20" }, style: { /* … */ } }
|
|
61
|
+
|
|
62
|
+
// Read the current value in renderEmail or Canvas / Inspector
|
|
63
|
+
controls.code // string
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 1. useEmailEditorConfig(options?)
|
|
67
|
+
|
|
68
|
+
### Example
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
function useCouponConfig() {
|
|
72
|
+
return useEmailEditorConfig({
|
|
73
|
+
plugins: [coupon],
|
|
74
|
+
builtins: ["heading", "text", "image", "button"],
|
|
75
|
+
pickImage: async ({ value, label, signal }) => {
|
|
76
|
+
// Open your application's asset picker; initialize with value,
|
|
77
|
+
// use label as its title, and cancel requests with signal.
|
|
78
|
+
// Return an image URL, or null when canceled.
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Call this Hook at the top level of a React component or custom Hook. Stable `plugins`, `builtins`, and `pickImage` references retain configuration identity. Updates preserve instance documents. Removing a plugin used by a document preserves its data and produces a missing-plugin validation issue. Configuration methods `toHtml(document)` and `validate(document)` accept any document; instance methods `getHtml()` and `validate()` use the current document.
|
|
86
|
+
|
|
87
|
+
### Parameters: UseEmailEditorConfigOptions
|
|
88
|
+
|
|
89
|
+
All options are optional.
|
|
90
|
+
|
|
91
|
+
| Option | Type | Default and purpose |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `plugins` | `BlockPlugin[]` | Defaults to `[]`. Registers custom blocks; duplicate types throw. |
|
|
94
|
+
| `builtins` | `false \| string[]` | Omit to show all built-ins; use an array to filter and order them, or false to hide all. |
|
|
95
|
+
| `pickImage` | `(context: ImagePickerContext) => Promise<string \| null>` | Image asset picker adapter. Manual URL input remains available without it. |
|
|
96
|
+
|
|
97
|
+
`builtins` only affects library visibility. Hidden built-ins can still be created, loaded, and rendered. The array accepts built-in types only and rejects duplicates. Custom types are appended in plugins order. A plugin overriding a built-in follows that built-in's position and visibility rules. Overrides require `override: true` on the plugin.
|
|
98
|
+
|
|
99
|
+
Built-in types are heading, text, button, image, divider, spacer, and html, available through `BLOCK_TYPES`.
|
|
100
|
+
|
|
101
|
+
### Return value: EmailEditorConfig
|
|
102
|
+
|
|
103
|
+
| Property / method | Type | Meaning and purpose |
|
|
104
|
+
| --- | --- | --- |
|
|
105
|
+
| `definitions` | `readonly BlockPlugin[]` | All registered definitions, including hidden built-ins, used for loading, creation, validation, and rendering. |
|
|
106
|
+
| `library` | `readonly BlockPlugin[]` | Visible library entries after filtering, ordering, and merging plugins. |
|
|
107
|
+
| `pickImage` | Same as the optional input | Registered image picker adapter. |
|
|
108
|
+
| `createBlock(type)` | `(type: string) => EmailBlockNode` | Creates an independent node and ID, cloning default values, styles, and sample data. Unknown types throw. |
|
|
109
|
+
| `toHtml(document, options?)` | `(...) => string` | Synchronously generates a complete email document. Validation and rendering failures throw. |
|
|
110
|
+
| `validate(document)` | `(...) => ValidationIssue[]` | Collects control issues and missing plugins without invoking renderers or changing the document. |
|
|
111
|
+
|
|
112
|
+
`createBlock()` returns a node; it does not insert it into a mounted editor. Configuration and plain declaration metadata are copied and frozen. Functions, React elements, and non-plain objects retain identity; business state captured by closures is not isolated. Recreate plugins and configuration to change declarations.
|
|
113
|
+
|
|
114
|
+
### toHtml options: DocumentHtmlOptions
|
|
115
|
+
|
|
116
|
+
| Option | Type | Purpose |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `resolveHtmlTemplates` | `boolean` | Defaults to preserving templates; true resolves supported Go template expressions using preview data. |
|
|
119
|
+
| `variables` | `Variable[]` | Original variable tokens, optional descriptions, and preview values. Export adds dots only to exactly matching listed tokens. |
|
|
120
|
+
| `templateData` | `Record<string, unknown>` | Data for this template preview. |
|
|
121
|
+
|
|
122
|
+
Preview precedence is node defaultData > templateData > variables, merged by top-level keys. Normal export preserves templates. For example, a listed `{{ store_name }}` becomes `{{ .store_name }}` while preserving whitespace; expressions outside the list remain unchanged.
|
|
123
|
+
|
|
124
|
+
### validate result items: ValidationIssue
|
|
125
|
+
|
|
126
|
+
| Property | Type | Purpose |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `blockId` | `string` | ID of the affected block. |
|
|
129
|
+
| `blockType` | `string` | Type of the affected block. |
|
|
130
|
+
| `controlPath` | `string` | Control path, such as terms[0].text; empty when the plugin is missing. |
|
|
131
|
+
| `message` | `string` | Reason validation failed. |
|
|
132
|
+
|
|
133
|
+
Unsupported document versions throw directly. validate() does not check renderer execution; export can still fail due to a rendering error.
|
|
134
|
+
|
|
135
|
+
## 2. defineBlock(options)
|
|
136
|
+
|
|
137
|
+
### Example: controls, container styles, and inspector sections
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const banner = defineBlock({
|
|
141
|
+
type: "my-app.banner",
|
|
142
|
+
title: "Banner",
|
|
143
|
+
description: "A notice with a link",
|
|
144
|
+
controls: {
|
|
145
|
+
title: EmailControls.text({ label: "Title", defaultValue: "Limited offer" }),
|
|
146
|
+
href: EmailControls.url({ label: "Link" }),
|
|
147
|
+
},
|
|
148
|
+
containerStyles: { spacing: true, appearance: true },
|
|
149
|
+
defaultStyle: { dimension: { padding: "16px" } },
|
|
150
|
+
inspectorSections: [
|
|
151
|
+
{ title: "Content", controlNames: ["title", "href"] },
|
|
152
|
+
{ title: "Spacing", styleIds: ["dimension.padding", "dimension.margin"] },
|
|
153
|
+
],
|
|
154
|
+
renderEmail: ({ controls, email }) => email.button({
|
|
155
|
+
href: controls.href,
|
|
156
|
+
label: controls.title,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Parameters: BlockPluginOptions
|
|
162
|
+
|
|
163
|
+
| Property | Required | Meaning and purpose |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `type` | Yes | Stable unique type. Use an application namespace, such as my-app.banner. Do not casually change persisted types. |
|
|
166
|
+
| `title` | Yes | Library and inspector name. type/title cannot be blank. |
|
|
167
|
+
| `icon` | No | A string or ReactNode icon. |
|
|
168
|
+
| `description` | No | Block description. |
|
|
169
|
+
| `override` | No | Defaults to false; allows replacing a built-in with the same type during registration. |
|
|
170
|
+
| `controls` | No | ControlSchema object defining content controls in declaration order, with inferred value types. Defaults to an empty object. |
|
|
171
|
+
| `containerStyles` | No | Enables spacing and appearance by default. false disables both; `{ spacing?: boolean; appearance?: boolean }` controls them separately. |
|
|
172
|
+
| `styleControls` | No | StyleControlConfig[] adding or replacing style controls with matching IDs. |
|
|
173
|
+
| `inspectorSections` | No | BlockInspectorSection[] ordering content and style controls. Omit for automatic grouping. |
|
|
174
|
+
| `defaultStyle` | No | EmailBlockStyle providing defaults for enabled styles only; takes precedence over a style control's defaultValue. |
|
|
175
|
+
| `defaultData` | No | Template preview sample data, cloned into newly created nodes. Separate from control defaults. |
|
|
176
|
+
| `Canvas` | No | Custom React canvas. Otherwise uses the default HTML canvas or a single rich-text editing region. |
|
|
177
|
+
| `Inspector` | No | Custom React content inspector. Replaces default content editing while keeping style panels. |
|
|
178
|
+
| `renderEmail` | Yes | Synchronous function returning an HTML string or EmailMarkup containing the block's inner content. |
|
|
179
|
+
|
|
180
|
+
Default container controls include padding, margin, background color, border color/width/style, and corner radius. They exclude fixed height and typography. `containerStyles: false` disables automatic controls; explicitly supplied styleControls can still enable individual capabilities.
|
|
181
|
+
|
|
182
|
+
When inspectorSections is specified, only listed default content and style controls appear. Omitted controls remain in the schema but are not appended automatically.
|
|
183
|
+
|
|
184
|
+
### Return value: BlockPlugin
|
|
185
|
+
|
|
186
|
+
Pass the result directly to plugins rather than constructing a runtime definition manually.
|
|
187
|
+
|
|
188
|
+
| Returned property | Purpose |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `type/title/icon/description/override` | Identification, display, and registration metadata. |
|
|
191
|
+
| `schema` | Frozen control declaration object for lookup and type inference. |
|
|
192
|
+
| `controls` | Normalized control definition array, with a name on each entry. These are definitions; a node's controls contains values. |
|
|
193
|
+
| `readControls(source)` | Normalizes values, fills missing defaults, and preserves unknown business properties. |
|
|
194
|
+
| `styleControls/inspectorSections` | Merged style capabilities and inspector layout. |
|
|
195
|
+
| `defaultStyle/defaultData` | Default styles and template sample data. |
|
|
196
|
+
| `inlineControl` | Rich-text control edited by the default canvas; unused with a custom Canvas. |
|
|
197
|
+
| `canvas/inspector` | Adapted React components corresponding to the capitalized Canvas/Inspector declaration options. |
|
|
198
|
+
| `renderEmail(context)` | Wrapped renderer performing control validation in email mode and converting output to a string. |
|
|
199
|
+
|
|
200
|
+
### renderEmail context: PluginRenderContext
|
|
201
|
+
|
|
202
|
+
| Property | Meaning and purpose |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `block` | Complete node snapshot, including ID, type, controls, styles, and template sample data. |
|
|
205
|
+
| `controls` | Normalized values inferred from the control declarations. |
|
|
206
|
+
| `style` | Enabled block styles. |
|
|
207
|
+
| `body` | Email body layout, appearance, and default typography. |
|
|
208
|
+
| `mode` | canvas or email. Preview resolves templates after email rendering; it is not a third mode. |
|
|
209
|
+
| `email` | Email HTML helpers described below. |
|
|
210
|
+
|
|
211
|
+
Render synchronously without side effects. Fetch business data in the application or editing components, not during export. Return inner HTML only: shared renderers provide outer tables, container spacing, and the full document. Custom Canvas DOM is not exported; preview and export always use renderEmail().
|
|
212
|
+
|
|
213
|
+
## 3. EmailControls
|
|
214
|
+
|
|
215
|
+
### Example: text, options, and lists
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const productControls = {
|
|
219
|
+
name: EmailControls.text({ label: "Name", defaultValue: "Mug" }),
|
|
220
|
+
price: EmailControls.number({ label: "Price", min: 0, step: 0.01 }),
|
|
221
|
+
visible: EmailControls.boolean({ label: "Visible", defaultValue: true }),
|
|
222
|
+
variant: EmailControls.select({
|
|
223
|
+
label: "Variant",
|
|
224
|
+
defaultValue: "small",
|
|
225
|
+
options: [{ label: "Small", value: "small" }, { label: "Large", value: "large" }],
|
|
226
|
+
}),
|
|
227
|
+
terms: EmailControls.list({
|
|
228
|
+
label: "Terms",
|
|
229
|
+
controls: {
|
|
230
|
+
text: EmailControls.text({ label: "Term", defaultValue: "One use per customer" }),
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
EmailControls is a collection of control factories using camelCase method names. Each call creates a declaration combining editing UI, value type, defaults, and validation.
|
|
237
|
+
|
|
238
|
+
### Methods
|
|
239
|
+
|
|
240
|
+
Each method takes an options object and returns ControlDefinition<T>.
|
|
241
|
+
|
|
242
|
+
| Method | Value type | Default when omitted | Default editing UI |
|
|
243
|
+
| --- | --- | --- | --- |
|
|
244
|
+
| `text(options)` | string | `""` | Single-line input with variable insertion. |
|
|
245
|
+
| `textarea(options)` | string | `""` | Multiline input with variable insertion. |
|
|
246
|
+
| `richText(options)` | string | `"<p></p>"` | Rich-text HTML inheriting body typography. |
|
|
247
|
+
| `html(options)` | string | `""` | HTML source editor. |
|
|
248
|
+
| `url(options)` | string | `""` | URL input, protocol validation, variable insertion. |
|
|
249
|
+
| `image(options)` | string | `""` | Image URL, preview, and optional asset picker. |
|
|
250
|
+
| `number(options)` | number | 0 | Numeric input. |
|
|
251
|
+
| `color(options)` | string | `""` | Color picker. |
|
|
252
|
+
| `select(options)` | string | `""` | Select menu with a clearable empty option, labeled by placeholder or None by default. |
|
|
253
|
+
| `boolean(options)` | boolean | false | Checkbox. |
|
|
254
|
+
| `list(options)` | Array of child value objects | [] | Add, remove, move up/down, and nest items. Requires controls. |
|
|
255
|
+
| `custom(options)` | read() return type | Explicitly required | Custom React component. Requires defaultValue/read/component. |
|
|
256
|
+
|
|
257
|
+
### Parameters: ControlOptions
|
|
258
|
+
|
|
259
|
+
| Option | Type | Purpose |
|
|
260
|
+
| --- | --- | --- |
|
|
261
|
+
| `label` | string, required | Control label. |
|
|
262
|
+
| `defaultValue` | T | Default for new nodes and missing values. Mutable data is independently cloned. |
|
|
263
|
+
| `placeholder` | string | Placeholder for controls that support it. |
|
|
264
|
+
| `options` | `{ label: string; value: string }[]` | Select choices. |
|
|
265
|
+
| `rows` | number | Textarea/HTML editor row count; textarea defaults to 4. |
|
|
266
|
+
| `min/max` | number | Numeric range or list item count. |
|
|
267
|
+
| `step` | number | Numeric input step, not a separate validation constraint. |
|
|
268
|
+
| `unit` | string | Unit hint in the input label; does not change values or append CSS units. |
|
|
269
|
+
| `profile` | `"text" \| "heading" \| "button"` | Rich-text editing capabilities. |
|
|
270
|
+
| `inline` | boolean | Edit richText directly in the default canvas. Use Canvas for composite layouts. |
|
|
271
|
+
| `component` | `ComponentType<ControlComponentProps<T>>` | Replaces this control's default React editor. |
|
|
272
|
+
| `validate` | `(value: T) => string \| undefined` | Receives a normalized value; return a nonempty error or undefined. |
|
|
273
|
+
|
|
274
|
+
list additionally requires `controls: ControlSchema` for each row. custom requires a pure `read: (value: unknown) => T` to parse unknown persisted values. Values must support JSON persistence; do not store React nodes, functions, or editor instances.
|
|
275
|
+
|
|
276
|
+
### Defaults, reading, and validation
|
|
277
|
+
|
|
278
|
+
- Defaults apply only to undefined, preserving empty strings, 0, and false. String controls convert null to an empty string. Invalid numbers become NaN and fail validation. Boolean controls accept actual booleans and normalize other values to false. Non-array lists become [].
|
|
279
|
+
- Values are read once per context and reused by rendering, components, and validation. read() should be pure. Undeclared business IDs and metadata in list rows are preserved.
|
|
280
|
+
- Numbers must be finite and within min/max. Nonempty selects must match options. URLs/images validate protocols. List min/max limits item count; child controls are validated recursively.
|
|
281
|
+
- Empty URLs and selects are optional unless a business validate() requires them. Inspectors show errors; drafts can retain invalid values, but email export refuses them. Preview also validates through email rendering.
|
|
282
|
+
|
|
283
|
+
### Return value: ControlDefinition
|
|
284
|
+
|
|
285
|
+
| Property | Purpose |
|
|
286
|
+
| --- | --- |
|
|
287
|
+
| `kind` | Control type, such as text, richText, or list. |
|
|
288
|
+
| `defaultValue` | Resolved default. |
|
|
289
|
+
| `read(value)` | Normalizes an unknown value into the control's value type. |
|
|
290
|
+
| `validateValue(value)` | Runs built-in and business constraints on normalized values without reading again. |
|
|
291
|
+
| `renderControl(props)` | Optional custom rendering entry generated from component. |
|
|
292
|
+
| `itemControls` | Optional child schema for list controls. |
|
|
293
|
+
| Other metadata | Retains label, placeholder, options, profile, and related configuration. |
|
|
294
|
+
|
|
295
|
+
### Example: a custom value type
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
import { TextInput, type ControlComponentProps } from "@standhigher/react-email-editor";
|
|
299
|
+
|
|
300
|
+
function TagInput({ value, onChange, label }: ControlComponentProps<string[]>) {
|
|
301
|
+
return <label>{label}<TextInput value={value.join(", ")} onChange={(event) => onChange(event.target.value.split(",").map((tag) => tag.trim()))} /></label>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const tags = EmailControls.custom<string[]>({
|
|
305
|
+
label: "Tags",
|
|
306
|
+
defaultValue: [],
|
|
307
|
+
read: (value) => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [],
|
|
308
|
+
component: TagInput,
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## 4. Custom React editing components
|
|
313
|
+
|
|
314
|
+
### A single control: component
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { TextInput, type ControlComponentProps } from "@standhigher/react-email-editor";
|
|
318
|
+
|
|
319
|
+
function CouponInput({ value, onChange, label }: ControlComponentProps<string>) {
|
|
320
|
+
return <label>{label}<TextInput value={value} onChange={(event) => onChange(event.target.value.toUpperCase())} /></label>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const code = EmailControls.text({ label: "Code", component: CouponInput });
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Prop | Purpose |
|
|
327
|
+
| --- | --- |
|
|
328
|
+
| `value` | Current control value. |
|
|
329
|
+
| `onChange(value)` | Submit a new value. |
|
|
330
|
+
| `label` | Control label. |
|
|
331
|
+
| `body` | Body settings. |
|
|
332
|
+
| `variables` | Available template variables. |
|
|
333
|
+
|
|
334
|
+
Components receive independent value, body, and variables snapshots and can use hooks. Business async interactions handle loading, error/retry, empty states, and unmount cancellation. The shared image picker already handles these states: null cancels, thrown errors allow retry, and unmount aborts signal and ignores stale results. Async list updates merge into the latest data by stable row identity, preserving intervening edits and reordering; deleted rows no longer receive results.
|
|
335
|
+
|
|
336
|
+
### Canvas / Inspector
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import {
|
|
340
|
+
ControlEditor, EditableRichText,
|
|
341
|
+
type ControlValues, type PluginEditorProps,
|
|
342
|
+
} from "@standhigher/react-email-editor";
|
|
343
|
+
|
|
344
|
+
const cardControls = {
|
|
345
|
+
title: EmailControls.text({ label: "Title", defaultValue: "New arrivals" }),
|
|
346
|
+
description: EmailControls.richText({ label: "Description" }),
|
|
347
|
+
footer: EmailControls.richText({ label: "Footer" }),
|
|
348
|
+
};
|
|
349
|
+
type CardValues = ControlValues<typeof cardControls>;
|
|
350
|
+
|
|
351
|
+
function CardCanvas({ controls }: PluginEditorProps<CardValues>) {
|
|
352
|
+
return <div>
|
|
353
|
+
<strong>{controls.title}</strong>
|
|
354
|
+
<EditableRichText control="description" />
|
|
355
|
+
<EditableRichText control="footer" />
|
|
356
|
+
</div>;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function CardInspector() {
|
|
360
|
+
return <>
|
|
361
|
+
<ControlEditor control="title" />
|
|
362
|
+
<ControlEditor control="description" />
|
|
363
|
+
<ControlEditor control="footer" />
|
|
364
|
+
</>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const card = defineBlock({
|
|
368
|
+
type: "my-app.card", title: "Card", controls: cardControls,
|
|
369
|
+
Canvas: CardCanvas, Inspector: CardInspector,
|
|
370
|
+
renderEmail: ({ controls, email }) => email.html`
|
|
371
|
+
<h2>${controls.title}</h2>
|
|
372
|
+
${email.richText(controls.description)}
|
|
373
|
+
${email.richText(controls.footer)}
|
|
374
|
+
`,
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Canvas/Inspector receive PluginEditorProps<ControlValues<Schema>>:
|
|
379
|
+
|
|
380
|
+
| Prop | Meaning and purpose |
|
|
381
|
+
| --- | --- |
|
|
382
|
+
| `block/controls/style/body` | Snapshots of the node, normalized values, block styles, and body. Submit changes through update methods. |
|
|
383
|
+
| `variables` | Template variables. |
|
|
384
|
+
| `selected` | Whether the block is selected; true in Inspector. |
|
|
385
|
+
| `renderedHtml` | Resolved inner HTML in Canvas; an empty string in Inspector. |
|
|
386
|
+
| `select()` | Requests block selection; unnecessary in Inspector. |
|
|
387
|
+
| `setControl(name, value)` | Type-safe update to one value. |
|
|
388
|
+
| `setControls(patch)` | Shallow-merges multiple values with one document update and one onChange. Replace nested objects in full. |
|
|
389
|
+
| `setStyle(id, value)` | Updates a style path, such as setStyle("dimension.padding", "12px"). |
|
|
390
|
+
|
|
391
|
+
ControlEditor takes only a required `control: string` and reuses declared editing UI, validation, and resource adapters. EditableRichText takes a required control and optional profile/placeholder. It connects value updates, selection, toolbars, variables, and body typography, and requires a richText control. Both are used inside a block Canvas/Inspector only.
|
|
392
|
+
|
|
393
|
+
A single richText({ inline: true }) replaces the entire default canvas with that editing region and removes the control from the default inspector. It does not insert an editor into part of the renderEmail layout. Multiple inline regions require Canvas or definition throws. With Canvas, that component composes all editing regions.
|
|
394
|
+
|
|
395
|
+
## 5. Style controls and inspector sections
|
|
396
|
+
|
|
397
|
+
### Example
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import { createDimensionControls } from "@standhigher/react-email-editor";
|
|
401
|
+
|
|
402
|
+
const spacing = createDimensionControls({ padding: { defaultValue: "12px" } });
|
|
403
|
+
// Pass these options to defineBlock:
|
|
404
|
+
const layoutBlock = defineBlock({
|
|
405
|
+
type: "my-app.layout",
|
|
406
|
+
title: "Layout",
|
|
407
|
+
controls: { title: EmailControls.text({ label: "Title" }) },
|
|
408
|
+
containerStyles: false,
|
|
409
|
+
styleControls: [spacing.padding, spacing.margin],
|
|
410
|
+
inspectorSections: [{
|
|
411
|
+
title: "Content and spacing",
|
|
412
|
+
controlNames: ["title"],
|
|
413
|
+
styleIds: ["dimension.padding", "dimension.margin"],
|
|
414
|
+
}],
|
|
415
|
+
renderEmail: ({ controls, email }) => email.html`<p>${controls.title}</p>`,
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
createDimensionControls(), createDecorationControls(), and createTypographyControls() return style configuration objects indexed by capability names. Pass overrides keyed by name, then select entries for styleControls.
|
|
420
|
+
|
|
421
|
+
| StyleControlConfig property | Purpose |
|
|
422
|
+
| --- | --- |
|
|
423
|
+
| `id` | Style path, such as dimension.padding, typography.fontSize, or element.width. |
|
|
424
|
+
| `group` | general/dimension/typography/decorations for default inspector grouping. |
|
|
425
|
+
| `label` | Control label. |
|
|
426
|
+
| `control` | text/color/alignment/select/number/range/spacing/corners. |
|
|
427
|
+
| `options` | Select choices. |
|
|
428
|
+
| `unit/units` | Unit suffix or available units; use email-compatible CSS units. |
|
|
429
|
+
| `min/max/step` | Numeric and range parameters. |
|
|
430
|
+
| `defaultValue` | Default style string, below defaultStyle in precedence. |
|
|
431
|
+
| `placeholder` | Input hint. |
|
|
432
|
+
|
|
433
|
+
id/group/label/control are required; other properties are optional.
|
|
434
|
+
|
|
435
|
+
| BlockInspectorSection property | Purpose |
|
|
436
|
+
| --- | --- |
|
|
437
|
+
| `title` | Required section title. |
|
|
438
|
+
| `controlNames` | Content control names in display order. |
|
|
439
|
+
| `styleIds` | Style IDs in display order. |
|
|
440
|
+
| `advancedStyleIds` | Style IDs placed in a collapsed section. |
|
|
441
|
+
|
|
442
|
+
Body settings live in body; container styles in style.dimension/decorations; button/image element styles in style.element; explicit rich-text formatting in HTML strings inside controls. Only enabled block styles are read. Apply custom element styles explicitly in renderEmail.
|
|
443
|
+
|
|
444
|
+
## 6. Email rendering helpers: email
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
renderEmail: ({ controls, email }) => email.html`
|
|
448
|
+
<p>${controls.title}</p>
|
|
449
|
+
${email.richText(controls.description)}
|
|
450
|
+
${email.button({ href: controls.url, label: "Learn more" })}
|
|
451
|
+
`
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
| Helper | Purpose |
|
|
455
|
+
| --- | --- |
|
|
456
|
+
| `email.html` tagged template | Escapes ordinary interpolations, supports EmailMarkup fragments and arrays, and preserves Go template tokens. |
|
|
457
|
+
| `email.raw(html)` | Explicit raw HTML insertion without sanitization. |
|
|
458
|
+
| `email.richText(html)` | Preserves explicit formatting and adds body defaults for paragraphs, lists, and links. |
|
|
459
|
+
| `email.table(rows)` | Generates email tables from a two-dimensional EmailMarkup array. |
|
|
460
|
+
| `email.image({ src, alt?, width? })` | Validates the image protocol and normalizes width, defaulting to 100%. |
|
|
461
|
+
| `email.button({ href, label })` | Email table button; validates protocol and omits links for empty or invalid addresses. |
|
|
462
|
+
| `email.url(value, image?)` | Returns allowed URLs or an empty string. image defaults to false. |
|
|
463
|
+
|
|
464
|
+
Links allow http/https/mailto/tel, anchors, or templates; images allow http/https/cid or templates. When assembling attributes, validate the protocol with email.url() and escape attribute values. Plugins returning plain strings are responsible for escaping. Raw HTML, rich text, and template preview are not security sandboxes for arbitrary untrusted content.
|
|
465
|
+
|
|
466
|
+
## 7. Documents, errors, and public types
|
|
467
|
+
|
|
468
|
+
EmailBlockNode contains required id, type, controls and optional style/defaultData. EmailDocument contains version, body, blocks, and optional subject. Register matching plugins after JSON reload; JSON does not store components or renderers.
|
|
469
|
+
|
|
470
|
+
The current document version is **4**. The entire API uses controls, with no old API aliases, old-data compatibility, or automatic migration. Unsupported versions throw. Create new data with createEmailDocument() and config.createBlock(); do not merely change an old JSON version number.
|
|
471
|
+
|
|
472
|
+
Missing plugins produce a canvas placeholder and preserve the node; email export throws. Canvas, Inspector, and canvas rendering errors are isolated by block with retry. Email rendering errors identify the block type, ID, and reason. getData()/getDocument() and onChange/onBodyChange arguments provide independent snapshots.
|
|
473
|
+
|
|
474
|
+
| Public type | Purpose |
|
|
475
|
+
| --- | --- |
|
|
476
|
+
| `ControlKind/ControlDefinition<T>/ControlOptions<T>` | Control kinds, factory results, and options. |
|
|
477
|
+
| `ControlSchema/ControlValues<Schema>` | Control schema and inferred value object. |
|
|
478
|
+
| `ControlComponentProps<T>` | Single-control React editor props. |
|
|
479
|
+
| `BlockPlugin/BlockPluginOptions` | Block definition and declaration options. |
|
|
480
|
+
| `PluginEditorProps/PluginRenderContext` | Editing component props and rendering context. |
|
|
481
|
+
| `EmailEditorConfig/UseEmailEditorConfigOptions` | Registration configuration and options. |
|
|
482
|
+
| `ImagePickerContext/ValidationIssue` | Image picker context and validation results. |
|
|
483
|
+
| `ControlOption/StyleControlConfig/BlockInspectorSection` | Options, style controls, and inspector sections. |
|
|
484
|
+
| `ControlEditorProps/EditableRichTextProps` | Reusable control and rich-text component props. |
|
|
485
|
+
| `EmailMarkup/EmailTools` | Email fragments and rendering helpers. |
|
|
486
|
+
|
|
487
|
+
See the [API reference](./api.md) for editor Hooks, instance methods, body, and variables, and [advanced examples](./advanced-examples.md) for business integration. Runnable examples: [coupon](../playground/plugins/coupon.ts), [product card](../playground/plugins/product.ts), and [shared configuration](../playground/plugins/config.ts).
|