From 90eae0368001425292704ca8736b412a1170e223 Mon Sep 17 00:00:00 2001 From: qiang Date: Wed, 11 Dec 2024 16:22:18 +0800 Subject: [PATCH] feat(Form): add models attribute (#544) * feat(Form): add models attribute * chore: optimize the compatibility of modelKey attribute --- demo/Form/{modelKey.vue => models.vue} | 2 +- docs/en-US/components/Form.md | 14 +++-- docs/en-US/components/Search.md | 2 +- docs/zh-CN/components/Form.md | 80 ++++++++++++----------- docs/zh-CN/components/Search.md | 68 ++++++++++---------- src/Form/FormComponent.ts | 22 ++----- src/Form/FormItem.ts | 72 ++++++++++++++------- src/Form/index.test.ts | 87 +++++++++++++++++--------- src/Form/props.ts | 2 - src/Form/type.ts | 17 ++++- 10 files changed, 216 insertions(+), 150 deletions(-) rename demo/Form/{modelKey.vue => models.vue} (94%) diff --git a/demo/Form/modelKey.vue b/demo/Form/models.vue similarity index 94% rename from demo/Form/modelKey.vue rename to demo/Form/models.vue index dff47bd6..8c717a24 100644 --- a/demo/Form/modelKey.vue +++ b/demo/Form/models.vue @@ -38,7 +38,7 @@ export default defineComponent({ label: 'Use in the ProForm', prop: 'value', component: markRaw(MyInput), - modelKey: 'value', // or ['value', 'onUpdate:value'] + models: [{ prop: 'value', key: 'value', event: 'update:value' }], }, ]) const submit = defineFormSubmit((done, isValid, invalidFields) => { diff --git a/docs/en-US/components/Form.md b/docs/en-US/components/Form.md index 4fa6f1e2..6b1b3148 100644 --- a/docs/en-US/components/Form.md +++ b/docs/en-US/components/Form.md @@ -61,10 +61,16 @@ Local component can be passed directly through `component` in `columns` attribut ### Configure the v-model arguments for component -By default, the ProForm component only supports components that bind values through `v-model`. If you need to use other arguments to bind values, you can configure it through `modelKey`. +By default, the ProForm component only supports components that bind values through `v-model`. If you need to use other arguments to bind values, you can configure it through `models`. When `models` is specified, the `prop` attribute will be ignored (the binding implemented by `v-model` will be invalid), and you need to pass `{ prop: [prop], key: 'modelValue' }` to `models` to implement the binding. -::: demo In addition to supporting strings, modelKey also supports passing in `[prop, event]` (`prop` is used to configure the parameters of the bound value, `event` is used to configure the event of the bound value) -@/demo/Form/modelKey.vue +In addition, `models` can also be used to bind multiple parameters to a component + +::: tip +Since `1.4.0`, the `modelKey` attribute has been deprecated, please use the `models` attribute instead +::: + +::: demo prop: the name of the bound field, key: the parameter used to configure v-model, event: the event used to configure the bound value +@/demo/Form/models.vue ::: ### Slots @@ -236,7 +242,7 @@ The function `defineFormColumns` supports passing in a Generics type to infer th | label | label text | string | - | - | | component | binding component | string | - | - | | props | transfer `props` to the current component | object | - | - | -| modelKey | the arguments name bound to the `v-model` of the current component | string / [string, string] | - | - | +| models | Configure the `v-model` binding parameters of the component corresponding to the current item (Array<{ prop, key, event }>) | array | - | - | | children | group form or sub-form content | array | - | - | | type | type of children internal forms | string | array / group / tabs / collapse / steps | array | | max | limit the maximum number of `type=array` | number | - | - | diff --git a/docs/en-US/components/Search.md b/docs/en-US/components/Search.md index 5ed5a5b1..f7429c6e 100644 --- a/docs/en-US/components/Search.md +++ b/docs/en-US/components/Search.md @@ -85,7 +85,7 @@ The function `defineSearchColumns` supports passing in a Generics type to infer | label | label text | string | - | - | | component | binding component | string | - | - | | props | transfer `props` to the current component | object | - | - | -| modelKey | the arguments name bound to the `v-model` of the current component | string / [string, string] | - | - | +| models | Configure the `v-model` binding parameters of the component corresponding to the current item (Array<{ prop, key, event }>) | array | - | - | | children | group form or sub-form content | array | - | - | | type | type of children internal forms | string | array / group / tabs / collapse / steps | array | | max | limit the maximum number of `type=array` | number | - | - | diff --git a/docs/zh-CN/components/Form.md b/docs/zh-CN/components/Form.md index aef05321..82ba6f8d 100644 --- a/docs/zh-CN/components/Form.md +++ b/docs/zh-CN/components/Form.md @@ -61,10 +61,16 @@ meta: ### 配置组件绑定的 v-model 参数 -默认情况下 ProForm 组件仅支持使用 `v-model` 绑定值的组件,如果需要使用其他参数绑定值,可以通过 `modelKey` 配置 +默认情况下 ProForm 组件仅支持使用 `v-model` 绑定值的组件,如果需要使用其他参数绑定值,可以通过 `models` 配置。当指定 `models` 时,`prop` 属性会被忽略(`v-model` 实现的绑定将无效),需要额外向 `models` 中传入 `{ prop: [prop], key: 'modelValue' }` 来实现绑定。 -::: demo modelKey 除了支持字符串,还支持传入 `[prop, event]` (`prop` 用于配置绑定值的参数,`event` 用于配置绑定值的事件) -@/demo/Form/modelKey.vue +另外 `models` 也可以用来为一个组件绑定多个参数 + +::: tip 提示 +从 `1.4.0` 起,`modelKey` 属性已废弃,请使用 `models` 属性替代 +::: + +::: demo prop: 绑定的字段名, key: 用于配置 v-model 的参数, event: 用于配置绑定值的事件 +@/demo/Form/models.vue ::: ### 使用插槽 @@ -230,40 +236,40 @@ meta: #### columns -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -| :------------ | :---------------------------------------------------------------------------------------------------------- | :------------------------ | :---------------------------------------- | :----- | -| prop | v-model 绑定的字段名 (**需要是唯一值**) | string | - | - | -| label | 标签文本 | string | - | - | -| component | 当前项对应的组件,可以直接传入局部组件 | string / Component | - | - | -| props | 传递的对应的组件的参数 | object | - | - | -| modelKey | 当前项对应的组件的 `v-model` 绑定的字段名 | string / [string, string] | - | - | -| children | 分组表单或子表单内容 | array | - | - | -| type | children 内部表单的类型 | string | array / group / tabs / collapse / steps | array | -| max | 限制 `type=array` 时子表单的最大数量 | number | - | - | -| show | 是否在表单中显示当前项 | boolean | - | true | -| labelWidth | 表单域标签的宽度,例如 '50px' 或 'auto' | string | - | - | -| labelPosition | 表单域标签的位置, 当设置为 left 或 right 时,则也需要设置 label-width 属性 默认会继承 Form的label-position | string | left / right / top | - | -| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | - | false | -| rules | 表单验证规则 | object / array | - | - | -| error | 表单域验证错误信息, 设置该值会使表单验证状态变为`error`,并显示该错误信息 | string | - | - | -| showMessage | 是否显示校验错误信息 | boolean | - | true | -| inlineMessage | 以行内形式展示校验信息 | boolean | - | false | -| size | 用于控制该表单域下组件的尺寸 | string | large / default /small | - | -| span | 栅格占据的列数 | number | - | 24 | -| offset | 栅格左侧的间隔格数 | number | - | 0 | -| push | 栅格向右移动格数 | number | - | 0 | -| pull | 栅格向左移动格数 | number | - | 0 | -| xs | `<768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| sm | `≥768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| md | `≥992px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| lg | `≥1200px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| xl | `≥1920px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| disabled | 是否禁用, 当 `type=tabs` 或者 `type=collapse` 时生效 | boolean | — | false | -| closable | 标签是否可关闭, 当 `type=tabs` 时生效 | boolean | — | false | -| lazy | 标签是否延迟渲染, 当 `type=tabs` 时生效 | boolean | — | false | -| description | 分步描述文案, 当 `type=steps` 时生效 | string | — | — | -| icon | 自定义分步图标, 当 `type=steps` 时生效 | string / Component | — | — | -| status | 设置当前步骤的状态, 当 `type=steps` 时生效 | string | wait / process / finish / error / success | — | +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| :------------ | :---------------------------------------------------------------------------------------------------------- | :----------------- | :---------------------------------------- | :----- | +| prop | v-model 绑定的字段名 (**需要是唯一值**) | string | - | - | +| label | 标签文本 | string | - | - | +| component | 当前项对应的组件,可以直接传入局部组件 | string / Component | - | - | +| props | 传递的对应的组件的参数 | object | - | - | +| models | 配置当前项对应的组件的 `v-model` 绑定的参数 (Array<{ prop, key, event }>) | array | - | - | +| children | 分组表单或子表单内容 | array | - | - | +| type | children 内部表单的类型 | string | array / group / tabs / collapse / steps | array | +| max | 限制 `type=array` 时子表单的最大数量 | number | - | - | +| show | 是否在表单中显示当前项 | boolean | - | true | +| labelWidth | 表单域标签的宽度,例如 '50px' 或 'auto' | string | - | - | +| labelPosition | 表单域标签的位置, 当设置为 left 或 right 时,则也需要设置 label-width 属性 默认会继承 Form的label-position | string | left / right / top | - | +| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | - | false | +| rules | 表单验证规则 | object / array | - | - | +| error | 表单域验证错误信息, 设置该值会使表单验证状态变为`error`,并显示该错误信息 | string | - | - | +| showMessage | 是否显示校验错误信息 | boolean | - | true | +| inlineMessage | 以行内形式展示校验信息 | boolean | - | false | +| size | 用于控制该表单域下组件的尺寸 | string | large / default /small | - | +| span | 栅格占据的列数 | number | - | 24 | +| offset | 栅格左侧的间隔格数 | number | - | 0 | +| push | 栅格向右移动格数 | number | - | 0 | +| pull | 栅格向左移动格数 | number | - | 0 | +| xs | `<768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| sm | `≥768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| md | `≥992px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| lg | `≥1200px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| xl | `≥1920px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| disabled | 是否禁用, 当 `type=tabs` 或者 `type=collapse` 时生效 | boolean | — | false | +| closable | 标签是否可关闭, 当 `type=tabs` 时生效 | boolean | — | false | +| lazy | 标签是否延迟渲染, 当 `type=tabs` 时生效 | boolean | — | false | +| description | 分步描述文案, 当 `type=steps` 时生效 | string | — | — | +| icon | 自定义分步图标, 当 `type=steps` 时生效 | string / Component | — | — | +| status | 设置当前步骤的状态, 当 `type=steps` 时生效 | string | wait / process / finish / error / success | — | ::: tip 关于 props props 的属性将全部传递给 component 指定的组件 diff --git a/docs/zh-CN/components/Search.md b/docs/zh-CN/components/Search.md index e02482c8..d56af7a2 100644 --- a/docs/zh-CN/components/Search.md +++ b/docs/zh-CN/components/Search.md @@ -79,40 +79,40 @@ Search 组件基于 Form 组件 #### columns -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -| :------------ | :---------------------------------------------------------------------------------------------------------- | :------------------------ | :---------------------------------------- | :----- | -| prop | v-model 绑定的字段名 (**需要是唯一值**) | string | - | - | -| label | 标签文本 | string | - | - | -| component | 当前项对应的组件,可以直接传入局部组件 | string / Component | - | - | -| props | 传递的对应的组件的参数 | object | - | - | -| modelKey | 当前项对应的组件的 `v-model` 绑定的字段名 | string / [string, string] | - | - | -| children | 分组表单或子表单内容 | array | - | - | -| type | children 内部表单的类型 | string | array / group / tabs / collapse / steps | array | -| max | 限制 `type=array` 时子表单的最大数量 | number | - | - | -| show | 是否在表单中显示当前项 | boolean | - | true | -| labelWidth | 表单域标签的宽度,例如 '50px' 或 'auto' | string | - | - | -| labelPosition | 表单域标签的位置, 当设置为 left 或 right 时,则也需要设置 label-width 属性 默认会继承 Form的label-position | string | left / right / top | - | -| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | - | false | -| rules | 表单验证规则 | object / array | - | - | -| error | 表单域验证错误信息, 设置该值会使表单验证状态变为`error`,并显示该错误信息 | string | - | - | -| showMessage | 是否显示校验错误信息 | boolean | - | true | -| inlineMessage | 以行内形式展示校验信息 | boolean | - | false | -| size | 用于控制该表单域下组件的尺寸 | string | large / default /small | - | -| span | 栅格占据的列数 | number | - | 24 | -| offset | 栅格左侧的间隔格数 | number | - | 0 | -| push | 栅格向右移动格数 | number | - | 0 | -| pull | 栅格向左移动格数 | number | - | 0 | -| xs | `<768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| sm | `≥768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| md | `≥992px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| lg | `≥1200px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| xl | `≥1920px` 响应式栅格数或者栅格属性对象 | number / object | - | - | -| disabled | 是否禁用, 当 `type=tabs` 或者 `type=collapse` 时生效 | boolean | — | false | -| closable | 标签是否可关闭, 当 `type=tabs` 时生效 | boolean | — | false | -| lazy | 标签是否延迟渲染, 当 `type=tabs` 时生效 | boolean | — | false | -| description | 分步描述文案, 当 `type=steps` 时生效 | string | — | — | -| icon | 自定义分步图标, 当 `type=steps` 时生效 | string / Component | — | — | -| status | 设置当前步骤的状态, 当 `type=steps` 时生效 | string | wait / process / finish / error / success | — | +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| :------------ | :---------------------------------------------------------------------------------------------------------- | :----------------- | :---------------------------------------- | :----- | +| prop | v-model 绑定的字段名 (**需要是唯一值**) | string | - | - | +| label | 标签文本 | string | - | - | +| component | 当前项对应的组件,可以直接传入局部组件 | string / Component | - | - | +| props | 传递的对应的组件的参数 | object | - | - | +| models | 配置当前项对应的组件的 `v-model` 绑定的参数 (Array<{ prop, key, event }>) | array | - | - | +| children | 分组表单或子表单内容 | array | - | - | +| type | children 内部表单的类型 | string | array / group / tabs / collapse / steps | array | +| max | 限制 `type=array` 时子表单的最大数量 | number | - | - | +| show | 是否在表单中显示当前项 | boolean | - | true | +| labelWidth | 表单域标签的宽度,例如 '50px' 或 'auto' | string | - | - | +| labelPosition | 表单域标签的位置, 当设置为 left 或 right 时,则也需要设置 label-width 属性 默认会继承 Form的label-position | string | left / right / top | - | +| required | 是否必填,如不设置,则会根据校验规则自动生成 | boolean | - | false | +| rules | 表单验证规则 | object / array | - | - | +| error | 表单域验证错误信息, 设置该值会使表单验证状态变为`error`,并显示该错误信息 | string | - | - | +| showMessage | 是否显示校验错误信息 | boolean | - | true | +| inlineMessage | 以行内形式展示校验信息 | boolean | - | false | +| size | 用于控制该表单域下组件的尺寸 | string | large / default /small | - | +| span | 栅格占据的列数 | number | - | 24 | +| offset | 栅格左侧的间隔格数 | number | - | 0 | +| push | 栅格向右移动格数 | number | - | 0 | +| pull | 栅格向左移动格数 | number | - | 0 | +| xs | `<768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| sm | `≥768px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| md | `≥992px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| lg | `≥1200px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| xl | `≥1920px` 响应式栅格数或者栅格属性对象 | number / object | - | - | +| disabled | 是否禁用, 当 `type=tabs` 或者 `type=collapse` 时生效 | boolean | — | false | +| closable | 标签是否可关闭, 当 `type=tabs` 时生效 | boolean | — | false | +| lazy | 标签是否延迟渲染, 当 `type=tabs` 时生效 | boolean | — | false | +| description | 分步描述文案, 当 `type=steps` 时生效 | string | — | — | +| icon | 自定义分步图标, 当 `type=steps` 时生效 | string / Component | — | — | +| status | 设置当前步骤的状态, 当 `type=steps` 时生效 | string | wait / process / finish / error / success | — | ::: tip 关于 props props 的属性将全部传递给 component 指定的组件 diff --git a/src/Form/FormComponent.ts b/src/Form/FormComponent.ts index 26420a65..681ee601 100644 --- a/src/Form/FormComponent.ts +++ b/src/Form/FormComponent.ts @@ -1,5 +1,5 @@ import { defineComponent, h, mergeProps, resolveComponent } from 'vue' -import { isArray, isFunction, isObject, isString } from '../utils/index' +import { isFunction, isObject, isString } from '../utils/index' import { formComponentProps } from './props' import type { DefineComponent, Slot } from 'vue' import type { StringObject } from '../types/index' @@ -14,7 +14,6 @@ interface TargetEvent { export default defineComponent({ name: 'ProFormComponent', props: formComponentProps, - emits: ['update:modelValue'], setup(props, { attrs, emit }) { const nativeComponents = ['input', 'textarea', 'select'] @@ -25,20 +24,7 @@ export default defineComponent({ } function getProps() { - let prop = 'modelValue' - let event = 'onUpdate:modelValue' - - if (props.modelKey && isArray(props.modelKey)) { - ;[prop, event] = props.modelKey - } else if (props.modelKey) { - prop = props.modelKey - event = `onUpdate:${props.modelKey}` - } - - const _props: StringObject = mergeProps(attrs, { - [prop]: props.modelValue, - [event]: (value: unknown) => emit('update:modelValue', value), - }) + const _props: StringObject = mergeProps({}, attrs) if (isString(props.is) && nativeComponents.includes(props.is)) { if ( @@ -46,11 +32,11 @@ export default defineComponent({ attrs.type === 'checkbox' || attrs.type === 'radio' ) { - _props.checked = props.modelValue + _props.checked = attrs.modelValue _props.onChange = (value: TargetEvent) => emit('update:modelValue', value.target.checked) } else { - _props.value = props.modelValue + _props.value = attrs.modelValue _props.onInput = (value: TargetEvent) => emit('update:modelValue', value.target.value) } diff --git a/src/Form/FormItem.ts b/src/Form/FormItem.ts index 583b20fd..173570bc 100644 --- a/src/Form/FormItem.ts +++ b/src/Form/FormItem.ts @@ -1,12 +1,13 @@ -import { defineComponent, h, Slot, VNode, mergeProps, toRef } from 'vue' +import { defineComponent, h, mergeProps, computed } from 'vue' import { ElFormItem, useSize } from 'element-plus' import { useCol } from '../composables/index' -import { get, set, has } from '../utils/index' +import { get, set, has, throwWarn, isArray } from '../utils/index' import { useCreateLabel, useFormInject } from './useForm' import { getFormItemBind } from './utils' import { formItemProps, formItemEmits } from './props' import ProFormList from './FormList' import ProFormComponent from './FormComponent' +import type { Slot, VNode } from 'vue' import type { UnknownObject } from '../types/index' export default defineComponent({ @@ -14,29 +15,57 @@ export default defineComponent({ props: formItemProps, emits: formItemEmits, setup(props, { emit }) { - const item = toRef(props, 'item') const form = useFormInject() const size = useSize() const createLabel = useCreateLabel(props) - const { colStyle, colClass } = useCol(item) + const { colStyle, colClass } = useCol(props.item) + + const modelProps = computed(() => { + const list = props.item.models ?? [ + props.item.modelKey + ? isArray(props.item.modelKey) + ? { + prop: props.item.prop, + key: props.item.modelKey[0], + event: props.item.modelKey[1], + } + : { prop: props.item.prop, key: props.item.modelKey } + : { prop: props.item.prop, key: 'modelValue' }, + ] + + return list.reduce((all, model) => { + return { + ...all, + [model.key]: get(props.modelValue, model.prop, undefined), + [model.event ?? `onUpdate:${model.key}`]: (value: unknown) => + emit('update:modelValue', set(props.modelValue, model.prop, value)), + } + }, {}) + }) + + if (props.item.modelKey) { + throwWarn( + `[ProForm] The 'modelKey' attribute will be removed in the next major version, please use 'models' instead`, + ) + } function update(value: unknown) { - emit('update:modelValue', set(props.modelValue, item.value.prop, value)) + emit('update:modelValue', set(props.modelValue, props.item.prop, value)) } function createError(scope: UnknownObject) { - if (form?.slots[`form-${item.value.prop}-error`]) { - return (form?.slots[`form-${item.value.prop}-error`] as Slot)({ + if (form?.slots[`form-${props.item.prop}-error`]) { + return (form?.slots[`form-${props.item.prop}-error`] as Slot)({ ...scope, - item: item.value, + item: props.item, indexes: props.indexes, }) } } function createDefault() { - !has(props.modelValue, item.value.prop) && update(undefined) - const currentValue = get(props.modelValue, item.value.prop, undefined) + !has(props.modelValue, props.item.prop) && update(undefined) + const currentValue = get(props.modelValue, props.item.prop, undefined) let list: VNode[] = [] if (props.item.children?.length) { @@ -51,24 +80,23 @@ export default defineComponent({ 'onUpdate:modelValue': update, }), ) - } else if (form?.slots[`form-${item.value.prop}`]) { + } else if (form?.slots[`form-${props.item.prop}`]) { list = list.concat( - (form?.slots[`form-${item.value.prop}`] as Slot)({ - item: item.value, + (form?.slots[`form-${props.item.prop}`] as Slot)({ + ...modelProps.value, + item: props.item, indexes: props.indexes, value: currentValue, setValue: update, }), ) - } else if (item.value.component) { + } else if (props.item.component) { list.push( h( ProFormComponent, - mergeProps(item.value.props || {}, { - is: item.value.component, - modelValue: currentValue, - modelKey: item.value.modelKey, - 'onUpdate:modelValue': update, + mergeProps(props.item.props || {}, { + ...modelProps.value, + is: props.item.component, }), ), ) @@ -78,17 +106,17 @@ export default defineComponent({ } return () => - item.value.show === false + props.item.show === false ? null : h( ElFormItem, - mergeProps({ size: size.value }, getFormItemBind(item.value), { + mergeProps({ size: size.value }, getFormItemBind(props.item), { prop: props.prefix, style: !form?.props.inline ? colStyle.value : undefined, class: ['pro-form-item', !form?.props.inline && colClass.value], }), { - label: () => createLabel(item.value), + label: () => createLabel(props.item), error: (scope: UnknownObject) => createError(scope), default: () => createDefault(), }, diff --git a/src/Form/index.test.ts b/src/Form/index.test.ts index 8d0ab603..a5247944 100644 --- a/src/Form/index.test.ts +++ b/src/Form/index.test.ts @@ -29,20 +29,32 @@ import type { ExternalParam, Mutable } from '../types/index' const MyInput = defineComponent({ name: 'MyInput', props: { - value: { + start: { + type: String, + default: '', + }, + end: { type: String, default: '', }, }, - emits: ['update:value'], + emits: ['update:start', 'update:end'], setup(props, { emit }) { return () => - h('input', { - class: 'my-input', - value: props.value, - onInput: (e: { target: { value: string } }) => - emit('update:value', e.target.value), - }) + h('div', [ + h('input', { + class: 'my-input', + value: props.start, + onInput: (e: { target: { value: string } }) => + emit('update:start', e.target.value), + }), + h('input', { + class: 'my-input-end', + value: props.end, + onInput: (e: { target: { value: string } }) => + emit('update:end', e.target.value), + }), + ]) }, }) @@ -234,21 +246,25 @@ describe('ProFormComponent', () => { expect(wrapper.find('div').text()).toBe('slots') }) - test.concurrent('modelKey', async () => { + test.concurrent('binding multiple arguments', async () => { const wrapper = await mount({ template: - '', + '', setup() { - const form = ref() + const form = ref({}) return { form } }, }) expect(wrapper.find('.my-input').exists()).toBe(true) + expect(wrapper.find('.my-input-end').exists()).toBe(true) - await wrapper.find('.my-input').setValue('value') - expect(wrapper.find('input').element.value).toBe('value') - expect(wrapper.vm.form).toBe('value') + await wrapper.find('.my-input').setValue('start') + expect(wrapper.find('input').element.value).toBe('start') + expect(wrapper.vm.form).toEqual({ start: 'start' }) + + await wrapper.find('.my-input-end').setValue('end') + expect(wrapper.vm.form).toEqual({ start: 'start', end: 'end' }) }) }) @@ -338,18 +354,21 @@ describe('ProFormItem', () => { expect(wrapper.find('.el-input').exists()).toBe(false) }) - test.concurrent('modelKey', async () => { + test.concurrent('models', async () => { const wrapper = await mount({ template: '', setup() { - const form = ref() + const form = ref({}) const columns = ref( defineFormColumns([ { label: 'input', prop: 'input', component: 'my-input', - modelKey: 'value', + models: [ + { prop: 'start', key: 'start' }, + { prop: 'end', key: 'end' }, + ], }, ]), ) @@ -359,21 +378,20 @@ describe('ProFormItem', () => { const vm = wrapper.vm as unknown as { columns: IFormColumns } expect(wrapper.find('.my-input').exists()).toBe(true) + expect(wrapper.find('.my-input-end').exists()).toBe(true) expect(wrapper.find('.el-form-item__label').text()).toBe('input') - await wrapper.find('.my-input').setValue('value') - expect(wrapper.find('input').element.value).toBe('value') - expect(wrapper.vm.form).toEqual({ input: 'value' }) + await wrapper.find('.my-input').setValue('start') + expect(wrapper.find('input').element.value).toBe('start') + expect(wrapper.vm.form).toEqual({ start: 'start' }) + + await wrapper.find('.my-input-end').setValue('end') + expect(wrapper.vm.form).toMatchObject({ end: 'end' }) - await (vm.columns[0].modelKey = 'modelValue') + await (vm.columns[0].models = undefined) await wrapper.find('.my-input').setValue('modelValue') expect(wrapper.find('input').element.value).toBe('modelValue') - expect(wrapper.vm.form).not.toEqual({ input: 'modelValue' }) - - await (vm.columns[0].modelKey = ['value', 'onUpdate:value']) - await wrapper.find('.my-input').setValue('my-input') - expect(wrapper.find('input').element.value).toBe('my-input') - expect(wrapper.vm.form).toEqual({ input: 'my-input' }) + expect(wrapper.vm.form).not.toMatchObject({ input: 'modelValue' }) }) }) @@ -1567,6 +1585,7 @@ describe('Form', () => { input: string switch: boolean input1: string + end?: string } const wrapper = mount({ @@ -1576,6 +1595,7 @@ describe('Form', () => { input: '123', switch: false, input1: 'abc', + end: undefined, }) const columns: IFormColumns
= [ { @@ -1592,14 +1612,17 @@ describe('Form', () => { label: 'MyInput', prop: 'input1', component: 'my-input', - modelKey: 'value', + models: [ + { prop: 'input1', key: 'start' }, + { prop: 'end', key: 'end' }, + ], }, ] return { form, columns } }, }) const vm = wrapper.vm as unknown as { - form: { input: string; switch: boolean; input1: string } + form: Form columns: IFormColumns } @@ -1620,6 +1643,9 @@ describe('Form', () => { await wrapper.find('input.my-input').setValue('value1') expect(vm.form.input1).toBe('value1') + await wrapper.find('input.my-input-end').setValue('value2') + expect(vm.form.end).toBe('value2') + await (vm.form = { input: 'input', switch: false, @@ -1630,6 +1656,9 @@ describe('Form', () => { expect( (wrapper.find('input.my-input').element as HTMLInputElement).value, ).toBe('input1') + expect( + (wrapper.find('input.my-input-end').element as HTMLInputElement).value, + ).toBe('') }) test.concurrent('menu', async () => { diff --git a/src/Form/props.ts b/src/Form/props.ts index 5e8ca5dd..94f50c44 100644 --- a/src/Form/props.ts +++ b/src/Form/props.ts @@ -53,12 +53,10 @@ export const formItemProps = { } export const formComponentProps = { - modelValue: null, is: { type: [String, Object] as PropType, require: true, }, - modelKey: [String, Array] as PropType, slots: [Function, Object, String] as PropType, } diff --git a/src/Form/type.ts b/src/Form/type.ts index dd6fb514..9c54fb82 100644 --- a/src/Form/type.ts +++ b/src/Form/type.ts @@ -44,8 +44,6 @@ export interface FormColumn Partial> { /** component name */ component?: string | ColumnComponent - /** bind v-model arguments, default modelValue */ - modelKey?: string /** props for component */ props?: UnknownObject & { slots?: ColumnPropsSlots } /** the type of sub-form */ @@ -58,6 +56,21 @@ export interface FormColumn prop: ColumnProp /** whether to display the current column */ show?: boolean + /** + * @deprecated The `modelKey` attribute will be removed in the next major version, please use `models` instead + * + * bind v-model arguments, default modelValue + */ + modelKey?: string | [string, string] + /** bind v-model arguments, support binding multiple arguments */ + models?: Array<{ + /** keys of model that passed to form */ + prop: string + /** bind v-model arguments */ + key: string + /** bind v-model event, default `onUpdate:${key}` */ + event?: string + }> } export type GroupFormType = 'group' | 'tabs' | 'collapse' | 'steps'