diff --git a/docs/api/form/submit-form.md b/docs/api/form/submit-form.md index 5f5a23a0..73cd3648 100644 --- a/docs/api/form/submit-form.md +++ b/docs/api/form/submit-form.md @@ -18,7 +18,7 @@ function submitForm(options: SubmitFormOptions): Promise; ## 参数 -`options` 与 `MForm` 组件的 props 基本对齐,额外提供了 `native`、`appContext`、`timeout` 三个参数。 +`options` 与 `MForm` 组件的 props 基本对齐,额外提供了 `native`、`returnChangeRecords`、`appContext`、`timeout` 等参数。 | 名称 | 类型 | 默认值 | 说明 | | ---------------------- | ------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- | @@ -39,17 +39,22 @@ function submitForm(options: SubmitFormOptions): Promise; | `preventSubmitDefault` | `boolean` | — | 是否阻止表单原生 submit | | `extendState` | `(state: FormState) => Record \| Promise>` | — | 扩展 `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` resolve 当前表单值(`native` 决定是否克隆) +- `校验通过` — `Promise` resolve 当前表单值(`native` 决定是否克隆);当 `returnChangeRecords` 为 `true` 时,resolve `{ values, changeRecords }` - `校验失败` — `Promise` reject 一个 `Error`,`message` 中包含逐条字段错误信息(格式 `${text} -> ${message}`,多条用 `
` 分隔) - `初始化超时` — `Promise` 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} +::: diff --git a/packages/form/src/submitForm.ts b/packages/form/src/submitForm.ts index 77cde7cd..5ace5485 100644 --- a/packages/form/src/submitForm.ts +++ b/packages/form/src/submitForm.ts @@ -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 | Promise>; /** 透传给 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 => { - 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 => { 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 { diff --git a/packages/form/tests/unit/submitForm.spec.ts b/packages/form/tests/unit/submitForm.spec.ts index e056a30c..638d9f07 100644 --- a/packages/form/tests/unit/submitForm.spec.ts +++ b/packages/form/tests/unit/submitForm.spec.ts @@ -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({