Commit a065de4fbc8745323105ff5ba9032108dab91d24
Committed by
GitHub
1 parent
fa5ecb09
feat: Form 自定义组件渲染 新增 opts: {disabled} 用于自定义渲染判断 示例: /comp/form/customerForm页面 (#2944)
* feat: Form 自定义组件渲染 示例: /comp/form/customerForm页面 1. 针对自定义渲染功能 FormSchema 中 render, renderColContent, renderComponentContent 新增 opts:{disabled} 扩展 帮助自定义渲染时做 条件判断、展示同步 渲染: ((renderCallbackParams) => any) ===> ((renderCallbackParams, opts) => any) 2. slot, colSlot 分别是 render, renderColContent 插槽,为方便插槽使用 slotFn 进行解构 #test={scope} scope==={...data, ...opts} * feat: Form 自定义组件渲染 示例: /comp/form/customerForm页面 1. 针对自定义渲染功能 FormSchema 新增 [fields] 和 [defaultValueObj] 帮助 render, renderColContent 自定义渲染时 存在多个 表单字段操作(复合field 场景)
Showing
6 changed files
with
226 additions
and
29 deletions
src/components/Form/src/components/FormItem.vue
... | ... | @@ -299,7 +299,7 @@ |
299 | 299 | return <Comp {...compAttr} />; |
300 | 300 | } |
301 | 301 | const compSlot = isFunction(renderComponentContent) |
302 | - ? { ...renderComponentContent(unref(getValues)) } | |
302 | + ? { ...renderComponentContent(unref(getValues), { disabled: unref(getDisable) }) } | |
303 | 303 | : { |
304 | 304 | default: () => renderComponentContent, |
305 | 305 | }; |
... | ... | @@ -333,7 +333,7 @@ |
333 | 333 | const { itemProps, slot, render, field, suffix, component } = props.schema; |
334 | 334 | const { labelCol, wrapperCol } = unref(itemLabelWidthProp); |
335 | 335 | const { colon } = props.formProps; |
336 | - | |
336 | + const opts = { disabled: unref(getDisable) }; | |
337 | 337 | if (component === 'Divider') { |
338 | 338 | return ( |
339 | 339 | <Col span={24}> |
... | ... | @@ -343,9 +343,9 @@ |
343 | 343 | } else { |
344 | 344 | const getContent = () => { |
345 | 345 | return slot |
346 | - ? getSlot(slots, slot, unref(getValues)) | |
346 | + ? getSlot(slots, slot, unref(getValues), opts) | |
347 | 347 | : render |
348 | - ? render(unref(getValues)) | |
348 | + ? render(unref(getValues), opts) | |
349 | 349 | : renderComponent(); |
350 | 350 | }; |
351 | 351 | |
... | ... | @@ -391,12 +391,13 @@ |
391 | 391 | const realColProps = { ...baseColProps, ...colProps }; |
392 | 392 | const { isIfShow, isShow } = getShow(); |
393 | 393 | const values = unref(getValues); |
394 | + const opts = { disabled: unref(getDisable) }; | |
394 | 395 | |
395 | 396 | const getContent = () => { |
396 | 397 | return colSlot |
397 | - ? getSlot(slots, colSlot, values) | |
398 | + ? getSlot(slots, colSlot, values, opts) | |
398 | 399 | : renderColContent |
399 | - ? renderColContent(values) | |
400 | + ? renderColContent(values, opts) | |
400 | 401 | : renderItem(); |
401 | 402 | }; |
402 | 403 | ... | ... |
src/components/Form/src/hooks/useFormEvents.ts
... | ... | @@ -87,6 +87,13 @@ export function useFormEvents({ |
87 | 87 | |
88 | 88 | Object.keys(formModel).forEach((key) => { |
89 | 89 | const schema = unref(getSchema).find((item) => item.field === key); |
90 | + const defaultValueObj = schema?.defaultValueObj; | |
91 | + const fieldKeys = Object.keys(defaultValueObj || {}); | |
92 | + if (fieldKeys.length) { | |
93 | + fieldKeys.map((field) => { | |
94 | + formModel[field] = defaultValueObj![field]; | |
95 | + }); | |
96 | + } | |
90 | 97 | const isInput = schema?.component && defaultValueComponents.includes(schema.component); |
91 | 98 | const defaultValue = cloneDeep(defaultValueRef.value[key]); |
92 | 99 | formModel[key] = isInput ? defaultValue || '' : defaultValue; |
... | ... | @@ -96,14 +103,17 @@ export function useFormEvents({ |
96 | 103 | emit('reset', toRaw(formModel)); |
97 | 104 | submitOnReset && handleSubmit(); |
98 | 105 | } |
99 | - | |
106 | + // 获取表单fields | |
107 | + const getAllFields = () => | |
108 | + unref(getSchema) | |
109 | + .map((item) => [...(item.fields || []), item.field]) | |
110 | + .flat(1) | |
111 | + .filter(Boolean); | |
100 | 112 | /** |
101 | 113 | * @description: Set form value |
102 | 114 | */ |
103 | 115 | async function setFieldsValue(values: Recordable): Promise<void> { |
104 | - const fields = unref(getSchema) | |
105 | - .map((item) => item.field) | |
106 | - .filter(Boolean); | |
116 | + const fields = getAllFields(); | |
107 | 117 | |
108 | 118 | // key 支持 a.b.c 的嵌套写法 |
109 | 119 | const delimiter = '.'; |
... | ... | @@ -340,8 +350,14 @@ export function useFormEvents({ |
340 | 350 | return unref(formElRef)?.validateFields(nameList); |
341 | 351 | } |
342 | 352 | |
343 | - async function validate(nameList?: NamePath[] | undefined) { | |
344 | - return await unref(formElRef)?.validate(nameList); | |
353 | + async function validate(nameList?: NamePath[] | false | undefined) { | |
354 | + let _nameList: any; | |
355 | + if (nameList === undefined) { | |
356 | + _nameList = getAllFields(); | |
357 | + } else { | |
358 | + _nameList = nameList === Array.isArray(nameList) ? nameList : undefined; | |
359 | + } | |
360 | + return await unref(formElRef)?.validate(_nameList); | |
345 | 361 | } |
346 | 362 | |
347 | 363 | async function clearValidate(name?: string | string[]) { | ... | ... |
src/components/Form/src/hooks/useFormValues.ts
... | ... | @@ -127,7 +127,16 @@ export function useFormValues({ |
127 | 127 | const schemas = unref(getSchema); |
128 | 128 | const obj: Recordable = {}; |
129 | 129 | schemas.forEach((item) => { |
130 | - const { defaultValue } = item; | |
130 | + const { defaultValue, defaultValueObj } = item; | |
131 | + const fieldKeys = Object.keys(defaultValueObj || {}); | |
132 | + if (fieldKeys.length) { | |
133 | + fieldKeys.map((field) => { | |
134 | + obj[field] = defaultValueObj![field]; | |
135 | + if (formModel[field] === undefined) { | |
136 | + formModel[field] = defaultValueObj![field]; | |
137 | + } | |
138 | + }); | |
139 | + } | |
131 | 140 | if (!isNullOrUnDef(defaultValue)) { |
132 | 141 | obj[item.field] = defaultValue; |
133 | 142 | ... | ... |
src/components/Form/src/types/form.ts
... | ... | @@ -39,7 +39,7 @@ export interface FormActionType { |
39 | 39 | first?: boolean | undefined, |
40 | 40 | ) => Promise<void>; |
41 | 41 | validateFields: (nameList?: NamePath[]) => Promise<any>; |
42 | - validate: (nameList?: NamePath[]) => Promise<any>; | |
42 | + validate: (nameList?: NamePath[] | false) => Promise<any>; | |
43 | 43 | scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>; |
44 | 44 | } |
45 | 45 | |
... | ... | @@ -123,15 +123,21 @@ export interface FormProps { |
123 | 123 | transformDateFunc?: (date: any) => string; |
124 | 124 | colon?: boolean; |
125 | 125 | } |
126 | +export type RenderOpts = { | |
127 | + disabled: boolean; | |
128 | + [key: string]: any; | |
129 | +}; | |
126 | 130 | export interface FormSchema { |
127 | 131 | // Field name |
128 | 132 | field: string; |
133 | + // Extra Fields name[] | |
134 | + fields?: string[]; | |
129 | 135 | // Event name triggered by internal value change, default change |
130 | 136 | changeEvent?: string; |
131 | 137 | // Variable name bound to v-model Default value |
132 | 138 | valueField?: string; |
133 | 139 | // Label name |
134 | - label: string | VNode; | |
140 | + label?: string | VNode; | |
135 | 141 | // Auxiliary text |
136 | 142 | subLabel?: string; |
137 | 143 | // Help text on the right side of the text |
... | ... | @@ -175,6 +181,9 @@ export interface FormSchema { |
175 | 181 | // 默认值 |
176 | 182 | defaultValue?: any; |
177 | 183 | |
184 | + // 额外默认值数组对象 | |
185 | + defaultValueObj?: { [key: string]: any }; | |
186 | + | |
178 | 187 | // 是否自动处理与时间相关组件的默认值 |
179 | 188 | isHandleDateDefaultValue?: boolean; |
180 | 189 | |
... | ... | @@ -188,13 +197,19 @@ export interface FormSchema { |
188 | 197 | show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); |
189 | 198 | |
190 | 199 | // Render the content in the form-item tag |
191 | - render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string; | |
200 | + render?: ( | |
201 | + renderCallbackParams: RenderCallbackParams, | |
202 | + opts: RenderOpts, | |
203 | + ) => VNode | VNode[] | string; | |
192 | 204 | |
193 | 205 | // Rendering col content requires outer wrapper form-item |
194 | - renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string; | |
206 | + renderColContent?: ( | |
207 | + renderCallbackParams: RenderCallbackParams, | |
208 | + opts: RenderOpts, | |
209 | + ) => VNode | VNode[] | string; | |
195 | 210 | |
196 | 211 | renderComponentContent?: |
197 | - | ((renderCallbackParams: RenderCallbackParams) => any) | |
212 | + | ((renderCallbackParams: RenderCallbackParams, opts: RenderOpts) => any) | |
198 | 213 | | VNode |
199 | 214 | | VNode[] |
200 | 215 | | string; | ... | ... |
src/utils/helper/tsxHelper.tsx
1 | 1 | import { Slots } from 'vue'; |
2 | 2 | import { isFunction } from '/@/utils/is'; |
3 | +import { RenderOpts } from '/@/components/Form'; | |
3 | 4 | |
4 | 5 | /** |
5 | 6 | * @description: Get slot to prevent empty error |
6 | 7 | */ |
7 | -export function getSlot(slots: Slots, slot = 'default', data?: any) { | |
8 | +export function getSlot(slots: Slots, slot = 'default', data?: any, opts?: RenderOpts) { | |
8 | 9 | if (!slots || !Reflect.has(slots, slot)) { |
9 | 10 | return null; |
10 | 11 | } |
... | ... | @@ -14,7 +15,8 @@ export function getSlot(slots: Slots, slot = 'default', data?: any) { |
14 | 15 | } |
15 | 16 | const slotFn = slots[slot]; |
16 | 17 | if (!slotFn) return null; |
17 | - return slotFn(data); | |
18 | + const params = { ...data, ...opts }; | |
19 | + return slotFn(params); | |
18 | 20 | } |
19 | 21 | |
20 | 22 | /** | ... | ... |
src/views/demo/form/CustomerForm.vue
... | ... | @@ -2,21 +2,42 @@ |
2 | 2 | <PageWrapper title="自定义组件示例"> |
3 | 3 | <CollapseContainer title="自定义表单"> |
4 | 4 | <BasicForm @register="register" @submit="handleSubmit"> |
5 | - <template #f3="{ model, field }"> | |
6 | - <a-input v-model:value="model[field]" placeholder="自定义slot" /> | |
5 | + <template #f3="{ model, field, disabled }"> | |
6 | + <a-input v-model:value="model[field]" :disabled="disabled" placeholder="自定义slot" /> | |
7 | + </template> | |
8 | + <template #colSlot_field5="{ model, field, disabled }"> | |
9 | + <FormItem :name="field" label="自定义colSlot" :rules="[{ required: true }]"> | |
10 | + <a-input | |
11 | + v-model:value="model[field]" | |
12 | + :disabled="disabled" | |
13 | + placeholder="自定义colSlot" | |
14 | + /> | |
15 | + </FormItem> | |
7 | 16 | </template> |
8 | 17 | </BasicForm> |
9 | 18 | </CollapseContainer> |
10 | 19 | </PageWrapper> |
11 | 20 | </template> |
12 | -<script lang="ts"> | |
21 | +<script lang="tsx"> | |
13 | 22 | import { defineComponent, h } from 'vue'; |
14 | 23 | import { BasicForm, FormSchema, useForm } from '/@/components/Form/index'; |
15 | 24 | import { CollapseContainer } from '/@/components/Container/index'; |
16 | 25 | import { useMessage } from '/@/hooks/web/useMessage'; |
17 | - import { Input } from 'ant-design-vue'; | |
26 | + import { Input, FormItem, Select } from 'ant-design-vue'; | |
18 | 27 | import { PageWrapper } from '/@/components/Page'; |
19 | 28 | |
29 | + const custom_typeKey2typeValueRules = (model) => { | |
30 | + return [ | |
31 | + { | |
32 | + required: true, | |
33 | + validator: (rule, value, callback) => { | |
34 | + if (!model.typeKey) return callback('请选择类型'); | |
35 | + if (!model.typeValue) return callback('请输入数据'); | |
36 | + callback(); | |
37 | + }, | |
38 | + }, | |
39 | + ]; | |
40 | + }; | |
20 | 41 | const schemas: FormSchema[] = [ |
21 | 42 | { |
22 | 43 | field: 'field1', |
... | ... | @@ -25,14 +46,18 @@ |
25 | 46 | colProps: { |
26 | 47 | span: 8, |
27 | 48 | }, |
49 | + dynamicDisabled: ({ values }) => { | |
50 | + return !!values.field_disabled; | |
51 | + }, | |
28 | 52 | rules: [{ required: true }], |
29 | - render: ({ model, field }) => { | |
53 | + render: ({ model, field }, { disabled }) => { | |
30 | 54 | return h(Input, { |
31 | 55 | placeholder: '请输入', |
32 | 56 | value: model[field], |
33 | - onChange: (e: ChangeEvent) => { | |
57 | + onChange: (e) => { | |
34 | 58 | model[field] = e.target.value; |
35 | 59 | }, |
60 | + disabled, | |
36 | 61 | }); |
37 | 62 | }, |
38 | 63 | }, |
... | ... | @@ -43,10 +68,13 @@ |
43 | 68 | colProps: { |
44 | 69 | span: 8, |
45 | 70 | }, |
71 | + dynamicDisabled: ({ values }) => { | |
72 | + return !!values.field_disabled; | |
73 | + }, | |
46 | 74 | rules: [{ required: true }], |
47 | - renderComponentContent: () => { | |
75 | + renderComponentContent: (_, { disabled }) => { | |
48 | 76 | return { |
49 | - suffix: () => 'suffix', | |
77 | + suffix: () => (disabled ? 'suffix_disabled' : 'suffix_default'), | |
50 | 78 | }; |
51 | 79 | }, |
52 | 80 | }, |
... | ... | @@ -58,11 +86,136 @@ |
58 | 86 | colProps: { |
59 | 87 | span: 8, |
60 | 88 | }, |
89 | + dynamicDisabled: ({ values }) => { | |
90 | + return !!values.field_disabled; | |
91 | + }, | |
61 | 92 | rules: [{ required: true }], |
62 | 93 | }, |
94 | + { | |
95 | + field: 'field4', | |
96 | + component: 'Input', | |
97 | + // label: 'renderColContent渲染', | |
98 | + /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/ | |
99 | + renderColContent({ model, field }, { disabled }) { | |
100 | + return ( | |
101 | + <FormItem name="field4" label="renderColContent渲染" rules={[{ required: true }]}> | |
102 | + <Input placeholder="请输入" v-model:value={model[field]} disabled={disabled}></Input> | |
103 | + </FormItem> | |
104 | + ); | |
105 | + }, | |
106 | + colProps: { | |
107 | + span: 8, | |
108 | + }, | |
109 | + dynamicDisabled: ({ values }) => { | |
110 | + return !!values.field_disabled; | |
111 | + }, | |
112 | + }, | |
113 | + { | |
114 | + field: 'field5', | |
115 | + component: 'Input', | |
116 | + label: '自定义colSlot', | |
117 | + /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/ | |
118 | + colSlot: 'colSlot_field5', | |
119 | + colProps: { | |
120 | + span: 8, | |
121 | + }, | |
122 | + dynamicDisabled: ({ values }) => { | |
123 | + return !!values.field_disabled; | |
124 | + }, | |
125 | + }, | |
126 | + // 复合field 场景 自定义表单控件 一个控件包含多个表单录入 示例: 选择+输入 | |
127 | + { | |
128 | + required: true, | |
129 | + field: 'typeKey2', | |
130 | + defaultValue: '测试类型', | |
131 | + fields: ['typeValue2'], | |
132 | + defaultValueObj: { typeValue2: '默认测试_文字' }, | |
133 | + component: 'Input', | |
134 | + label: '复合field render', | |
135 | + render({ model, field }, { disabled }) { | |
136 | + return ( | |
137 | + <Input.Group compact> | |
138 | + <Select | |
139 | + disabled={disabled} | |
140 | + style="width: 120px" | |
141 | + allowClear | |
142 | + v-model:value={model[field]} | |
143 | + > | |
144 | + <Select.Option value="测试类型">测试类型</Select.Option> | |
145 | + <Select.Option value="测试名称">测试名称</Select.Option> | |
146 | + </Select> | |
147 | + <FormItem | |
148 | + name="typeValue2" | |
149 | + style="width: calc(100% - 120px); margin-left: -1px; border-right: 0; margin-bottom: 0;" | |
150 | + rules={[{ required: true }]} | |
151 | + > | |
152 | + <Input placeholder="请输入" v-model:value={model['typeValue2']} disabled={disabled} /> | |
153 | + </FormItem> | |
154 | + </Input.Group> | |
155 | + ); | |
156 | + }, | |
157 | + colProps: { | |
158 | + span: 8, | |
159 | + }, | |
160 | + dynamicDisabled: ({ values }) => { | |
161 | + return !!values.field_disabled; | |
162 | + }, | |
163 | + }, | |
164 | + // 复合field 场景 自定义表单控件 一个控件包含多个表单录入 示例: 选择+输入 | |
165 | + { | |
166 | + field: 'typeKey', | |
167 | + defaultValue: '公司名称', | |
168 | + fields: ['typeValue'], | |
169 | + defaultValueObj: { typeValue: '默认文字' }, | |
170 | + component: 'Input', | |
171 | + // label: 'renderColContent渲染', | |
172 | + /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/ | |
173 | + renderColContent({ model, field }, { disabled }) { | |
174 | + return ( | |
175 | + <FormItem | |
176 | + name="typeKey" | |
177 | + label="复合field renderColContent" | |
178 | + rules={custom_typeKey2typeValueRules(model)} | |
179 | + > | |
180 | + <Input.Group compact> | |
181 | + <Select | |
182 | + allowClear | |
183 | + disabled={disabled} | |
184 | + style="width: 120px" | |
185 | + v-model:value={model[field]} | |
186 | + > | |
187 | + <Select.Option value="公司名称">公司名称</Select.Option> | |
188 | + <Select.Option value="产品名称">产品名称</Select.Option> | |
189 | + </Select> | |
190 | + <Input | |
191 | + style="width: calc(100% - 120px); margin-left: -1px;" | |
192 | + placeholder="请输入" | |
193 | + v-model:value={model['typeValue']} | |
194 | + disabled={disabled} | |
195 | + /> | |
196 | + </Input.Group> | |
197 | + </FormItem> | |
198 | + ); | |
199 | + }, | |
200 | + colProps: { | |
201 | + span: 16, | |
202 | + }, | |
203 | + dynamicDisabled: ({ values }) => { | |
204 | + return !!values.field_disabled; | |
205 | + }, | |
206 | + }, | |
207 | + { | |
208 | + field: 'field_disabled', | |
209 | + component: 'Switch', | |
210 | + label: '是否禁用 编辑字段', | |
211 | + colProps: { | |
212 | + span: 8, | |
213 | + }, | |
214 | + labelWidth: 200, | |
215 | + }, | |
63 | 216 | ]; |
64 | 217 | export default defineComponent({ |
65 | - components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input }, | |
218 | + components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input, FormItem }, | |
66 | 219 | setup() { |
67 | 220 | const { createMessage } = useMessage(); |
68 | 221 | const [register, { setProps }] = useForm({ |
... | ... | @@ -76,6 +229,7 @@ |
76 | 229 | register, |
77 | 230 | schemas, |
78 | 231 | handleSubmit: (values: any) => { |
232 | + console.log('submit values', values); | |
79 | 233 | createMessage.success('click search,values:' + JSON.stringify(values)); |
80 | 234 | }, |
81 | 235 | setProps, | ... | ... |