Native form
Edit content, submit to inspect the field, then reset to its default.
Make room for your content
An article, a product story, or your next announcement. Familiar tools for the work you do every day.
Loads on demand · Content stays in this page
What to verify
Edit content, submit to inspect the field, then reset to its default. Read HTML displays canonical content. Recreate demo destroys and remounts the instances. Upload and save send no editing content; mock uploads return the bundled image.
Complete integration code
Download all example sources. Extract, run pnpm install and pnpm dev, then open basic.html or another example page.
The demo runs this exact code. Use a Vite TypeScript page with the same file structure; the alias pins the npm release.
pnpm add soeditor-release@npm:@soeditor/editor@1.2.1 @soeditor/file-manager@1.2.1 @soeditor/adapter-sofinder@1.2.1 @soeditor/presets@1.2.1import { createClassicEditor } from 'soeditor-release/cms';
import {
options,
instance,
button,
label,
type DemoContext,
} from './shared.js';
export async function mount(ctx: DemoContext) {
const editor = await createClassicEditor(ctx.host, options(ctx));
ctx.form.addEventListener(
'submit',
(event) => {
event.preventDefault();
ctx.output.textContent = String(
new FormData(ctx.form).get('content'),
);
ctx.status.textContent = label(
ctx,
'表单已读取,未发送网络请求',
'Form read locally; no request was sent',
);
},
{ signal: ctx.signal },
);
button(ctx, '提交表单', 'Submit form', () => ctx.form.requestSubmit());
button(ctx, '重置表单', 'Reset form', () => ctx.form.reset());
return instance(editor);
}Shared context and lifecycle
import type {
ClassicEditor,
CreateClassicEditorOptions,
} from 'soeditor-release/cms';
export type Locale = 'zh-CN' | 'en';
export interface DemoContext {
locale: Locale;
host: HTMLTextAreaElement;
form: HTMLFormElement;
controls: HTMLElement;
output: HTMLElement;
status: HTMLElement;
signal: AbortSignal;
}
export interface DemoInstance {
editors: ClassicEditor[];
destroy(): Promise<void>;
}
export function options(ctx: DemoContext): CreateClassicEditorOptions {
return {
locale: ctx.locale,
minHeight: 280,
onChange: () => {
ctx.status.textContent = label(
ctx,
'内容已修改',
'Content changed',
);
},
};
}
export function label(ctx: DemoContext, zh: string, en: string): string {
return ctx.locale === 'zh-CN' ? zh : en;
}
export function button(
ctx: DemoContext,
zh: string,
en: string,
action: () => void | Promise<unknown>,
): HTMLButtonElement {
const el = document.createElement('button');
el.type = 'button';
el.textContent = label(ctx, zh, en);
el.addEventListener(
'click',
() => {
el.disabled = true;
Promise.resolve()
.then(action)
.catch((error: unknown) => {
ctx.status.textContent =
error instanceof Error
? error.message
: label(ctx, '操作失败', 'Action failed');
})
.finally(() => {
el.disabled = false;
});
},
{ signal: ctx.signal },
);
ctx.controls.append(el);
return el;
}
export function instance(...editors: ClassicEditor[]): DemoInstance {
return {
editors,
async destroy() {
await Promise.all(editors.map((editor) => editor.destroy()));
},
};
}
export function delay(signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(new DOMException('Aborted', 'AbortError'));
return;
}
const abort = () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
};
const timer = setTimeout(() => {
signal.removeEventListener('abort', abort);
resolve();
}, 500);
signal.addEventListener('abort', abort, { once: true });
});
}Page mounting and recreation
import 'soeditor-release/cms/styles.css';
import './style.css';
import {
button,
label,
type DemoContext,
type DemoInstance,
} from './shared.js';
const loaders = {
video: () => import('./video.js'),
basic: () => import('./basic.js'),
form: () => import('./form.js'),
source: () => import('./source.js'),
assets: () => import('./assets.js'),
save: () => import('./save.js'),
multiple: () => import('./multiple.js'),
};
const name = document.documentElement.dataset.example ?? 'basic';
function isExample(value: string): value is keyof typeof loaders {
return value in loaders;
}
if (!isExample(name)) throw new Error('Unknown example');
const loadExample = loaders[name];
const locale =
new URLSearchParams(location.search).get('lang') === 'en' ? 'en' : 'zh-CN';
document.documentElement.lang = locale;
const form = document.createElement('form');
const host = document.createElement('textarea');
host.name = 'content';
host.setAttribute(
'aria-label',
locale === 'en' ? 'Article content' : '文章内容',
);
const initial =
locale === 'en'
? '<h2>A place for your next story</h2><p>Edit this article with SoEditor.</p><table><tbody><tr><th>Feature</th><th>Use</th></tr><tr><td>HTML</td><td>CMS content</td></tr></tbody></table>'
: '<h2>从这里开始你的下一篇文章</h2><p>使用 SoEditor 编辑文章内容。</p><table><tbody><tr><th>功能</th><th>用途</th></tr><tr><td>HTML</td><td>CMS 内容</td></tr></tbody></table>';
host.defaultValue = initial;
form.append(host);
const controls = document.createElement('div');
controls.className = 'controls';
const status = document.createElement('p');
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'HTML';
const output = document.createElement('pre');
details.append(summary, output);
const note = document.createElement('p');
note.className = 'notice';
note.textContent =
locale === 'en'
? 'Local demo. Upload and save are simulated; no content is sent to a server.'
: '本地演示:上传和保存均为模拟,编辑内容不会发送到服务器。';
document.body.append(note, controls, status, form, details);
let owned: DemoInstance | undefined;
let disposed = false;
let controller = new AbortController();
let task: Promise<void> = Promise.resolve();
async function mount() {
controller = new AbortController();
const ctx: DemoContext = {
locale,
host,
form,
controls,
output,
status,
signal: controller.signal,
};
status.textContent = label(ctx, '正在加载…', 'Loading…');
const module = await loadExample();
if (disposed) return;
const mounted = await module.mount(ctx);
owned = mounted;
if (disposed) {
await mounted.destroy();
owned = undefined;
return;
}
button(ctx, '读取 HTML', 'Read HTML', () => {
output.textContent =
owned?.editors.map((editor) => editor.getData()).join('\n\n') ?? '';
details.open = true;
});
button(ctx, '重建示例', 'Recreate demo', () => {
task = task.then(async () => {
controller.abort();
await owned?.destroy();
owned = undefined;
controls.replaceChildren();
host.value = initial;
output.textContent = '';
await mount();
});
return task;
});
status.textContent = label(ctx, '就绪', 'Ready');
document.body.dataset.ready = 'true';
}
window.addEventListener(
'pagehide',
() => {
disposed = true;
controller.abort();
void owned?.destroy();
},
{ once: true },
);
task = mount().catch((error: unknown) => {
status.textContent =
error instanceof Error ? error.message : 'Failed to initialize';
});HTML / CSS
<!doctype html>
<html lang="en" data-example="form">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, nofollow" />
<title>SoEditor demo</title>
</head>
<body>
<script type="module" src="./runner.ts"></script>
</body>
</html>html {
color-scheme: light;
font:
15px/1.6 system-ui,
sans-serif;
color: #202b3d;
background: #fff;
}
body {
margin: 12px;
}
* {
box-sizing: border-box;
}
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.controls button,
.controls input {
max-width: 100%;
font: inherit;
}
.controls button {
border: 1px solid #b9c5d8;
border-radius: 6px;
background: #f7f9fc;
color: #203c68;
padding: 6px 12px;
cursor: pointer;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid #2459c4;
outline-offset: 2px;
}
button:disabled {
opacity: 0.6;
cursor: wait;
}
.notice {
color: #4b5870;
font-size: 13px;
}
[role='status'] {
min-height: 24px;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
background: #f5f7fa;
padding: 12px;
}
textarea {
width: 100%;
min-height: 200px;
}
form {
min-width: 0;
}Configuration and troubleshooting
Each instance receives locale at creation. Closing removes the iframe; starting again restores initial content. If styles or Source are missing, check that all build assets were deployed.