tmagic-editor/docs/api/form/submit-form.md
roymondchen 638c3e9f3c feat(form): 新增 submitForm 命令式提交函数
提供脱离组件树以函数方式完成一次表单校验/提交的能力,类似 ElMessage 用法:
传入 config/initValues 等 props 后内部临时挂载 Form 实例,
初始化完成即调用 submitForm,校验通过 resolve 表单值、失败 reject,
最后自动卸载,并支持 appContext 继承、timeout 与 native 透传。

同步补充单元测试、API 文档及侧边栏入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 15:55:28 +08:00

193 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# submitForm 函数
以命令式方式调用 `MForm` 组件完成一次表单校验/提交,类似 `ElMessage` 的用法。
调用时函数内部会临时挂载一个不可见的 `MForm` 实例,把入参作为 props 透传给它,等待初始化完成后调用其 `submitForm` 方法。校验通过则 `resolve` 表单值,校验失败则 `reject` 错误信息,最后自动卸载实例并清理 DOM。
适用于一些没有合适的容器、但又需要复用 `MForm` 校验逻辑的场景,例如:
- 通过快捷菜单/命令面板触发一次性表单
- 在脚本/服务层完成一次表单值校验后再发请求
-`config` 配置当作"可执行的校验规则"使用
## 签名
```ts
function submitForm(options: SubmitFormOptions): Promise<any>;
```
## 参数
`options``MForm` 组件的 props 基本对齐,额外提供了 `native``appContext``timeout` 三个参数。
| 名称 | 类型 | 默认值 | 说明 |
| ---------------------- | ------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- |
| `config` | `FormConfig` | — | 必填,表单配置 |
| `initValues` | `Record<string, any>` | `{}` | 表单初始值 |
| `lastValues` | `Record<string, any>` | `{}` | 需对比的值(开启对比模式时传入) |
| `isCompare` | `boolean` | `false` | 是否开启对比模式 |
| `parentValues` | `Record<string, any>` | `{}` | 父级 values透传给字段的回调 |
| `labelWidth` | `string` | `'200px'` | label 宽度 |
| `disabled` | `boolean` | `false` | 是否禁用 |
| `height` | `string` | `'auto'` | 表单高度 |
| `stepActive` | `string \| number` | `1` | 步骤表单当前激活步骤 |
| `size` | `'small' \| 'default' \| 'large'` | — | 组件尺寸 |
| `inline` | `boolean` | `false` | 是否行内表单 |
| `labelPosition` | `string` | `'right'` | label 对齐方式 |
| `keyProp` | `string` | `'__key'` | 配置项的唯一 key |
| `popperClass` | `string` | — | 弹层 className |
| `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit |
| `extendState` | `(state: FormState) => Record<string, any> \| Promise<Record<string, any>>` | — | 扩展 `formState` |
| `native` | `boolean` | `false` | 透传给 `Form.submitForm``true` 时返回内部响应式 `values`,否则返回 `cloneDeep(toRaw(values))` |
| `appContext` | `AppContext \| null` | `null` | 父级 Vue 应用上下文。需要继承全局组件、指令、provide 等时传入,常通过 `app._context``getCurrentInstance()?.appContext` 获取 |
| `timeout` | `number` | `10000` | 等待表单初始化的最长时间(毫秒)。超时将以错误 reject。设为 `<= 0` 时关闭超时兜底 |
## 返回值
- `校验通过``Promise<any>` resolve 当前表单值(`native` 决定是否克隆)
- `校验失败``Promise<any>` reject 一个 `Error``message` 中包含逐条字段错误信息(格式 `${text} -> ${message}`,多条用 `<br>` 分隔)
- `初始化超时``Promise<any>` reject `Error('submitForm timeout after ${timeout}ms: form is not initialized.')`
无论成功或失败,函数都会在最后自动 `unmount` 内部 app 并移除挂载用的 DOM 容器,无需调用方手动清理。
## 基础用法
```ts
import { submitForm } from '@tmagic/form';
try {
const values = await submitForm({
config: [
{
type: 'text',
name: 'username',
text: '用户名',
rules: [{ required: true, message: '请输入用户名' }],
},
],
initValues: { username: '' },
});
console.log('提交成功', values);
} catch (e) {
console.error('校验失败', e);
}
```
## 在组件中继承父级应用上下文
`MForm` 内部使用 `@tmagic/design` 的组件(背后可能是 `element-plus``tdesign`),需要宿主应用先完成相应的 `app.use(...)` 安装。在 `submitForm` 这种脱离常规组件树的命令式调用中,可通过 `appContext` 把父级应用上下文带过去:
```vue
<script setup lang="ts">
import { getCurrentInstance } from 'vue';
import { submitForm } from '@tmagic/form';
const { appContext } = getCurrentInstance()!;
const onClick = async () => {
const values = await submitForm({
config: [{ type: 'text', name: 'text', text: '文本' }],
initValues: { text: 'hello' },
appContext,
});
console.log(values);
};
</script>
```
也可以在初始化 app 时把上下文缓存下来,再在任意位置复用:
```ts
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import MagicForm, { type SubmitFormOptions, submitForm as rawSubmitForm } from '@tmagic/form';
import App from './App.vue';
const app = createApp(App);
app.use(ElementPlus);
app.use(MagicForm);
app.mount('#app');
export const submitForm = (options: Omit<SubmitFormOptions, 'appContext'>) =>
rawSubmitForm({ ...options, appContext: app._context });
```
## 处理校验错误
校验失败时 reject 的 `Error.message` 已经把出错字段拼好,可以直接展示到用户:
```ts
import { tMagicMessage } from '@tmagic/design';
try {
const values = await submitForm({ config, initValues });
await save(values);
} catch (e: any) {
tMagicMessage.error({
dangerouslyUseHTMLString: true,
message: e.message,
});
}
```
## 运行环境
`submitForm` 内部依赖 `document` / `window` 来挂载临时 Vue 实例,因此**只能在浏览器或具备 DOM 环境的运行时中使用**。
| 环境 | 是否可用 | 说明 |
| ----------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| 浏览器 / Electron 渲染进程 / 浏览器扩展 | ✅ | 直接可用 |
| Vitest / Jest + `happy-dom` / `jsdom` | ✅ | 项目自身的单测就跑在这种环境下 |
| 纯 Node.js / Bun / Deno无 DOM polyfill | ❌ | 模块顶层就会读 `document`,会抛 `document is not defined` |
| Node.js + 手动注入 `happy-dom` / `jsdom` | ⚠️ | 可用,需要在 import `@tmagic/form` **之前**完成全局变量注入;校验行为不一定与浏览器完全一致 |
### 在 Node.js 中使用(需要先准备 DOM
下面是一个在 Node 脚本里调用 `submitForm` 的完整例子,使用 [`happy-dom`](https://github.com/capricorn86/happy-dom) 作为 DOM polyfill
```ts
// scripts/check-form.ts
import { Window } from 'happy-dom';
const window = new Window();
Object.assign(globalThis, {
window,
document: window.document,
navigator: window.navigator,
HTMLElement: window.HTMLElement,
});
// 注意DOM polyfill 必须先注入到 globalThis再用动态 import
// 加载业务模块,否则 @tmagic/design 等模块顶层执行时就会读 document
const { createApp } = await import('vue');
const ElementPlus = (await import('element-plus')).default;
const MagicForm = (await import('@tmagic/form')).default;
const { submitForm } = await import('@tmagic/form');
const parentApp = createApp({ render: () => null });
parentApp.use(ElementPlus);
parentApp.use(MagicForm);
const values = await submitForm({
config: [{ type: 'text', name: 'username', text: '用户名' }],
initValues: { username: 'foo' },
appContext: parentApp._context,
});
console.log(values);
```
::: warning 注意
- DOM polyfill 必须在 **import 业务模块之前** 注入到 `globalThis`,否则模块顶层执行时仍会失败
-`happy-dom` / `jsdom` 中,`element-plus` 的部分 `validate()` 行为不一定能 1:1 复现真实浏览器(例如某些场景下必填规则可能不触发),建议关键校验使用自定义 `validator` 函数确保稳定
- 如果只是想在 Node 端做一次纯校验,更稳妥的做法是直接复用 [`async-validator`](https://github.com/yiminghe/async-validator)element-plus 内部用的就是它),绕开整个 Vue 渲染层
:::
## 类型定义
::: details 查看 `SubmitFormOptions` 类型定义
<<< @/../packages/form/src/submitForm.ts#SubmitFormOptions{ts}
:::