feat(form,editor): submitForm/validateForm 静默挂载时注入静默标记,vs-code 字段跳过 monaco 渲染

This commit is contained in:
roymondchen 2026-07-23 20:22:47 +08:00
parent 2e031cc134
commit 3b9d5da0bc
5 changed files with 144 additions and 8 deletions

View File

@ -1,5 +1,6 @@
<template>
<MagicCodeEditor
v-if="!silentMode"
:height="config.height"
:type="diffMode ? 'diff' : undefined"
:init-values="diffMode ? (lastValues || {})[name] : model[name]"
@ -17,9 +18,9 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, inject } from 'vue';
import type { CodeConfig, FieldProps } from '@tmagic/form';
import { type CodeConfig, type FieldProps, FORM_SILENT_MODE_KEY } from '@tmagic/form';
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
@ -27,6 +28,13 @@ defineOptions({
name: 'MFieldsVsCode',
});
/**
* 静默模式submitForm/validateForm 隐藏挂载下跳过 monaco 渲染
* 校验由 FormItem 针对 model 中的值完成与编辑器实例无关本组件挂载无值副作用
* save 仅由用户操作/编辑器内容变更触发跳过可省去 monaco worker/model 的无谓实例化
*/
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
const emit = defineEmits<{
change: [value: string | any];
}>();

View File

@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils';
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
import Code from '@editor/fields/Code.vue';
// 用一个简单的桩组件代替 MagicCodeEditor把所有 props 原样渲染到 data-* 属性上,
@ -45,7 +47,7 @@ vi.mock('@editor/layouts/CodeEditor.vue', () => ({
}),
}));
const mountCode = (props: Record<string, any>) =>
const mountCode = (props: Record<string, any>, global?: Record<string, any>) =>
mount(Code, {
props: {
// FieldProps 必填字段,用 as any 绕过测试中类型严格匹配
@ -55,6 +57,7 @@ const mountCode = (props: Record<string, any>) =>
prop: 'codeField',
...props,
} as any,
global,
});
const getEl = (wrapper: ReturnType<typeof mountCode>) => wrapper.find('.fake-code-editor').element as HTMLElement;
@ -168,6 +171,28 @@ describe('Code', () => {
});
});
describe('静默模式', () => {
test('注入 FORM_SILENT_MODE_KEY=true 时不渲染 CodeEditor', () => {
const wrapper = mountCode(
{},
{
provide: { [FORM_SILENT_MODE_KEY as symbol]: true },
},
);
expect(wrapper.find('.fake-code-editor').exists()).toBe(false);
});
test('注入 FORM_SILENT_MODE_KEY=false 时正常渲染 CodeEditor', () => {
const wrapper = mountCode(
{},
{
provide: { [FORM_SILENT_MODE_KEY as symbol]: false },
},
);
expect(wrapper.find('.fake-code-editor').exists()).toBe(true);
});
});
describe('diff 模式 (对比)', () => {
test('isCompare=true 且有 lastValues 时切换为 diff 模式', () => {
const wrapper = mountCode({

View File

@ -29,6 +29,17 @@ export interface FormDiffConfig {
export const FORM_DIFF_CONFIG_KEY: InjectionKey<FormDiffConfig> = Symbol('mFormDiffConfig');
export const FORM_TYPE_MATCH_VALID_KEY: InjectionKey<ComputedRef<boolean>> = Symbol('mFormTypeMatchValid');
/**
* `submitForm` / `validateForm` providedebug
*
* vs-code inject FormItem model
* UI monaco worker / model
*
* onMounted/watch immediate emit change DOM
*
*/
export const FORM_SILENT_MODE_KEY: InjectionKey<boolean> = Symbol('mFormSilentMode');
export interface ValidateError {
message: string;
field: string;

View File

@ -16,11 +16,22 @@
* limitations under the License.
*/
import { type AppContext, type Component, createApp, defineComponent, h, nextTick, type Ref, ref, watch } from 'vue';
import {
type AppContext,
type Component,
createApp,
defineComponent,
h,
nextTick,
provide,
type Ref,
ref,
watch,
} from 'vue';
import { applyExtendState } from './utils/form';
import Form from './Form.vue';
import type { ChangeRecord, FormConfig, FormState } from './schema';
import { type ChangeRecord, FORM_SILENT_MODE_KEY, type FormConfig, type FormState } from './schema';
// #region SubmitFormOptions
/**
@ -210,7 +221,21 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
})
: userWrapper;
const app = createApp(wrapperComponent);
// 静默隐藏挂载模式下注入静默标记vs-code 等重型字段组件可据此跳过自身渲染,
// 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
// 用组件级 provide 而非 app.provideappContext 合并后 app._context.provides 与父级应用
// 共享引用app.provide 会把标记泄漏到父级应用。
const rootComponent = hidden
? defineComponent({
name: 'MFormSilentProvider',
setup() {
provide(FORM_SILENT_MODE_KEY, true);
return () => h(wrapperComponent);
},
})
: wrapperComponent;
const app = createApp(rootComponent);
instance.app = app;
// 继承父级应用上下文components / directives / provides / config 等)

View File

@ -16,18 +16,29 @@
* limitations under the License.
*/
import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest';
import { type AppContext, createApp, defineComponent, h, nextTick } from 'vue';
import MagicForm, { submitForm } from '@form/index';
import { type AppContext, createApp, defineComponent, h, inject, nextTick } from 'vue';
import MagicForm, { FORM_SILENT_MODE_KEY, submitForm, validateForm } from '@form/index';
import ElementPlus from 'element-plus';
let appContext: AppContext;
// 探针字段:挂载时注入静默标记并记录,用于验证 submitForm/validateForm 的 provide 行为
const silentProbeValues: (boolean | undefined)[] = [];
const SilentProbe = defineComponent({
name: 'MFieldsSilentProbe',
setup() {
silentProbeValues.push(inject(FORM_SILENT_MODE_KEY, undefined));
return () => h('div');
},
});
beforeAll(() => {
// 构造一个父级 app把 element-plus 与 m-form 插件装上,
// 之后通过 appContext 传给 submitForm 复用全局注册
const parentApp = createApp(defineComponent({ render: () => h('div') }));
parentApp.use(ElementPlus);
parentApp.use(MagicForm);
parentApp.component('m-fields-silent-probe', SilentProbe);
appContext = parentApp._context;
});
@ -216,6 +227,44 @@ describe('submitForm', () => {
expect(caught).toBeInstanceOf(Error);
});
test('静默(隐藏挂载)模式下向字段 provide FORM_SILENT_MODE_KEY=true', async () => {
silentProbeValues.length = 0;
const values = await submitForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect(values).toEqual({ text: 'hello' });
expect(silentProbeValues).toEqual([true]);
});
test('静默标记不会泄漏到父级应用的 provides', async () => {
silentProbeValues.length = 0;
await submitForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect((appContext as any).provides[FORM_SILENT_MODE_KEY as symbol]).toBeUndefined();
});
test('validateForm 静默模式下同样 provide FORM_SILENT_MODE_KEY=true', async () => {
silentProbeValues.length = 0;
const error = await validateForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect(error).toBe('');
expect(silentProbeValues).toEqual([true]);
});
test('timeout > 0 时会注册定时器timeout <= 0 时不注册', async () => {
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
@ -354,6 +403,24 @@ describe('submitForm —— debug 模式', () => {
expect(document.body.querySelector('.m-form')).toBeNull();
});
test('debug 模式不提供静默标记(表单可见,字段应正常渲染)', async () => {
silentProbeValues.length = 0;
const pending = submitForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
debug: true,
appContext,
});
await nextTick();
await nextTick();
expect(silentProbeValues).toEqual([undefined]);
findButton('确定').click();
await pending;
});
test('debug 模式不注册超时定时器(等待人工操作)', async () => {
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');