From cb52807b2711c9a4e3abf7c20376bc1a00ecad5d Mon Sep 17 00:00:00 2001 From: dream-weave <62940878+dream-weave@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:07:52 +0800 Subject: [PATCH] fix(@vben-core/form-ui): correct field slot props (#8216) --- .../__tests__/form-integration.test.ts | 76 +++++++++++++++++++ .../form-ui/__tests__/form-types.test.ts | 18 +++++ .../form-ui/src/form-render/form-field.vue | 28 +++++-- packages/@core/ui-kit/form-ui/src/types.ts | 25 ++++-- 4 files changed, 135 insertions(+), 12 deletions(-) diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts index e5491a9d..524e86d3 100644 --- a/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts +++ b/packages/@core/ui-kit/form-ui/__tests__/form-integration.test.ts @@ -104,6 +104,82 @@ describe('useVbenForm integration', () => { expect(validateValue).toHaveBeenCalledTimes(initialValidationCount + 1); }); + it('keeps only the active model protocol and boolean disabled in field slots', async () => { + interface ModelProtocolValues { + defaultField: string; + valueField: string; + } + + let defaultSlotProps: Record | undefined; + let valueSlotProps: Record | undefined; + const [Form, formApi] = useVbenForm({ + schema: [ + { + component: TestInput, + defaultValue: 'default-initial', + fieldName: 'defaultField', + }, + { + component: TestInput, + componentProps: { + eventMode: 'value-and-change', + modelValue: 'stale-model-value', + }, + defaultValue: 'value-initial', + fieldName: 'valueField', + modelPropName: 'value', + }, + ], + }); + const wrapper = mount(Form, { + slots: { + defaultField(slotProps: Record) { + defaultSlotProps = slotProps; + return h(TestInput, { + class: 'default-protocol-input', + modelValue: slotProps.modelValue, + 'onUpdate:modelValue': slotProps['onUpdate:modelValue'], + }); + }, + valueField(slotProps: Record) { + valueSlotProps = slotProps; + return h(TestInput, { + class: 'value-protocol-input', + eventMode: slotProps.eventMode, + value: slotProps.value, + 'onUpdate:value': slotProps['onUpdate:value'], + }); + }, + }, + }); + wrappers.push(wrapper); + await flushPromises(); + + expect(defaultSlotProps).toBeDefined(); + expect(valueSlotProps).toBeDefined(); + if (!defaultSlotProps || !valueSlotProps) return; + + expect(defaultSlotProps.disabled).toBe(false); + expect(defaultSlotProps.modelValue).toBe('default-initial'); + expect(defaultSlotProps).toHaveProperty('onUpdate:modelValue'); + expect(defaultSlotProps).not.toHaveProperty('value'); + + expect(valueSlotProps.disabled).toBe(false); + expect(valueSlotProps.value).toBe('value-initial'); + expect(valueSlotProps).toHaveProperty('onUpdate:value'); + expect(valueSlotProps).not.toHaveProperty('modelValue'); + expect(valueSlotProps).not.toHaveProperty('onUpdate:modelValue'); + + await wrapper.get('.default-protocol-input').setValue('default-updated'); + await wrapper.get('.value-protocol-input').setValue('value-updated'); + await flushPromises(); + + expect(await formApi.getValues()).toEqual({ + defaultField: 'default-updated', + valueField: 'value-updated', + }); + }); + it('keeps values reactive when exposed through the default slot', async () => { const [Form, formApi] = useVbenForm({ schema: [ diff --git a/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts b/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts index aee61d1e..f44c468a 100644 --- a/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts +++ b/packages/@core/ui-kit/form-ui/__tests__/form-types.test.ts @@ -147,6 +147,24 @@ describe('form public types', () => { expectTypeOf< DefaultSlotProps['values'] >().toEqualTypeOf(); + + const [WideForm] = useVbenForm>({ schema: [] }); + type WideFormSlots = InstanceType['$slots']; + type WideFieldSlot = NonNullable; + type WideFieldSlotProps = Parameters[0]; + + expectTypeOf().not.toBeAny(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf< + WideFieldSlotProps['field']['state']['value'] + >().toEqualTypeOf(); + expectTypeOf< + WideFieldSlotProps['componentField']['modelValue'] + >().toEqualTypeOf(); + expectTypeOf().toBeString(); + expectTypeOf().toEqualTypeOf< + Record + >(); }); it('keeps form and submit values distinct with a codec', () => { diff --git a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue index 223a96df..3c4d7c83 100644 --- a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue +++ b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue @@ -264,7 +264,7 @@ watch( ); const shouldDisabled = computed(() => { - return isDisabled.value || disabled || computedProps.value?.disabled; + return Boolean(isDisabled.value || disabled || computedProps.value?.disabled); }); const customContentRender = computed(() => { @@ -307,14 +307,20 @@ function createFieldSlotProps(slotProps: RuntimeFieldSlotProps) { }; } -function fieldBindEvent(componentField: Record) { +function resolveModelPropName() { + return ( + modelPropName || + (isString(component) ? componentBindEventMap.value?.[component] : null) + ); +} + +function fieldBindEvent( + componentField: Record, + bindEventField: null | string | undefined, +) { const modelValue = componentField.modelValue; const handler = componentField['onUpdate:modelValue']; - const bindEventField = - modelPropName || - (isString(component) ? componentBindEventMap.value?.[component] : null); - let value = modelValue; // antd design 的一些组件会传递一个 event 对象 if (modelValue && isObject(modelValue) && bindEventField) { @@ -348,7 +354,11 @@ function fieldBindEvent(componentField: Record) { function createComponentProps(slotProps: RuntimeFieldSlotProps) { const normalizedSlotProps = createFieldSlotProps(slotProps); - const bindEvents = fieldBindEvent(normalizedSlotProps.componentField); + const bindEventField = resolveModelPropName(); + const bindEvents = fieldBindEvent( + normalizedSlotProps.componentField, + bindEventField, + ); const binds = { ...normalizedSlotProps.componentField, @@ -361,6 +371,10 @@ function createComponentProps(slotProps: RuntimeFieldSlotProps) { ? { onInput: computedProps.value.onInput } : {}), }; + if (bindEventField && bindEventField !== 'modelValue') { + Reflect.deleteProperty(binds, 'modelValue'); + Reflect.deleteProperty(binds, 'onUpdate:modelValue'); + } return binds; } diff --git a/packages/@core/ui-kit/form-ui/src/types.ts b/packages/@core/ui-kit/form-ui/src/types.ts index 9902c9c3..93f64475 100644 --- a/packages/@core/ui-kit/form-ui/src/types.ts +++ b/packages/@core/ui-kit/form-ui/src/types.ts @@ -235,16 +235,19 @@ export interface VbenFormDefaultSlotProps< export interface VbenFormFieldSlotProps< TValues extends FormValues = FormValues, - TFieldName extends KnownFormFieldName = KnownFormFieldName, + TFieldName extends FormFieldName = FormFieldName, T extends BaseFormComponentType = BaseFormComponentType, P extends Record = Record, TSubmitValues extends FormValues = TValues, > extends VbenFormActionSlotProps { - componentField: FormComponentField; + componentField: FormComponentField< + FormFieldValue, + TFieldName + >; disabled: boolean; - field: FormRuntimeField; + field: FormRuntimeField>; isInValid: boolean; - modelValue: TValues[TFieldName]; + modelValue: FormFieldValue; name: TFieldName; } @@ -255,7 +258,19 @@ type VbenFormFieldSlots< TSubmitValues extends FormValues, > = string extends Extract - ? Record any) | undefined> + ? Record< + string, + | (( + props: VbenFormFieldSlotProps< + TValues, + FormFieldName, + T, + P, + TSubmitValues + >, + ) => any) + | undefined + > : { [TFieldName in KnownFormFieldName]?: ( props: VbenFormFieldSlotProps<