fix(@vben-core/form-ui): correct field slot props (#8216)

This commit is contained in:
dream-weave
2026-07-31 06:07:52 +08:00
committed by GitHub
parent 418c16e093
commit cb52807b27
4 changed files with 135 additions and 12 deletions

View File

@@ -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<string, any> | undefined;
let valueSlotProps: Record<string, any> | undefined;
const [Form, formApi] = useVbenForm<ModelProtocolValues>({
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<string, any>) {
defaultSlotProps = slotProps;
return h(TestInput, {
class: 'default-protocol-input',
modelValue: slotProps.modelValue,
'onUpdate:modelValue': slotProps['onUpdate:modelValue'],
});
},
valueField(slotProps: Record<string, any>) {
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: [

View File

@@ -147,6 +147,24 @@ describe('form public types', () => {
expectTypeOf<
DefaultSlotProps['values']
>().toEqualTypeOf<AccountFormValues>();
const [WideForm] = useVbenForm<Record<string, unknown>>({ schema: [] });
type WideFormSlots = InstanceType<typeof WideForm>['$slots'];
type WideFieldSlot = NonNullable<WideFormSlots['dynamic-field']>;
type WideFieldSlotProps = Parameters<WideFieldSlot>[0];
expectTypeOf<WideFieldSlotProps>().not.toBeAny();
expectTypeOf<WideFieldSlotProps['modelValue']>().toEqualTypeOf<unknown>();
expectTypeOf<
WideFieldSlotProps['field']['state']['value']
>().toEqualTypeOf<unknown>();
expectTypeOf<
WideFieldSlotProps['componentField']['modelValue']
>().toEqualTypeOf<unknown>();
expectTypeOf<WideFieldSlotProps['name']>().toBeString();
expectTypeOf<WideFieldSlotProps['values']>().toEqualTypeOf<
Record<string, unknown>
>();
});
it('keeps form and submit values distinct with a codec', () => {

View File

@@ -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<string, any>) {
function resolveModelPropName() {
return (
modelPropName ||
(isString(component) ? componentBindEventMap.value?.[component] : null)
);
}
function fieldBindEvent(
componentField: Record<string, any>,
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<string, any>) {
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;
}

View File

@@ -235,16 +235,19 @@ export interface VbenFormDefaultSlotProps<
export interface VbenFormFieldSlotProps<
TValues extends FormValues = FormValues,
TFieldName extends KnownFormFieldName<TValues> = KnownFormFieldName<TValues>,
TFieldName extends FormFieldName<TValues> = FormFieldName<TValues>,
T extends BaseFormComponentType = BaseFormComponentType,
P extends Record<string, any> = Record<never, never>,
TSubmitValues extends FormValues = TValues,
> extends VbenFormActionSlotProps<TValues, T, P, TSubmitValues> {
componentField: FormComponentField<TValues[TFieldName], TFieldName>;
componentField: FormComponentField<
FormFieldValue<TValues, TFieldName>,
TFieldName
>;
disabled: boolean;
field: FormRuntimeField<TValues[TFieldName]>;
field: FormRuntimeField<FormFieldValue<TValues, TFieldName>>;
isInValid: boolean;
modelValue: TValues[TFieldName];
modelValue: FormFieldValue<TValues, TFieldName>;
name: TFieldName;
}
@@ -255,7 +258,19 @@ type VbenFormFieldSlots<
TSubmitValues extends FormValues,
> =
string extends Extract<keyof TValues, string>
? Record<string, ((props: any) => any) | undefined>
? Record<
string,
| ((
props: VbenFormFieldSlotProps<
TValues,
FormFieldName<TValues>,
T,
P,
TSubmitValues
>,
) => any)
| undefined
>
: {
[TFieldName in KnownFormFieldName<TValues>]?: (
props: VbenFormFieldSlotProps<