Vben-Admin之Form组件篇

Vben-Admin Form组件篇

: 关于Vben总体设计参考Vben框架设计


去年的今天我写了关于Vben第一篇文章,今天突发奇想更新下关于Vben的文章,准备发布时看到和去年日期竟然一样 0 0

本文主要讲解的是 BasicForm组件的用法及原理

用法

  • 当我们引入BasicForm组件时,通常需要通过register进行挂载
  • 通过其封装的useForm进行挂载,传入相应的配置,并可获得表单的一些方法能力
javascript 复制代码
 setup() {
    const [register, { validate }] = useForm({
      labelWidth: 130,
      schemas: aboutSchema,
      showSubmitButton: true,
      showResetButton: false,
      submitButtonOptions: {
        text: '提交',
      },
      actionColOptions: {
        span: 24,
      },
    });
    const handleSubmit = async () => {
      const data = await validate();
      console.log(data);
    };

    return {
      register,
      handleSubmit,
    };
  },
  render() {
    return (
      <Row>
        <Col span={20}>
          <BasicForm onRegister={this.register} onSubmit={this.handleSubmit}></BasicForm>
        </Col>
      </Row>
    );
  },

一、让我们先看看通过useForm是如何进行挂载,以及如何获取form的validate方法

  • 由BasicForm中的代码可见,在其mounted时,调用了外部传入的register方法,并传入了表单实用方法
javascript 复制代码
 const formActionType: Partial<FormActionType> = {
        getFieldsValue,
        setFieldsValue,
        resetFields,
        updateSchema,
        resetSchema,
        setProps,
        removeSchemaByFiled,
        appendSchemaByField,
        clearValidate,
        validateFields,
        validate,
        submit: handleSubmit,
        scrollToField: scrollToField,
      };

onMounted(() => {
    initDefault();
    emit('register', formActionType);
});
  • 这时调用了useForm中的register方法,并传入了formActionType
  • register中做的事情也比较简单,声明了一个对象,并保存了formActionType
javascript 复制代码
function register(instance: FormActionType) {
    isProdMode() &&
      onUnmounted(() => {
        formRef.value = null;
        loadedRef.value = null;
      });
    if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;

    formRef.value = instance;
    loadedRef.value = true;

    watch(
      () => props,
      () => {
        props && instance.setProps(getDynamicProps(props));
      },
      {
        immediate: true,
        deep: true,
      },
    );
  }
  • 注意 这里监听了props,在挂载的过程中,调用了BasicForm中的setProps方法,useForm传入的配置项在这时进行初始化
  • 紧接着 我们一起看下BasicForm中的setProps做了哪些事情
javascript 复制代码
    // Get the basic configuration of the form
      const getProps = computed((): FormProps => {
        return { ...props, ...unref(propsRef as FormProps) } as FormProps;
      });
      async function setProps(formProps: Partial<FormProps>): Promise<void> {
        propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
      }
  • 这里大家应该明白,通过setProps后,props通过计算属性,以getProps方式传给BasicForm下的FormItem 组件

以上讲了BasicForm主要挂载方式流程,并举例了setProps方法的实现,例如 validate、resetSchema等其他form方法,有兴趣可以看看其实现,这里不做过多分析

二、通过shcemas如何渲染出具体组件,以及自定义组件如何封装

这里通过两个例子来说明渲染组件逻辑
javascript 复制代码
export const aboutSchema: FormSchema[] = [
  {
    label: '数量',
    field: 'number',
    component: 'InputNumber',
  },
  {
    label: '愿景',
    field: 'vision',
    component: 'Input',
    required: true,
    colProps: {
      span: 12,
    },
    render: ({ model, field }) => {
      return h(Tinymce, {
        value: model[field],
        onChange: (value: string) => {
          model[field] = value;
        },
      });
    },
  }]
  • 在FormItem中,核心渲染逻辑为 renderComponent方法,这里先贴下完整的代码
javascript 复制代码
      function renderComponent() {
        const {
          renderComponentContent,
          component,
          field,
          changeEvent = 'change',
          valueField,
        } = props.schema;

        const isCheck = component && ['Switch', 'Checkbox'].includes(component);

        const eventKey = `on${upperFirst(changeEvent)}`;

        const on = {
          [eventKey]: (...args: Nullable<Recordable>[]) => {
            const [e] = args;
            if (propsData[eventKey]) {
              propsData[eventKey](...args);
            }
            const target = e ? e.target : null;
            const value = target ? (isCheck ? target.checked : target.value) : e;
            props.setFormModel(field, value);
          },
        };
        const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;

        const { autoSetPlaceHolder, size } = props.formProps;
        const propsData: Recordable = {
          allowClear: true,
          getPopupContainer: (trigger: Element) => trigger.parentNode,
          size,
          ...unref(getComponentsProps),
          disabled: unref(getDisable),
        };

        const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
        // RangePicker place is an array
        if (isCreatePlaceholder && component !== 'RangePicker' && component) {
          propsData.placeholder =
            unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component);
        }
        propsData.codeField = field;
        propsData.formValues = unref(getValues);

        const bindValue: Recordable = {
          [valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
        };

        const compAttr: Recordable = {
          ...propsData,
          ...on,
          ...bindValue,
        };

        if (!renderComponentContent) {
          return <Comp {...compAttr} />;
        }
        const compSlot = isFunction(renderComponentContent)
          ? { ...renderComponentContent(unref(getValues)) }
          : {
              default: () => renderComponentContent,
            };
        return <Comp {...compAttr}>{compSlot}</Comp>;
      }
  • 由代码可以看出 最终返回的是Comp组件,不难看出,这里是根据schemas中的component字段,通过字典的形式,去取到对应的组件
  • 我们重点来看下 schemas中的field是如何进行双向绑定的,这个划重点 , 这里默认实现了自定义change事件,并对field字段进行赋值
ini 复制代码
        const on = {
          [eventKey]: (...args: Nullable<Recordable>[]) => {
            const [e] = args;
            if (propsData[eventKey]) {
              propsData[eventKey](...args);
            }
            const target = e ? e.target : null;
            const value = target ? (isCheck ? target.checked : target.value) : e;
            props.setFormModel(field, value);
          },
        };
  • 关于setFormModel的实现在BascifForm中,比较简单,不做赘述
  • 这里还对初始化的setProps传进来的props进行重新组合,并最终传给了 Comp 组件
  • 所以当我们schema的component为Input等组件时,渲染后在修改其值时,会调用这里自定义的change事件,并完成fled字段的赋值
接下来我们来看看自定义组件双向绑定逻辑

这里用ApiSelect进行举例,其位于src -> components ->Form -> src -> components

  • 映入眼帘的,通过Vue的b-bind="$attrs"进行属性透传,这里不理解的,自行看vue文档
  • 紧接着 通过v-model:value="state",进行value绑定,这里有人纳闷,这state是什么玩意,不是我传进来的啊?
javascript 复制代码
<template>
  <Select
    @dropdown-visible-change="handleFetch"
    v-bind="$attrs"
    @change="handleChange"
    :options="getOptions"
    v-model:value="state"
  >
    <template #[item]="data" v-for="item in Object.keys($slots)">
      <slot :name="item" v-bind="data || {}"></slot>
    </template>
    <template #suffixIcon v-if="loading">
      <LoadingOutlined spin />
    </template>
    <template #notFoundContent v-if="loading">
      <span>
        <LoadingOutlined spin class="mr-1" />
        {{ t('component.form.apiSelectNotFound') }}
      </span>
    </template>
  </Select>
</template>
  • 仔细一看,噢,这玩意原来是通过一个名为useRuleFormItem的hooks生成的, 那我们一起看看这个useRuleFormItem做了啥事情
javascript 复制代码
 const [state] = useRuleFormItem(props, 'value', 'change', emitData);
  • 好家伙,原来state是一个计算属性,并通过观察者模式,在state改变时,调用了chageEvent也就是传进来的自定义change事件

这样一看是不是逐渐脉络清晰了起来

相关推荐
文艺倾年1 分钟前
【系统架构师】2025论文《WEB系统性能优化技术》
前端·性能优化·系统架构
铃木隼.2 分钟前
Web技术与Nginx网站环境部署
前端·nginx·php
郭尘帅6667 分钟前
Vue3 父子组件传值, 跨组件传值,传函数
前端·javascript·vue.js
charlee4434 分钟前
使用Vite创建一个动态网页的前端项目
前端·javascript·vite
SnowDreamXUE1 小时前
快速搭建一个electron-vite项目
前端·electron
gxn_mmf2 小时前
典籍知识问答模块AI问答bug修改
前端·javascript·后端·bug
说码解字2 小时前
Kotlin 协程
java·前端·kotlin
_龙小鱼_3 小时前
Vue响应式系统演进与实现解析
前端·vue.js
哎呦你好3 小时前
CSS 选择器入门
开发语言·前端·css·html