Skip to content

Commit 04dff33

Browse files
committed
feat: improved formApi for component instance support
* 改进表单API以支持组件实例的获取,以及焦点字段的获取
1 parent cfa18c2 commit 04dff33

File tree

7 files changed

+119
-24
lines changed

7 files changed

+119
-24
lines changed

docs/src/components/common-ui/vben-form.md

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -279,22 +279,24 @@ const [Form, formApi] = useVbenForm({
279279

280280
useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
281281

282-
| 方法名 | 描述 | 类型 |
283-
| --- | --- | --- |
284-
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
285-
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` |
286-
| resetForm | 重置表单 | `()=>Promise<void>` |
287-
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
288-
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
289-
| validate | 表单校验 | `()=>Promise<void>` |
290-
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
291-
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
292-
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
293-
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
294-
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
295-
| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` |
296-
| getState | 获取组件状态(props) | `()=>Promise<VbenFormProps>` |
297-
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - |
282+
| 方法名 | 描述 | 类型 | 版本号 |
283+
| --- | --- | --- | --- |
284+
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
285+
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
286+
| resetForm | 重置表单 | `()=>Promise<void>` | - |
287+
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
288+
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` | - |
289+
| validate | 表单校验 | `()=>Promise<void>` | - |
290+
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` | - |
291+
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
292+
| resetValidate | 重置表单校验 | `()=>Promise<void>` | - |
293+
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
294+
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` | - |
295+
| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` | - |
296+
| getState | 获取组件状态(props) | `()=>Promise<VbenFormProps>` | - |
297+
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
298+
| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
299+
| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
298300

299301
## Props
300302

packages/@core/ui-kit/form-ui/src/form-api.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
ValidationOptions,
66
} from 'vee-validate';
77

8+
import type { ComponentPublicInstance } from 'vue';
9+
810
import type { Recordable } from '@vben-core/typings';
911

1012
import type { FormActions, FormSchema, VbenFormProps } from './types';
@@ -56,6 +58,11 @@ export class FormApi {
5658

5759
public store: Store<VbenFormProps>;
5860

61+
/**
62+
* 组件实例映射
63+
*/
64+
private componentRefMap: Map<string, unknown> = new Map();
65+
5966
// 最后一次点击提交时的表单值
6067
private latestSubmissionValues: null | Recordable<any> = null;
6168

@@ -85,6 +92,46 @@ export class FormApi {
8592
bindMethods(this);
8693
}
8794

95+
/**
96+
* 获取字段组件实例
97+
* @param fieldName 字段名
98+
* @returns 组件实例
99+
*/
100+
getFieldComponentRef<T = ComponentPublicInstance>(
101+
fieldName: string,
102+
): T | undefined {
103+
return this.componentRefMap.has(fieldName)
104+
? (this.componentRefMap.get(fieldName) as T)
105+
: undefined;
106+
}
107+
108+
/**
109+
* 获取当前聚焦的字段,如果没有聚焦的字段则返回undefined
110+
*/
111+
getFocusedField() {
112+
for (const fieldName of this.componentRefMap.keys()) {
113+
const ref = this.getFieldComponentRef(fieldName);
114+
if (ref) {
115+
let el: HTMLElement | null = null;
116+
if (ref instanceof HTMLElement) {
117+
el = ref;
118+
} else if (ref.$el instanceof HTMLElement) {
119+
el = ref.$el;
120+
}
121+
if (!el) {
122+
continue;
123+
}
124+
if (
125+
el === document.activeElement ||
126+
el.contains(document.activeElement)
127+
) {
128+
return fieldName;
129+
}
130+
}
131+
}
132+
return undefined;
133+
}
134+
88135
getLatestSubmissionValues() {
89136
return this.latestSubmissionValues || {};
90137
}
@@ -143,13 +190,14 @@ export class FormApi {
143190
return proxy;
144191
}
145192

146-
mount(formActions: FormActions) {
193+
mount(formActions: FormActions, componentRefMap: Map<string, unknown>) {
147194
if (!this.isMounted) {
148195
Object.assign(this.form, formActions);
149196
this.stateHandler.setConditionTrue();
150197
this.setLatestSubmissionValues({
151198
...toRaw(this.handleRangeTimeValue(this.form.values)),
152199
});
200+
this.componentRefMap = componentRefMap;
153201
this.isMounted = true;
154202
}
155203
}

packages/@core/ui-kit/form-ui/src/form-render/form-field.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ZodType } from 'zod';
33
44
import type { FormSchema, MaybeComponentProps } from '../types';
55
6-
import { computed, nextTick, useTemplateRef, watch } from 'vue';
6+
import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue';
77
88
import {
99
FormControl,
@@ -18,6 +18,7 @@ import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils';
1818
import { toTypedSchema } from '@vee-validate/zod';
1919
import { useFieldError, useFormValues } from 'vee-validate';
2020
21+
import { injectComponentRefMap } from '../use-form-context';
2122
import { injectRenderFormProps, useFormContext } from './context';
2223
import useDependencies from './dependencies';
2324
import FormLabel from './form-label.vue';
@@ -267,6 +268,15 @@ function autofocus() {
267268
fieldComponentRef.value?.focus?.();
268269
}
269270
}
271+
const componentRefMap = injectComponentRefMap();
272+
watch(fieldComponentRef, (componentRef) => {
273+
componentRefMap?.set(fieldName, componentRef);
274+
});
275+
onUnmounted(() => {
276+
if (componentRefMap?.has(fieldName)) {
277+
componentRefMap.delete(fieldName);
278+
}
279+
});
270280
</script>
271281

272282
<template>

packages/@core/ui-kit/form-ui/src/use-form-context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export const [injectFormProps, provideFormProps] =
2020
'VbenFormProps',
2121
);
2222

23+
export const [injectComponentRefMap, provideComponentRefMap] =
24+
createContext<Map<string, unknown>>('ComponentRefMap');
25+
2326
export function useFormInitial(
2427
props: ComputedRef<VbenFormProps> | VbenFormProps,
2528
) {

packages/@core/ui-kit/form-ui/src/vben-use-form.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import {
1717
DEFAULT_FORM_COMMON_CONFIG,
1818
} from './config';
1919
import { Form } from './form-render';
20-
import { provideFormProps, useFormInitial } from './use-form-context';
20+
import {
21+
provideComponentRefMap,
22+
provideFormProps,
23+
useFormInitial,
24+
} from './use-form-context';
2125
// 通过 extends 会导致热更新卡死,所以重复写了一遍
2226
interface Props extends VbenFormProps {
2327
formApi: ExtendedFormApi;
@@ -29,11 +33,14 @@ const state = props.formApi?.useStore?.();
2933
3034
const forward = useForwardPriorityValues(props, state);
3135
36+
const componentRefMap = new Map<string, unknown>();
37+
3238
const { delegatedSlots, form } = useFormInitial(forward);
3339
3440
provideFormProps([forward, form]);
41+
provideComponentRefMap(componentRefMap);
3542
36-
props.formApi?.mount?.(form);
43+
props.formApi?.mount?.(form, componentRefMap);
3744
3845
const handleUpdateCollapsed = (value: boolean) => {
3946
props.formApi?.setState({ collapsed: !!value });

playground/src/views/_core/authentication/login.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts" setup>
22
import type { VbenFormSchema } from '@vben/common-ui';
3-
import type { BasicOption } from '@vben/types';
3+
import type { BasicOption, Recordable } from '@vben/types';
44
5-
import { computed, markRaw } from 'vue';
5+
import { computed, markRaw, useTemplateRef } from 'vue';
66
77
import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
88
import { $t } from '@vben/locales';
@@ -104,12 +104,28 @@ const formSchema = computed((): VbenFormSchema[] => {
104104
},
105105
];
106106
});
107+
108+
const loginRef =
109+
useTemplateRef<InstanceType<typeof AuthenticationLogin>>('loginRef');
110+
111+
async function onSubmit(params: Recordable<any>) {
112+
authStore.authLogin(params).catch(() => {
113+
// 登陆失败,刷新验证码的演示
114+
115+
// 使用表单API获取验证码组件实例,并调用其resume方法来重置验证码
116+
loginRef.value
117+
?.getFormApi()
118+
?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
119+
?.resume();
120+
});
121+
}
107122
</script>
108123

109124
<template>
110125
<AuthenticationLogin
126+
ref="loginRef"
111127
:form-schema="formSchema"
112128
:loading="authStore.loginLoading"
113-
@submit="authStore.authLogin"
129+
@submit="onSubmit"
114130
/>
115131
</template>

playground/src/views/examples/form/api.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts" setup>
2+
import type { RefSelectProps } from 'ant-design-vue/es/select';
3+
24
import { ref } from 'vue';
35
46
import { Page } from '@vben/common-ui';
@@ -82,6 +84,7 @@ function handleClick(
8284
action:
8385
| 'batchAddSchema'
8486
| 'batchDeleteSchema'
87+
| 'componentRef'
8588
| 'disabled'
8689
| 'hiddenAction'
8790
| 'hiddenResetButton'
@@ -129,6 +132,11 @@ function handleClick(
129132
});
130133
break;
131134
}
135+
case 'componentRef': {
136+
// 获取下拉组件的实例,并调用它的focus方法
137+
formApi.getFieldComponentRef<RefSelectProps>('fieldOptions')?.focus();
138+
break;
139+
}
132140
case 'disabled': {
133141
formApi.setState({ commonConfig: { disabled: true } });
134142
break;
@@ -182,14 +190,14 @@ function handleClick(
182190
formApi.setState({ submitButtonOptions: { show: true } });
183191
break;
184192
}
193+
185194
case 'updateActionAlign': {
186195
formApi.setState({
187196
// 可以自行调整class
188197
actionWrapperClass: 'text-center',
189198
});
190199
break;
191200
}
192-
193201
case 'updateResetButton': {
194202
formApi.setState({
195203
resetButtonOptions: { disabled: true },
@@ -257,6 +265,7 @@ function handleClick(
257265
<Button @click="handleClick('batchDeleteSchema')">
258266
批量删除表单项
259267
</Button>
268+
<Button @click="handleClick('componentRef')">下拉组件获取焦点</Button>
260269
</Space>
261270
<Card title="操作示例">
262271
<BaseForm />

0 commit comments

Comments
 (0)