fix(@vben-core/form-ui): correct field slot props (#8216)
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
|
||||
Reference in New Issue
Block a user