From 04dff33ac5b3742db235adbd54049c7d3a2756f0 Mon Sep 17 00:00:00 2001 From: Netfan Date: Mon, 10 Mar 2025 02:24:58 +0800 Subject: [PATCH] feat: improved formApi for component instance support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 改进表单API以支持组件实例的获取,以及焦点字段的获取 --- docs/src/components/common-ui/vben-form.md | 34 +++++++------ packages/@core/ui-kit/form-ui/src/form-api.ts | 50 ++++++++++++++++++- .../form-ui/src/form-render/form-field.vue | 12 ++++- .../ui-kit/form-ui/src/use-form-context.ts | 3 ++ .../ui-kit/form-ui/src/vben-use-form.vue | 11 +++- .../src/views/_core/authentication/login.vue | 22 ++++++-- playground/src/views/examples/form/api.vue | 11 +++- 7 files changed, 119 insertions(+), 24 deletions(-) diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index 9babb67a..a18ee42d 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -279,22 +279,24 @@ const [Form, formApi] = useVbenForm({ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。 -| 方法名 | 描述 | 类型 | -| --- | --- | --- | -| submitForm | 提交表单 | `(e:Event)=>Promise>` | -| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise>` | -| resetForm | 重置表单 | `()=>Promise` | -| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record, filterFields?: boolean, shouldValidate?: boolean) => Promise` | -| getValues | 获取表单值 | `(fields:Record,shouldValidate: boolean = false)=>Promise` | -| validate | 表单校验 | `()=>Promise` | -| validateField | 校验指定字段 | `(fieldName: string)=>Promise>` | -| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise` | -| resetValidate | 重置表单校验 | `()=>Promise` | -| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | -| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise` | -| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial)\| Partial)=>Promise` | -| getState | 获取组件状态(props) | `()=>Promise` | -| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | +| 方法名 | 描述 | 类型 | 版本号 | +| --- | --- | --- | --- | +| submitForm | 提交表单 | `(e:Event)=>Promise>` | - | +| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise>` | - | +| resetForm | 重置表单 | `()=>Promise` | - | +| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record, filterFields?: boolean, shouldValidate?: boolean) => Promise` | - | +| getValues | 获取表单值 | `(fields:Record,shouldValidate: boolean = false)=>Promise` | - | +| validate | 表单校验 | `()=>Promise` | - | +| validateField | 校验指定字段 | `(fieldName: string)=>Promise>` | - | +| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise` | - | +| resetValidate | 重置表单校验 | `()=>Promise` | - | +| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - | +| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise` | - | +| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial)\| Partial)=>Promise` | - | +| getState | 获取组件状态(props) | `()=>Promise` | - | +| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - | +| getFieldComponentRef | 获取指定字段的组件实例 | `(fieldName: string)=>T` | >5.5.3 | +| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 | ## Props diff --git a/packages/@core/ui-kit/form-ui/src/form-api.ts b/packages/@core/ui-kit/form-ui/src/form-api.ts index db09d7c7..6a440368 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -5,6 +5,8 @@ import type { ValidationOptions, } from 'vee-validate'; +import type { ComponentPublicInstance } from 'vue'; + import type { Recordable } from '@vben-core/typings'; import type { FormActions, FormSchema, VbenFormProps } from './types'; @@ -56,6 +58,11 @@ export class FormApi { public store: Store; + /** + * 组件实例映射 + */ + private componentRefMap: Map = new Map(); + // 最后一次点击提交时的表单值 private latestSubmissionValues: null | Recordable = null; @@ -85,6 +92,46 @@ export class FormApi { bindMethods(this); } + /** + * 获取字段组件实例 + * @param fieldName 字段名 + * @returns 组件实例 + */ + getFieldComponentRef( + fieldName: string, + ): T | undefined { + return this.componentRefMap.has(fieldName) + ? (this.componentRefMap.get(fieldName) as T) + : undefined; + } + + /** + * 获取当前聚焦的字段,如果没有聚焦的字段则返回undefined + */ + getFocusedField() { + for (const fieldName of this.componentRefMap.keys()) { + const ref = this.getFieldComponentRef(fieldName); + if (ref) { + let el: HTMLElement | null = null; + if (ref instanceof HTMLElement) { + el = ref; + } else if (ref.$el instanceof HTMLElement) { + el = ref.$el; + } + if (!el) { + continue; + } + if ( + el === document.activeElement || + el.contains(document.activeElement) + ) { + return fieldName; + } + } + } + return undefined; + } + getLatestSubmissionValues() { return this.latestSubmissionValues || {}; } @@ -143,13 +190,14 @@ export class FormApi { return proxy; } - mount(formActions: FormActions) { + mount(formActions: FormActions, componentRefMap: Map) { if (!this.isMounted) { Object.assign(this.form, formActions); this.stateHandler.setConditionTrue(); this.setLatestSubmissionValues({ ...toRaw(this.handleRangeTimeValue(this.form.values)), }); + this.componentRefMap = componentRefMap; this.isMounted = true; } } diff --git a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue index 191c3019..873afe1d 100644 --- a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue +++ b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue @@ -3,7 +3,7 @@ import type { ZodType } from 'zod'; import type { FormSchema, MaybeComponentProps } from '../types'; -import { computed, nextTick, useTemplateRef, watch } from 'vue'; +import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue'; import { FormControl, @@ -18,6 +18,7 @@ import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils'; import { toTypedSchema } from '@vee-validate/zod'; import { useFieldError, useFormValues } from 'vee-validate'; +import { injectComponentRefMap } from '../use-form-context'; import { injectRenderFormProps, useFormContext } from './context'; import useDependencies from './dependencies'; import FormLabel from './form-label.vue'; @@ -267,6 +268,15 @@ function autofocus() { fieldComponentRef.value?.focus?.(); } } +const componentRefMap = injectComponentRefMap(); +watch(fieldComponentRef, (componentRef) => { + componentRefMap?.set(fieldName, componentRef); +}); +onUnmounted(() => { + if (componentRefMap?.has(fieldName)) { + componentRefMap.delete(fieldName); + } +});