feat(form): submitForm 支持返回 changeRecords

新增 returnChangeRecords 选项,开启后 resolve { values, changeRecords },
便于命令式调用时获取表单变更记录,并同步更新文档与单测。
This commit is contained in:
roymondchen 2026-06-02 16:43:07 +08:00
parent 1b66ab1b88
commit 12069e0937
3 changed files with 82 additions and 5 deletions

View File

@ -18,7 +18,7 @@ function submitForm(options: SubmitFormOptions): Promise<any>;
## 参数
`options``MForm` 组件的 props 基本对齐,额外提供了 `native``appContext`、`timeout` 三个参数。
`options``MForm` 组件的 props 基本对齐,额外提供了 `native``returnChangeRecords`、`appContext``timeout`参数。
| 名称 | 类型 | 默认值 | 说明 |
| ---------------------- | ------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- |
@ -39,17 +39,22 @@ function submitForm(options: SubmitFormOptions): Promise<any>;
| `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit |
| `extendState` | `(state: FormState) => Record<string, any> \| Promise<Record<string, any>>` | — | 扩展 `formState` |
| `native` | `boolean` | `false` | 透传给 `Form.submitForm``true` 时返回内部响应式 `values`,否则返回 `cloneDeep(toRaw(values))` |
| `returnChangeRecords` | `boolean` | `false` | `true` 时 resolve 结果为 `{ values, changeRecords }`,携带表单变更记录;否则仅 resolve `values` |
| `appContext` | `AppContext \| null` | `null` | 父级 Vue 应用上下文。需要继承全局组件、指令、provide 等时传入,常通过 `app._context``getCurrentInstance()?.appContext` 获取 |
| `timeout` | `number` | `10000` | 等待表单初始化的最长时间(毫秒)。超时将以错误 reject。设为 `<= 0` 时关闭超时兜底 |
## 返回值
- `校验通过``Promise<any>` resolve 当前表单值(`native` 决定是否克隆)
- `校验通过``Promise<any>` resolve 当前表单值(`native` 决定是否克隆);当 `returnChangeRecords``true`resolve `{ values, changeRecords }`
- `校验失败``Promise<any>` reject 一个 `Error``message` 中包含逐条字段错误信息(格式 `${text} -> ${message}`,多条用 `<br>` 分隔)
- `初始化超时``Promise<any>` reject `Error('submitForm timeout after ${timeout}ms: form is not initialized.')`
无论成功或失败,函数都会在最后自动 `unmount` 内部 app 并移除挂载用的 DOM 容器,无需调用方手动清理。
::: tip 关于 changeRecords
`changeRecords` 记录的是表单挂载后发生的字段变更(由各字段的 `change` 事件累积而来)。在 `submitForm` 这种命令式、无用户交互的场景下,通常为空数组;只有在 `extendState` 或字段联动等逻辑中触发了变更时才会有内容。`MForm` 内部的 `submitForm` 在校验通过后会清空变更记录,因此本函数会在调用前先对其做快照再返回。
:::
## 基础用法
```ts
@ -73,6 +78,23 @@ try {
}
```
## 同时获取变更记录changeRecords
设置 `returnChangeRecords: true`resolve 的结果会从单纯的 `values` 变为 `{ values, changeRecords }`
```ts
import { submitForm } from '@tmagic/form';
const { values, changeRecords } = await submitForm({
config: [{ type: 'text', name: 'username', text: '用户名' }],
initValues: { username: 'foo' },
returnChangeRecords: true,
});
console.log(values); // { username: 'foo' }
console.log(changeRecords); // ChangeRecord[]
```
## 在组件中继承父级应用上下文
`MForm` 内部使用 `@tmagic/design` 的组件(背后可能是 `element-plus``tdesign`),需要宿主应用先完成相应的 `app.use(...)` 安装。在 `submitForm` 这种脱离常规组件树的命令式调用中,可通过 `appContext` 把父级应用上下文带过去:
@ -190,3 +212,7 @@ console.log(values);
::: details 查看 `SubmitFormOptions` 类型定义
<<< @/../packages/form/src/submitForm.ts#SubmitFormOptions{ts}
:::
::: details 查看 `SubmitFormResult` 类型定义
<<< @/../packages/form/src/submitForm.ts#SubmitFormResult{ts}
:::

View File

@ -19,7 +19,7 @@
import { type AppContext, type Component, createApp, defineComponent, h, nextTick, ref, watch } from 'vue';
import Form from './Form.vue';
import type { FormConfig, FormState } from './schema';
import type { ChangeRecord, FormConfig, FormState } from './schema';
// #region SubmitFormOptions
/**
@ -48,6 +48,11 @@ export interface SubmitFormOptions {
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/** 透传给 Form.submitForm 的参数:是否直接返回原始响应式 values */
native?: boolean;
/**
* resolve changeRecords
* resolve `{ values, changeRecords }` resolve values
*/
returnChangeRecords?: boolean;
/**
* provide
* `app._context` `getCurrentInstance()?.appContext`
@ -58,6 +63,18 @@ export interface SubmitFormOptions {
}
// #endregion SubmitFormOptions
// #region SubmitFormResult
/**
* `returnChangeRecords` submitForm
*/
export interface SubmitFormResult {
/** 校验通过后的表单值 */
values: any;
/** 表单变更记录 */
changeRecords: ChangeRecord[];
}
// #endregion SubmitFormResult
/**
* Form.vue /
*
@ -78,10 +95,17 @@ export interface SubmitFormOptions {
* } catch (e) {
* console.error(e);
* }
*
* // 需要同时获取变更记录时:
* const { values, changeRecords } = await submitForm({
* config: [...],
* initValues: { name: 'foo' },
* returnChangeRecords: true,
* });
* ```
*/
export const submitForm = (options: SubmitFormOptions): Promise<any> => {
const { native, appContext, timeout = 10000, ...formProps } = options;
const { native, appContext, timeout = 10000, returnChangeRecords, ...formProps } = options;
return new Promise((resolve, reject) => {
const container = document.createElement('div');
@ -105,8 +129,10 @@ export const submitForm = (options: SubmitFormOptions): Promise<any> => {
try {
// 等待子组件FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
await nextTick();
// submitForm 校验通过后会清空 changeRecords需在调用前先做快照
const changeRecords: ChangeRecord[] = [...(formRef.value.changeRecords ?? [])];
const result = await formRef.value.submitForm(native);
resolve(result);
resolve(returnChangeRecords ? { values: result, changeRecords } : result);
} catch (err) {
reject(err);
} finally {

View File

@ -101,6 +101,31 @@ describe('submitForm', () => {
});
});
test('returnChangeRecords=true 时返回 { values, changeRecords }', async () => {
const result = await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
returnChangeRecords: true,
appContext,
});
expect(result).toHaveProperty('values');
expect(result).toHaveProperty('changeRecords');
expect(result.values).toEqual({ text: 'hello' });
expect(Array.isArray(result.changeRecords)).toBe(true);
});
test('未设置 returnChangeRecords 时仅返回 values不包裹', async () => {
const result = await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect(result).toEqual({ text: 'hello' });
expect(result).not.toHaveProperty('changeRecords');
});
test('多次连续调用不会相互干扰', async () => {
const [v1, v2] = await Promise.all([
submitForm({