瀏覽代碼

feat: improved formApi for component instance support

* 改进表单API以支持组件实例的获取,以及焦点字段的获取
Netfan 1 周之前
父節點
當前提交
04dff33ac5

+ 18 - 16
docs/src/components/common-ui/vben-form.md

@@ -279,22 +279,24 @@ const [Form, formApi] = useVbenForm({
 
 useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
 
-| 方法名 | 描述 | 类型 |
-| --- | --- | --- |
-| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` |
-| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` |
-| resetForm | 重置表单 | `()=>Promise<void>` |
-| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
-| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
-| validate | 表单校验 | `()=>Promise<void>` |
-| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
-| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
-| resetValidate | 重置表单校验 | `()=>Promise<void>` |
-| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
-| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
-| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` |
-| getState | 获取组件状态(props) | `()=>Promise<VbenFormProps>` |
-| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - |
+| 方法名 | 描述 | 类型 | 版本号 |
+| --- | --- | --- | --- |
+| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
+| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
+| resetForm | 重置表单 | `()=>Promise<void>` | - |
+| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
+| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` | - |
+| validate | 表单校验 | `()=>Promise<void>` | - |
+| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` | - |
+| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
+| resetValidate | 重置表单校验 | `()=>Promise<void>` | - |
+| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
+| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` | - |
+| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` | - |
+| getState | 获取组件状态(props) | `()=>Promise<VbenFormProps>` | - |
+| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
+| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
+| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
 
 ## Props
 

+ 49 - 1
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<VbenFormProps>;
 
+  /**
+   * 组件实例映射
+   */
+  private componentRefMap: Map<string, unknown> = new Map();
+
   // 最后一次点击提交时的表单值
   private latestSubmissionValues: null | Recordable<any> = null;
 
@@ -85,6 +92,46 @@ export class FormApi {
     bindMethods(this);
   }
 
+  /**
+   * 获取字段组件实例
+   * @param fieldName 字段名
+   * @returns 组件实例
+   */
+  getFieldComponentRef<T = ComponentPublicInstance>(
+    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<string, unknown>) {
     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;
     }
   }

+ 11 - 1
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);
+  }
+});
 </script>
 
 <template>

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

@@ -20,6 +20,9 @@ export const [injectFormProps, provideFormProps] =
     'VbenFormProps',
   );
 
+export const [injectComponentRefMap, provideComponentRefMap] =
+  createContext<Map<string, unknown>>('ComponentRefMap');
+
 export function useFormInitial(
   props: ComputedRef<VbenFormProps> | VbenFormProps,
 ) {

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

@@ -17,7 +17,11 @@ import {
   DEFAULT_FORM_COMMON_CONFIG,
 } from './config';
 import { Form } from './form-render';
-import { provideFormProps, useFormInitial } from './use-form-context';
+import {
+  provideComponentRefMap,
+  provideFormProps,
+  useFormInitial,
+} from './use-form-context';
 // 通过 extends 会导致热更新卡死,所以重复写了一遍
 interface Props extends VbenFormProps {
   formApi: ExtendedFormApi;
@@ -29,11 +33,14 @@ const state = props.formApi?.useStore?.();
 
 const forward = useForwardPriorityValues(props, state);
 
+const componentRefMap = new Map<string, unknown>();
+
 const { delegatedSlots, form } = useFormInitial(forward);
 
 provideFormProps([forward, form]);
+provideComponentRefMap(componentRefMap);
 
-props.formApi?.mount?.(form);
+props.formApi?.mount?.(form, componentRefMap);
 
 const handleUpdateCollapsed = (value: boolean) => {
   props.formApi?.setState({ collapsed: !!value });

+ 19 - 3
playground/src/views/_core/authentication/login.vue

@@ -1,8 +1,8 @@
 <script lang="ts" setup>
 import type { VbenFormSchema } from '@vben/common-ui';
-import type { BasicOption } from '@vben/types';
+import type { BasicOption, Recordable } from '@vben/types';
 
-import { computed, markRaw } from 'vue';
+import { computed, markRaw, useTemplateRef } from 'vue';
 
 import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
@@ -104,12 +104,28 @@ const formSchema = computed((): VbenFormSchema[] => {
     },
   ];
 });
+
+const loginRef =
+  useTemplateRef<InstanceType<typeof AuthenticationLogin>>('loginRef');
+
+async function onSubmit(params: Recordable<any>) {
+  authStore.authLogin(params).catch(() => {
+    // 登陆失败,刷新验证码的演示
+
+    // 使用表单API获取验证码组件实例,并调用其resume方法来重置验证码
+    loginRef.value
+      ?.getFormApi()
+      ?.getFieldComponentRef<InstanceType<typeof SliderCaptcha>>('captcha')
+      ?.resume();
+  });
+}
 </script>
 
 <template>
   <AuthenticationLogin
+    ref="loginRef"
     :form-schema="formSchema"
     :loading="authStore.loginLoading"
-    @submit="authStore.authLogin"
+    @submit="onSubmit"
   />
 </template>

+ 10 - 1
playground/src/views/examples/form/api.vue

@@ -1,4 +1,6 @@
 <script lang="ts" setup>
+import type { RefSelectProps } from 'ant-design-vue/es/select';
+
 import { ref } from 'vue';
 
 import { Page } from '@vben/common-ui';
@@ -82,6 +84,7 @@ function handleClick(
   action:
     | 'batchAddSchema'
     | 'batchDeleteSchema'
+    | 'componentRef'
     | 'disabled'
     | 'hiddenAction'
     | 'hiddenResetButton'
@@ -129,6 +132,11 @@ function handleClick(
       });
       break;
     }
+    case 'componentRef': {
+      // 获取下拉组件的实例,并调用它的focus方法
+      formApi.getFieldComponentRef<RefSelectProps>('fieldOptions')?.focus();
+      break;
+    }
     case 'disabled': {
       formApi.setState({ commonConfig: { disabled: true } });
       break;
@@ -182,6 +190,7 @@ function handleClick(
       formApi.setState({ submitButtonOptions: { show: true } });
       break;
     }
+
     case 'updateActionAlign': {
       formApi.setState({
         // 可以自行调整class
@@ -189,7 +198,6 @@ function handleClick(
       });
       break;
     }
-
     case 'updateResetButton': {
       formApi.setState({
         resetButtonOptions: { disabled: true },
@@ -257,6 +265,7 @@ function handleClick(
       <Button @click="handleClick('batchDeleteSchema')">
         批量删除表单项
       </Button>
+      <Button @click="handleClick('componentRef')">下拉组件获取焦点</Button>
     </Space>
     <Card title="操作示例">
       <BaseForm />