tmagic-editor/packages/form/src/submitForm.ts
roymondchen 12069e0937 feat(form): submitForm 支持返回 changeRecords
新增 returnChangeRecords 选项,开启后 resolve { values, changeRecords },
便于命令式调用时获取表单变更记录,并同步更新文档与单测。
2026-06-02 16:43:07 +08:00

188 lines
5.8 KiB
TypeScript
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.

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type AppContext, type Component, createApp, defineComponent, h, nextTick, ref, watch } from 'vue';
import Form from './Form.vue';
import type { ChangeRecord, FormConfig, FormState } from './schema';
// #region SubmitFormOptions
/**
* submitForm 函数参数(与 Form.vue 组件 props 对齐)
*/
export interface SubmitFormOptions {
/** 表单配置 */
config: FormConfig;
/** 表单初始值 */
initValues?: Record<string, any>;
/** 需对比的值(开启对比模式时传入) */
lastValues?: Record<string, any>;
/** 是否开启对比模式 */
isCompare?: boolean;
parentValues?: Record<string, any>;
labelWidth?: string;
disabled?: boolean;
height?: string;
stepActive?: string | number;
size?: 'small' | 'default' | 'large';
inline?: boolean;
labelPosition?: string;
keyProp?: string;
popperClass?: string;
preventSubmitDefault?: boolean;
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` 获取。
*/
appContext?: AppContext | null;
/** 等待表单初始化的最长时间(毫秒),超时将以错误 reject。默认 10000ms */
timeout?: number;
}
// #endregion SubmitFormOptions
// #region SubmitFormResult
/**
* 开启 `returnChangeRecords` 时 submitForm 的返回结果
*/
export interface SubmitFormResult {
/** 校验通过后的表单值 */
values: any;
/** 表单变更记录 */
changeRecords: ChangeRecord[];
}
// #endregion SubmitFormResult
/**
* 以命令式方式调用 Form.vue 完成一次表单校验/提交。
*
* 类似 ElMessage 的用法:传入 props包含 `config`/`initValues` 等),函数内部会临时挂载
* 一个不可见的 Form 组件实例,等待初始化完成后调用其 `submitForm` 方法,
* 校验通过则 resolve 表单值,校验失败则 reject 错误信息,最后自动卸载实例。
*
* @example
* ```ts
* import { submitForm } from '@tmagic/form';
*
* try {
* const values = await submitForm({
* config: [...],
* initValues: { name: 'foo' },
* });
* console.log(values);
* } 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, returnChangeRecords, ...formProps } = options;
return new Promise((resolve, reject) => {
const container = document.createElement('div');
container.style.display = 'none';
document.body.appendChild(container);
let cleaned = false;
let timer: ReturnType<typeof setTimeout> | null = null;
const wrapperComponent = defineComponent({
name: 'MFormSubmitWrapper',
setup() {
const formRef = ref<any>(null);
const stop = watch(
() => formRef.value?.initialized,
async (initialized) => {
if (!initialized) return;
stop();
try {
// 等待子组件FormItem 等)完成首次渲染,确保 validate 能拿到所有字段
await nextTick();
// submitForm 校验通过后会清空 changeRecords需在调用前先做快照
const changeRecords: ChangeRecord[] = [...(formRef.value.changeRecords ?? [])];
const result = await formRef.value.submitForm(native);
resolve(returnChangeRecords ? { values: result, changeRecords } : result);
} catch (err) {
reject(err);
} finally {
cleanup();
}
},
{ flush: 'post', immediate: true },
);
return () => h(Form as Component, { ...formProps, ref: formRef });
},
});
const app = createApp(wrapperComponent);
// 继承父级应用上下文components / directives / provides / config 等)
if (appContext) {
Object.assign(app._context, appContext);
}
const cleanup = () => {
if (cleaned) return;
cleaned = true;
if (timer) {
clearTimeout(timer);
timer = null;
}
try {
app.unmount();
} catch {
// ignore
}
container.parentNode?.removeChild(container);
};
if (timeout > 0) {
timer = setTimeout(() => {
if (!cleaned) {
reject(new Error(`submitForm timeout after ${timeout}ms: form is not initialized.`));
cleanup();
}
}, timeout);
}
try {
app.mount(container);
} catch (err) {
reject(err);
cleanup();
}
});
};