mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2026-10-06 14:23:19 +08:00
fix(@vben-core/form-ui): 修复嵌套部分更新与重复初始化绑定残留 (#8315)
* fix(@vben-core/form-ui): fix partial updates and bindings * fix(@vben-core/form-ui): guard cyclic patches and restore api docs table
This commit is contained in:
@@ -42,6 +42,8 @@ outline: deep
|
||||
|
||||
每个应用都有不同的 UI 框架,所以在应用的 `src/adapter/form` 和 `src/adapter/component` 内部,你可以根据自己的需求,进行组件适配。下面是 `Ant Design Vue` 的适配器示例代码,可根据注释查看说明:
|
||||
|
||||
必须先初始化组件适配器,再调用 `setupVbenForm`。每次调用都会以当前全局组件注册表重建组件及模型属性映射;重复初始化时,已从注册表移除的组件会同步清理,内置组件及其默认绑定保持不变。
|
||||
|
||||
::: details ant design vue 表单适配器
|
||||
|
||||
```ts
|
||||
@@ -417,7 +419,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| validateAndSubmit | 校验通过后提交表单 | `() => Promise<TSubmitValues \| undefined>` | - |
|
||||
| reset | 重置表单 | `(state?: FormResetState<TFormValues>, options?: FormResetOptions) => Promise<void>` | - |
|
||||
| clearValidation | 清空指定字段或全部校验,并取消进行中的异步校验 | `(fieldNames?: FormFieldName<TFormValues> \| FormFieldName<TFormValues>[]) => Promise<void>` | - |
|
||||
| setValues | 设置表单组件值,默认会过滤不在 schema 中定义的字段 | `(fields: Partial<TFormValues>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| setValues | 深层补丁更新表单值,默认会过滤不在 schema 中定义的字段 | `(fields: FormValuePatch<TFormValues>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| setSubmitValues | 通过 codec.decode 回填完整提交值 | `(values: TSubmitValues, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| getValues | 获取经过 codec.encode 或旧格式化管道的提交值 | `() => Promise<TSubmitValues>` | - |
|
||||
| getRawValues | 获取未格式化的独立表单值快照 | `() => Promise<TFormValues>` | - |
|
||||
@@ -434,6 +436,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
|
||||
| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
|
||||
|
||||
`setValues` 在默认的 `filterFields=true` 模式下会将普通对象作为深层补丁合并,因此更新 `profile.email` 时会保留 `profile` 下其他已声明字段和默认值。数组、日期、Day.js、`null`、`undefined` 等叶值仍会整体覆盖。需要替换整个对象分支时,请使用 `setFieldValue('profile', nextProfile)`;需要绕过 schema 字段过滤时,可以将 `filterFields` 设为 `false`。
|
||||
|
||||
旧命名 `submitForm`、`validateAndSubmitForm`、`resetForm`、`resetValidate` 分别对应 `submit`、`validateAndSubmit`、`reset`、`clearValidation`。它们仍可调用,但已标记 `@deprecated`,开发环境每个旧名称只警告一次,生产环境静默。
|
||||
|
||||
### FormContextApi 响应式读取
|
||||
|
||||
@@ -41,6 +41,8 @@ The current adapter pattern is:
|
||||
- map special `v-model:*` prop names through `modelPropNameMap`
|
||||
- keep the form empty state aligned with the actual UI library behavior
|
||||
|
||||
Each `setupVbenForm` call rebuilds the component and model-prop mappings from the current shared component registry. Repeated setup removes components that are no longer registered while preserving built-in components and their default bindings.
|
||||
|
||||
### Form Adapter Example
|
||||
|
||||
```ts
|
||||
@@ -227,6 +229,8 @@ Create the form through `useVbenForm`:
|
||||
|
||||
Use `useVbenForm<TFormValues, TSubmitValues>` to declare component-facing form values and submission values separately. Schema, slots, selectors, and `setValues` use `TFormValues`; `getValues()` and `submit()` return `Promise<TSubmitValues>`, while `submit()` only accepts an optional native `Event`; the first `handleSubmit` argument is `TSubmitValues`. Pass one generic when both shapes are identical.
|
||||
|
||||
`setValues` accepts a deep `FormValuePatch<TFormValues>`. With the default `filterFields=true`, plain objects are merged as patches before fields outside the schema are removed, so updating `profile.email` preserves declared sibling fields and defaults under `profile`. Arrays, dates, Day.js values, `null`, and `undefined` replace the corresponding value atomically. Use `setFieldValue('profile', nextProfile)` to replace an entire object branch, or set `filterFields` to `false` to bypass schema filtering.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben-core/shared/global-state';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
COMPONENT_BIND_EVENT_MAP,
|
||||
COMPONENT_MAP,
|
||||
setupVbenForm,
|
||||
} from '../src/config';
|
||||
|
||||
const builtInCheckbox = COMPONENT_MAP.VbenCheckbox;
|
||||
const componentMapReference = COMPONENT_MAP;
|
||||
const bindEventMapReference = COMPONENT_BIND_EVENT_MAP;
|
||||
|
||||
function resetFormConfig() {
|
||||
globalShareState.setComponents({});
|
||||
setupVbenForm({ config: {} });
|
||||
}
|
||||
|
||||
beforeEach(resetFormConfig);
|
||||
afterEach(resetFormConfig);
|
||||
|
||||
describe('setupVbenForm', () => {
|
||||
it('rebuilds user mappings while preserving built-ins and record identity', () => {
|
||||
const FirstInput = defineComponent({});
|
||||
const SecondInput = defineComponent({});
|
||||
|
||||
globalShareState.setComponents({ FirstInput });
|
||||
setupVbenForm({ config: { baseModelPropName: 'value' } });
|
||||
|
||||
expect(COMPONENT_MAP.FirstInput).toBe(FirstInput);
|
||||
expect(COMPONENT_BIND_EVENT_MAP.FirstInput).toBe('value');
|
||||
|
||||
globalShareState.setComponents({ SecondInput });
|
||||
setupVbenForm({ config: {} });
|
||||
|
||||
expect(COMPONENT_MAP).toBe(componentMapReference);
|
||||
expect(COMPONENT_BIND_EVENT_MAP).toBe(bindEventMapReference);
|
||||
expect(Reflect.has(COMPONENT_MAP, 'FirstInput')).toBe(false);
|
||||
expect(Reflect.has(COMPONENT_BIND_EVENT_MAP, 'FirstInput')).toBe(false);
|
||||
expect(COMPONENT_MAP.SecondInput).toBe(SecondInput);
|
||||
expect(COMPONENT_BIND_EVENT_MAP.SecondInput).toBeUndefined();
|
||||
expect(COMPONENT_MAP.VbenCheckbox).toBe(builtInCheckbox);
|
||||
expect(COMPONENT_BIND_EVENT_MAP.VbenCheckbox).toBe('checked');
|
||||
});
|
||||
|
||||
it('prefers component mappings over the base model prop name', () => {
|
||||
const CustomInput = defineComponent({});
|
||||
globalShareState.setComponents({ CustomInput });
|
||||
|
||||
setupVbenForm({
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
modelPropNameMap: { CustomInput: 'checked' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(COMPONENT_BIND_EVENT_MAP.CustomInput).toBe('checked');
|
||||
});
|
||||
});
|
||||
@@ -374,6 +374,155 @@ describe('formApi', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve nested schema siblings in touched branches', async () => {
|
||||
const setValuesMock = vi.fn();
|
||||
formApi.setState({
|
||||
schema: [
|
||||
{ component: 'text', fieldName: 'profile.email' },
|
||||
{ component: 'text', fieldName: 'profile.nickname' },
|
||||
],
|
||||
});
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
setValues: setValuesMock,
|
||||
values: {
|
||||
profile: {
|
||||
email: 'old@example.com',
|
||||
ignored: true,
|
||||
nickname: 'Ada',
|
||||
},
|
||||
untouched: 'keep',
|
||||
},
|
||||
};
|
||||
|
||||
await formApi.mount(formActions, new Map());
|
||||
await formApi.setValues({
|
||||
profile: { email: 'new@example.com' },
|
||||
});
|
||||
|
||||
expect(setValuesMock).toHaveBeenCalledWith(
|
||||
{
|
||||
profile: {
|
||||
email: 'new@example.com',
|
||||
nickname: 'Ada',
|
||||
},
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should patch object fields while replacing atomic values', async () => {
|
||||
class AtomicValue {
|
||||
constructor(readonly value: string) {}
|
||||
}
|
||||
|
||||
const setValuesMock = vi.fn();
|
||||
const atomicValue = new AtomicValue('new');
|
||||
const updatedAt = new Date('2026-08-27T00:00:00.000Z');
|
||||
const fields = {
|
||||
atomicValue,
|
||||
cleared: undefined,
|
||||
contacts: [{ name: 'Grace' }],
|
||||
nullable: null,
|
||||
profile: { email: 'new@example.com' },
|
||||
updatedAt,
|
||||
};
|
||||
formApi.setState({
|
||||
schema: [
|
||||
{ component: 'text', fieldName: 'atomicValue' },
|
||||
{ component: 'text', fieldName: 'cleared' },
|
||||
{ component: 'text', fieldName: 'contacts' },
|
||||
{ component: 'text', fieldName: 'nullable' },
|
||||
{ component: 'text', fieldName: 'profile' },
|
||||
{ component: 'text', fieldName: 'updatedAt' },
|
||||
],
|
||||
});
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
setValues: setValuesMock,
|
||||
values: {
|
||||
atomicValue: new AtomicValue('old'),
|
||||
cleared: 'remove',
|
||||
contacts: [{ name: 'Ada' }],
|
||||
nullable: 'remove',
|
||||
profile: { bio: 'Mathematician', email: 'old@example.com' },
|
||||
updatedAt: new Date('2026-08-26T00:00:00.000Z'),
|
||||
},
|
||||
};
|
||||
|
||||
await formApi.mount(formActions, new Map());
|
||||
await formApi.setValues(fields, true, true);
|
||||
|
||||
expect(fields).toEqual({
|
||||
atomicValue,
|
||||
cleared: undefined,
|
||||
contacts: [{ name: 'Grace' }],
|
||||
nullable: null,
|
||||
profile: { email: 'new@example.com' },
|
||||
updatedAt,
|
||||
});
|
||||
expect(setValuesMock).toHaveBeenCalledWith(
|
||||
{
|
||||
atomicValue,
|
||||
cleared: undefined,
|
||||
contacts: [{ name: 'Grace' }],
|
||||
nullable: null,
|
||||
profile: {
|
||||
bio: 'Mathematician',
|
||||
email: 'new@example.com',
|
||||
},
|
||||
updatedAt,
|
||||
},
|
||||
true,
|
||||
);
|
||||
expect(setValuesMock.mock.calls[0]?.[0]?.atomicValue).toBeInstanceOf(
|
||||
AtomicValue,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cyclic plain-object patches without recursion overflow', async () => {
|
||||
const setValuesMock = vi.fn();
|
||||
const profile: Record<string, unknown> = { email: 'new@example.com' };
|
||||
profile.self = profile;
|
||||
formApi.setState({
|
||||
schema: [{ component: 'text', fieldName: 'profile.email' }],
|
||||
});
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
setValues: setValuesMock,
|
||||
values: { profile: { bio: 'Mathematician' } },
|
||||
};
|
||||
|
||||
await formApi.mount(formActions, new Map());
|
||||
await formApi.setValues({ profile });
|
||||
|
||||
expect(setValuesMock).toHaveBeenCalledTimes(1);
|
||||
expect(setValuesMock).toHaveBeenCalledWith(
|
||||
{ profile: { email: 'new@example.com' } },
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should preserve raw-key fields during filtered updates', async () => {
|
||||
const setValuesMock = vi.fn();
|
||||
formApi.setState({
|
||||
schema: [{ component: 'text', fieldName: '[profile.email]' }],
|
||||
});
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
setValues: setValuesMock,
|
||||
values: { 'profile.email': 'old@example.com' },
|
||||
};
|
||||
|
||||
await formApi.mount(formActions, new Map());
|
||||
await formApi.setValues({ 'profile.email': 'new@example.com' });
|
||||
|
||||
expect(setValuesMock).toHaveBeenCalledWith(
|
||||
{ 'profile.email': 'new@example.com' },
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset form', async () => {
|
||||
const resetMock = vi.fn();
|
||||
const formActions: any = {
|
||||
|
||||
@@ -1081,6 +1081,45 @@ describe('useVbenForm integration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves nested defaults during partial setValues updates', async () => {
|
||||
interface NestedFormValues {
|
||||
profile: {
|
||||
email: string;
|
||||
nickname: string;
|
||||
};
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm<NestedFormValues>({
|
||||
schema: [
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: 'old@example.com',
|
||||
fieldName: 'profile.email',
|
||||
},
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: 'Ada',
|
||||
fieldName: 'profile.nickname',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
await formApi.setValues({
|
||||
profile: { email: 'new@example.com' },
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
expect(await formApi.getRawValues()).toEqual({
|
||||
profile: {
|
||||
email: 'new@example.com',
|
||||
nickname: 'Ada',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('retains initial values for unspecified fields on partial reset', async () => {
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
FormFieldOptions,
|
||||
FormItemDependencies,
|
||||
FormValidationResult,
|
||||
FormValuePatch,
|
||||
FormValueSnapshot,
|
||||
VbenFormAdapterOptions,
|
||||
VbenFormProps,
|
||||
@@ -121,7 +122,13 @@ describe('form public types', () => {
|
||||
}>();
|
||||
expectTypeOf(typedFormApi.setValues)
|
||||
.parameter(0)
|
||||
.toEqualTypeOf<Partial<AccountFormValues>>();
|
||||
.toEqualTypeOf<FormValuePatch<AccountFormValues>>();
|
||||
expectTypeOf<
|
||||
FormValuePatch<AccountFormValues>['profile']
|
||||
>().toEqualTypeOf<undefined | { nickname?: string }>();
|
||||
expectTypeOf<FormValuePatch<AccountFormValues>['roles']>().toEqualTypeOf<
|
||||
string[] | undefined
|
||||
>();
|
||||
expectTypeOf(typedFormApi.form.values).toEqualTypeOf<AccountFormValues>();
|
||||
expectTypeOf(contextApi.getFieldValue('email')).toEqualTypeOf<string>();
|
||||
expectTypeOf(
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_MODEL_PROP_NAME = 'modelValue';
|
||||
|
||||
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
|
||||
|
||||
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||
const BUILT_IN_COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||
DefaultButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
|
||||
PrimaryButton: h(VbenButton, { size: 'sm', variant: 'default' }),
|
||||
VbenCheckbox,
|
||||
@@ -37,12 +37,29 @@ export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||
VbenSelect,
|
||||
};
|
||||
|
||||
export const COMPONENT_BIND_EVENT_MAP: Partial<
|
||||
const BUILT_IN_COMPONENT_BIND_EVENT_MAP: Partial<
|
||||
Record<BaseFormComponentType, string>
|
||||
> = {
|
||||
VbenCheckbox: 'checked',
|
||||
};
|
||||
|
||||
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||
...BUILT_IN_COMPONENT_MAP,
|
||||
};
|
||||
|
||||
export const COMPONENT_BIND_EVENT_MAP: Partial<
|
||||
Record<BaseFormComponentType, string>
|
||||
> = {
|
||||
...BUILT_IN_COMPONENT_BIND_EVENT_MAP,
|
||||
};
|
||||
|
||||
function replaceRecord<T extends object>(target: T, source: T) {
|
||||
for (const key of Object.keys(target)) {
|
||||
Reflect.deleteProperty(target, key);
|
||||
}
|
||||
Object.assign(target, source);
|
||||
}
|
||||
|
||||
export function setupVbenForm<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
>(options: VbenFormAdapterOptions<T>) {
|
||||
@@ -74,18 +91,27 @@ export function setupVbenForm<
|
||||
| undefined;
|
||||
|
||||
const components = globalShareState.getComponents();
|
||||
const nextComponentMap = {
|
||||
...BUILT_IN_COMPONENT_MAP,
|
||||
...components,
|
||||
} as Record<BaseFormComponentType, Component>;
|
||||
const nextBindEventMap = {
|
||||
...BUILT_IN_COMPONENT_BIND_EVENT_MAP,
|
||||
} as Partial<Record<BaseFormComponentType, string>>;
|
||||
|
||||
for (const component of Object.keys(components)) {
|
||||
const key = component as BaseFormComponentType;
|
||||
COMPONENT_MAP[key] = components[component as never];
|
||||
|
||||
if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) {
|
||||
COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName;
|
||||
nextBindEventMap[key] = baseModelPropName;
|
||||
}
|
||||
|
||||
// 覆盖特殊组件的modelPropName
|
||||
if (modelPropNameMap && modelPropNameMap[key]) {
|
||||
COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key];
|
||||
nextBindEventMap[key] = modelPropNameMap[key];
|
||||
}
|
||||
}
|
||||
|
||||
replaceRecord(COMPONENT_MAP, nextComponentMap);
|
||||
replaceRecord(COMPONENT_BIND_EVENT_MAP, nextBindEventMap);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
FormResetOptions,
|
||||
FormResetState,
|
||||
FormSchema,
|
||||
FormValuePatch,
|
||||
FormValues,
|
||||
FormValueSnapshot,
|
||||
VbenFormProps,
|
||||
@@ -19,10 +20,7 @@ import { Store } from '@vben-core/shared/store';
|
||||
import {
|
||||
bindMethods,
|
||||
cloneDeep,
|
||||
isDate,
|
||||
isDayjsObject,
|
||||
isFunction,
|
||||
isObject,
|
||||
mergeWithArrayOverride,
|
||||
StateHandler,
|
||||
} from '@vben-core/shared/utils';
|
||||
@@ -46,6 +44,36 @@ type FormApiSchema<
|
||||
P extends Record<string, any>,
|
||||
> = FormSchema<T, P, TValues>;
|
||||
|
||||
function isPlainFormObject(value: unknown): value is Record<string, unknown> {
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === null || prototype === Object.prototype;
|
||||
}
|
||||
|
||||
function mergeFormValuePatch(
|
||||
currentValue: unknown,
|
||||
nextValue: unknown,
|
||||
visited = new WeakMap<object, Record<string, unknown>>(),
|
||||
): unknown {
|
||||
if (!isPlainFormObject(nextValue)) {
|
||||
return cloneDeep(nextValue);
|
||||
}
|
||||
|
||||
const cached = visited.get(nextValue);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = isPlainFormObject(currentValue) ? cloneDeep(currentValue) : {};
|
||||
visited.set(nextValue, result);
|
||||
for (const [key, value] of Object.entries(nextValue)) {
|
||||
result[key] = mergeFormValuePatch(result[key], value, visited);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getDefaultState<
|
||||
TFormValues extends FormValues,
|
||||
T extends BaseFormComponentType,
|
||||
@@ -452,7 +480,11 @@ export class FormApi<
|
||||
);
|
||||
}
|
||||
const formValues = decodeFormValues(codec, values);
|
||||
await this.setValues(formValues, filterFields, shouldValidate);
|
||||
await this.setValues(
|
||||
formValues as FormValuePatch<TFormValues>,
|
||||
filterFields,
|
||||
shouldValidate,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,16 +494,24 @@ export class FormApi<
|
||||
* @param shouldValidate
|
||||
*/
|
||||
async setValues(
|
||||
fields: Partial<TFormValues>,
|
||||
fields: FormValuePatch<TFormValues>,
|
||||
filterFields: boolean = true,
|
||||
shouldValidate: boolean = false,
|
||||
) {
|
||||
const form = await this.getForm();
|
||||
if (!filterFields) {
|
||||
form.setValues(fields, shouldValidate);
|
||||
form.setValues(fields as Partial<TFormValues>, shouldValidate);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValues = toRaw(form.values ?? {}) as Record<string, unknown>;
|
||||
const mergedFields = Object.fromEntries(
|
||||
Object.entries(fields).map(([key, value]) => [
|
||||
key,
|
||||
mergeFormValuePatch(currentValues[key], value),
|
||||
]),
|
||||
);
|
||||
|
||||
const schemaFieldPaths = (this.state?.schema ?? []).map(
|
||||
(schema) => resolveFieldNamePath(schema.fieldName).pathSegments,
|
||||
);
|
||||
@@ -479,12 +519,7 @@ export class FormApi<
|
||||
value: unknown,
|
||||
parentPath: string[] = [],
|
||||
): unknown => {
|
||||
if (
|
||||
!isObject(value) ||
|
||||
Array.isArray(value) ||
|
||||
isDate(value) ||
|
||||
isDayjsObject(value)
|
||||
) {
|
||||
if (!isPlainFormObject(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -510,8 +545,8 @@ export class FormApi<
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const filteredFields = filterValue(fields) as Partial<TFormValues>;
|
||||
form.setValues(filteredFields as Partial<TFormValues>, shouldValidate);
|
||||
const filteredFields = filterValue(mergedFields) as Partial<TFormValues>;
|
||||
form.setValues(filteredFields, shouldValidate);
|
||||
}
|
||||
|
||||
async submit(e?: Event) {
|
||||
|
||||
@@ -14,6 +14,15 @@ export type FormLabelWidthContext = UnwrapNestedRefs<
|
||||
|
||||
export type FormValues = Record<string, any>;
|
||||
|
||||
export type FormValuePatch<T> = T extends
|
||||
| ((...args: any[]) => unknown)
|
||||
| Date
|
||||
| readonly unknown[]
|
||||
? T
|
||||
: T extends object
|
||||
? { [K in keyof T]?: FormValuePatch<T[K]> }
|
||||
: T;
|
||||
|
||||
export interface FormCodec<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
|
||||
Reference in New Issue
Block a user