likes
comments
collection
share

前端公共组件开发之基础表单

作者站长头像
站长
· 阅读数 19

前言

企业应用中如何提炼公共模块,如何更好的扩展,如何让更多场景复用。以下将基于vue3 ts antd进行公共表单开发

FormItemComps

将每一种表单项分别拆分,易于后期扩展和管理。

    // index.ts
    import FormItemInput from "./FormItemInput.vue";
    import FormItemInputNumber from "./FormItemInputNumber.vue";
    
    interface ComsType {
      [key: string]: any
    }
    
    const ComponentMap: ComsType = {
      "input": FormItemInput,
      "input-number": FormItemInputNumber,
    }
  
    export default ComponentMap;

FormItemInput

基础的输入组件,有几个点我们需要先理解。

  1. 数据响应
  2. antd input props 项有很多,我们如何接入。

首先配置基础的表单项 name label rules等

使用 useChange 进行数据传递

使用 v-bind props.props 进行a-input的所有属性传递

// FormItemInput.vue
<template>
  <a-form-item
    :name="props.name"
    :label="props.label"
    :rules="props.rules"
  >
    <a-input
      v-model:value="inputValue"
      v-bind="props.props"
    />
  </a-form-item>
</template>

<script lang="ts" setup>
  // props.props 详细参数请查阅官方文档 https://antdv.com/components/input-cn/#API
  import { ref, watch } from "vue";
  import { AnyPropName } from "@/types";
  import { useChange } from "./useFormItem";
  import { formItemDefaultProps } from "./const";

  interface FormItemEmits {
    (e: "update:modelValue", value: any): void,
    (e: "onChange", value: any, key: string): void,
  }

  // vue3 当前版本暂不支持外部导入props类型定义 wait fix
  interface FormItemProps {
    modelValue: any, // v-model响应值
    name: string, // 数据键
    label: string, // 表单项文本名
    rules?: Array<AnyPropName>, // 校验规则 同antd 表单校验规则一致
    props?: AnyPropName, // 组件额外 props 同antd组件props一致
    change?: (value: any, key: string) => void, // 当前项值变化时触发
  }

  const emits = defineEmits<FormItemEmits>();
  const props = withDefaults(defineProps<FormItemProps>(), {...formItemDefaultProps});

  const inputValue = ref(props.modelValue);

  watch(inputValue, value => {
    useChange(emits, props, value);
  })

  watch(() => props.modelValue, value => {
    inputValue.value = value;
  })
</script>

FormItemInputNumber

数字输入组件,核心逻辑同输入组件一致,以后要扩展组件核心逻辑依然是不变的。我们唯一需要做的就是使用不同的a-xx组件或者你的自定义组件

<template>
  <a-form-item
    :name="props.name"
    :label="props.label"
    :rules="props.rules"
  >
    <a-input-number
      class="form-item-input-number"
      v-model:value="inputValue"
      v-bind="props.props"
    />
  </a-form-item>
</template>

<script lang="ts" setup>

  // props.props 详细参数请查阅官方文档 https://antdv.com/components/input-number-cn#API

  import { ref, watch } from "vue";

  import { AnyPropName } from "@/types";

  import { useChange } from "./useFormItem";
  import { formItemDefaultProps } from "./const";

  interface FormItemEmits {
    (e: "update:modelValue", value: any): void,
    (e: "onChange", value: any, key: string): void,
  }

  // vue3 当前版本暂不支持外部导入props类型定义 wait fix
  interface FormItemProps {
    modelValue: any, // v-model响应值
    name: string, // 数据键
    label: string, // 表单项文本名
    rules?: Array<AnyPropName>, // 校验规则 同antd 表单校验规则一致
    props?: AnyPropName, // 组件额外 props 同antd组件props一致
    change?: (value: any, key: string) => void, // 当前项值变化时触发
  }
  
  const emits = defineEmits<FormItemEmits>();
  const props = withDefaults(defineProps<FormItemProps>(), {...formItemDefaultProps});

  const inputValue = ref(props.modelValue);

  watch(inputValue, value => {
    useChange(emits, props, value);
  })

  watch(() => props.modelValue, value => {
    inputValue.value = value;
  })
</script>

<style lang="less">
  .form-item-input-number{
    width: 100%;
  }
</style>

useChange

响应事件逻辑是一致的,提取到公共模块。

import { FormItemProps, FormItemEmits } from "./types";

export function useChange(emits: FormItemEmits, props: FormItemProps, value: any) {
  const { change, name } = props;
  emits("update:modelValue", value); // v-model响应
  emits("onChange", value, name); // formSearch事件响应
  change!(value, name); // 数据项中传入的事件响应
}

BaseForm

整合定义好的表单项,并通过数据约束输出表单。

核心点:数据传入,修改表单内容,获取表单内容,校验提交表单。

<template>
  <a-spin :spinning="loading" :tip="loadingTip" :delay="delayTime">
    <a-form
      ref="formRef"
      name="base-form-instance"
      v-bind="props"
      :model="formState"
      :labelCol="labelCol"
      :wrapperCol="wrapperCol"
    >
      <a-row :gutter="24">
        <template v-for="item in data" :key="item.key">
          <a-col :span="item.span || colSpan">
            <a-tooltip :title="item.tip" :mouseEnterDelay="0.2">
              <component
                @onChange="onChange"
                :is="ComponentMap[item.type]"
                v-model="formState[item.key]"
                v-bind="item"
                :name="item.key"
              />
            </a-tooltip>
          </a-col>
        </template>
      </a-row>
    </a-form>
  </a-spin>
</template>

<script lang="ts" setup>
  import { ref, toRefs, reactive, nextTick } from "vue";

  import type { FormInstance } from "ant-design-vue";
  import type { AnyPropName, FormListDatas } from "@types";

  import ComponentMap from "@components/FormItemComs";
  import { defaultProps } from "./const";

  interface PropsType {
    data: FormListDatas, // 数据源
    loading?: boolean, // 是否加载中
    loadingTip?: string, // 自定义加载提示文案
    delayTime?: number, // 延迟显示loading状态, 当loading状态在 delayTime 时间内结束, 则不显示loding UI状态 单位ms
    colSpan?: number // 表单项一行占多少列 n/24
    labelCol?: AnyPropName, // 表单项 label 配置; https://www.antdv.com/components/grid-cn/#Col
    wrapperCol?: AnyPropName // 表单项 输入控件配置; 类型同 labelCol一致
  }

  const emits = defineEmits(["change"]);
  const props = withDefaults(defineProps<PropsType>(), { ...defaultProps }) // defineProps<>(["visible", "modelValue"]);
  const { data, loading, loadingTip, delayTime, labelCol } = toRefs(props);
  
  const formRef = ref<FormInstance>();
  const formState = reactive<AnyPropName>({});
  setDefaultFormState();

  let timeoutValidate: any;
  // 对 component 使用 onChange 的原因是组件本身有change事件这里不进行混合; 并且change参数类型也不相同会提示报错
  const onChange = (key: string, value: any): void => {
    emits("change", key, value);

    // 当数据变化时手动触发校验, 因为嵌套v-model导致表单自动校验会出现错误响应 预期与实际不一致
    clearTimeout(timeoutValidate);
    timeoutValidate = setTimeout(() => {
      formRef.value?.validateFields([key]);
    }, 120)
  }

  function setDefaultFormState() {
    data.value.forEach(v => {
      if (formState[v.key] !== v.defaultValue) formState[v.key] = v.defaultValue;      
    });
  }

  // 重置表单后会触发n次 change 响应事件
  const resetFields = () => {
    formRef.value?.resetFields(); // 清空表单状态; 当通过setFormState重新设置值之后,只调用resetFields一次无法重置
    nextTick(() => { 
      setDefaultFormState(); // 也可以再调用一次 formRef.value?.resetFields(); 但是这样重新设置的 data[n].defaultValue 无法生效;
    });
  }

  const getFormState = (payload?: string | string[]): string | object => {
    const type = payload ? typeof payload : "not";

    if (type === "not") return { ...formState };

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    if (type === "string") return formState[payload];

    if (Array.isArray(payload)) {
      const ret: AnyPropName = {};
      payload.forEach(key => {
        ret[key] = formState[key]
      });
      return ret;
    }
    console.error(`传入参数类型需为 string | string[]; 当前参数${JSON.stringify(payload)}`);
    return {};
  };

  const setFormState = (key: string | AnyPropName, value?: any) => {
    const type = key ? typeof key : "not";
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    if (type === "string") formState[key] = value;
    else if (!Array.isArray(key) && type === "object") {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      for (const name in key) {
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore
        formState[name] = key[name];
      }
    } else {
      console.error(`传入参数类型需为 string | object<{key: value}>; 当前参数${JSON.stringify(key)}`);
    }
  }

  const submit = async (nameList?: string | string[]): Promise<AnyPropName> => {
    return formRef.value!.validateFields(nameList);
  }

  defineExpose({ getFormState, setFormState, resetFields, submit });
</script>

结语

关于如何使用表单请前往GitHub查看。因为这是一系列东西,文章上写出来太多反而影响阅读。