无星的前端之旅(二十一)—— 表单封装

背景

咱们作的是后台类型的管理系统,所以相对应的表单就会不少。html

相信作过相似项目的老哥懂得都懂。vue

所以咱们但愿可以经过一些相对简单的配置方式生成表单,再也不须要写一大堆的组件。react

尽可能经过数据驱动。git

思路

无论是哪一个平台,思路都是相通的。github

1.基于UI框架封装

react咱们基于antd封装。api

vue咱们基于element封装。markdown

这两个框架下的表单,几乎都知足了咱们对表单的须要,只是须要写那么多标签代码,让人感到厌倦。网络

2.如何根据数据驱动

想要简化标签,首先就须要约定数据格式,什么样类型的数据渲染什么样的标签。antd

那么我能够暂定,须要一个type,去作判断,渲染什么样的表单内容标签(是的,if判断,没有那么多花里胡哨,最朴实无华的代码就能知足咱们的需求)app

3.肯定须要渲染的标签

业务中其实经常使用的表单标签就以下几类:

  • select
  • checkbox
  • radio
  • input(包括各个类型的,passwordtextarea之类的)
  • switch

等等,须要再加

4.类型须要传递下去

须要把表单可能用到的属性传递下去。

实现

由于咱们在vue和react上都有,因此我会给出两个框架的封装代码。

Vue

我使用的是vue3+element-plus

封装两个组件,Form和FormItem

代码以下:

Form

<!-- Form/index.vue-->
<template>
  <el-form :ref="setFormRef" :model="form" label-width="80px">
    <el-form-item
      v-for="(item, index) in needs"
      :key="index"
      :prop="item.prop"
      :label="item.label"
      :rules="item.rules"
    >
      <!-- 内容 -->
      <FormItem
        v-model="form[item.prop]"
        :type="item.type"
        placeholder="请输入内容"
        :options="item.options || []"
        :disabled="item.disabled"
        v-bind="item"
      />
    </el-form-item>
  </el-form>
</template>
<script>
import { defineComponent, computed, watch } from 'vue';
import FormItem from '../FormItem/index.vue';

export default defineComponent({
  components: {
    FormItem,
  },
  props: {
    // 须要写的表单内容
    needs: {
      type: Array,
      default: () => [],
    },
    // 已知的表单内容
    modelValue: {
      type: Object,
      default: () => {},
    },
    instance: {
      type: Object,
      default: () => {},
    },
  },
  emits: ['update:modelValue', 'update:instance'],
  setup(props, context) {
    const form = computed({
      get: () => props.modelValue,
      set: (val) => {
        console.log('变化');
        context.emit('update:modelValue', val);
      },
    });
    const setFormRef = (el) => {
      context.emit('update:instance', el);
    };
    // 变化触发更新
    watch(form, (newValue) => {
      context.emit('update:modelValue', newValue);
    });
    return { form, setFormRef };
  },
});
</script>

复制代码

FormItem

<!-- FormItem/index.vue-->
<template>
  <el-input v-if="type === 'input'" clearable v-model="value" v-bind="$attrs" :class="propsClass" />
  <el-input
    v-else-if="type === 'password'"
    type="password"
    clearable
    v-model="value"
    v-bind="$attrs"
    :class="propsClass"
  />
  <el-radio-group
    v-else-if="type === 'radio'"
    v-model="value"
    v-bind="$attrs"
    :disabled="disabled"
    :class="propsClass"
  >
    <el-radio
      v-for="(item, index) in options"
      :key="index"
      :label="item.value"
      :disabled="item.disabled"
    >
      {{ item.label }}
    </el-radio>
  </el-radio-group>
  <el-checkbox-group
    v-else-if="type === 'checkbox'"
    v-model="value"
    v-bind="$attrs"
    :disabled="disabled"
    :class="propsClass"
  >
    <el-checkbox
      v-for="(item, index) in options"
      :key="index"
      :label="item.value"
      :disabled="item.disabled"
    >
      {{ item.label }}
    </el-checkbox>
  </el-checkbox-group>
  <el-input
    v-else-if="type === 'textarea'"
    type="textarea"
    clearable
    v-model="value"
    v-bind="$attrs"
    :class="propsClass"
  />
  <el-select
    v-else-if="type === 'select'"
    clearable
    v-model="value"
    v-bind="$attrs"
    :disabled="disabled"
    :class="propsClass"
  >
    <el-option
      v-for="(item, index) in options"
      :key="index"
      :label="item.label"
      :disabled="item.disabled"
      :value="item.value"
    />
  </el-select>
  <el-switch v-else-if="type === 'switch'" v-model="value" v-bind="$attrs" :class="propsClass" />
  <el-time-select
    v-else-if="type === 'timeSelect'"
    v-model="value"
    v-bind="$attrs"
    :disabled="disabled"
    :class="propsClass"
  />
</template>

<script>
import { defineComponent, computed, watchEffect } from 'vue';

export default defineComponent({
  name: 'FormItem',
  props: {
    // 须要绑定的值
    modelValue: {
      type: [String, Boolean, Number, Array],
      default: '',
    },
    // 传递下来的class
    propsClass: {
      type: String,
      default: '',
    },
    /**
     * 表单的类型 radio 单选 checkbox 多选 input 输入 select 选择 cascader 卡片 switch 切换 timeSelect 时间选择
     * @values radio, checkbox, input, select, cascader, switch, timeSelect,
     */
    type: {
      type: String,
      default: '',
      require: true,
    },
    // {value,disabled,source}
    options: {
      type: Array,
      default: () => [{}],
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['update:modelValue'],
  setup(props, context) {
    const value = computed({
      get: () => props.modelValue,
      set: (val) => {
        context.emit('update:modelValue', val);
      },
    });

    watchEffect(
      () => props.modelValue,
      (newValue) => {
        value.value = newValue;
      },
    );
    return {
      value,
    };
  },
});
</script>
<style lang="less" scoped>
:deep(.el-*) {
  width: 100%;
}
.width100 {
  width: 100%;
}
</style>

复制代码

这里要注意的点是v-bind="$attrs"

  • 由于咱们不可能将全部组件可能用到的props都写在这并导出没,并且也没有这个必要。

  • 因此咱们能够用到vue提供的$attrs来帮助咱们透传下去

使用

好比像这样一个表单

1.png

咱们只须要以下代码

Rules规则是咱们单独定义的符合async-validator的规则,这里就不写引入了

<template>
<Form 
  v-model:instance="formRef" 
  v-model="formData" 
  :needs="needs" 
/>
</template>
<script>
import {
  defineComponent, reactive, computed, ref
} from 'vue';
export default defineComponent({
  setup(){
    const formRef = ref();
    const options = reactive({
      departments: [],
      places: [],
      roles: [],
    });
    const formData = reactive({
      account: '',
      department: [],
      name: '',
      password: '',
      practicePlace: [],
      rePassword: '',
      roleId: '',
      uniqueid: '',
    });
    const needs = computed(() => [
      {
        label: '用户名',
        type: 'input',
        prop: 'name',
        propsClass: 'width100',
        placeholder: '请输入2-20个汉字,字母或数字',
        rules: [
          Rules.required('用户名不得为空'),
          Rules.dynamicLength(2, 20, '用户名长度为2-20位'),
          Rules.cen,
        ],
      },
      {
        label: '用户帐号',
        type: 'input',
        prop: 'account',
        propsClass: 'width100',
        placeholder: '请输入2-20个字母或数字',
        rules: [
          Rules.required('用户帐号不得为空'),
          Rules.dynamicLength(2, 20, '用户帐号长度为2-20位'),
          Rules.en,
        ],
      },
      {
        label: '密码',
        type: 'password',
        prop: 'password',
        propsClass: 'width100',
        placeholder: '支持6-20个字母、数字、特殊字符',
        rules: [
          Rules.required('密码不得为空'),
          Rules.dynamicLength(6, 20, '密码长度为6-20位'),
          Rules.password,
        ],
      },
      {
        label: '再输一次',
        type: 'password',
        prop: 'rePassword',
        propsClass: 'width100',
        placeholder: '支持6-20个字母、数字、特殊字符',
        rules: [
          Rules.required('请再输入一次密码'),
          Rules.dynamicLength(6, 20, '密码长度为6-20位'),
          Rules.password,
          Rules.same(formData.password, formData.rePassword, '两次密码输入不一致'),
        ],
      },
      {
        label: '角色',
        type: 'select',
        prop: 'roleId',
        propsClass: 'width100',
        placeholder: '请选择角色',
        rules: [Rules.required('角色不得为空')],
        options: options.roles,
      },
      {
        label: '执业地点',
        type: 'select',
        prop: 'practicePlace',
        propsClass: 'width100',
        placeholder: '请选择执业地点',
        multiple: true,
        filterable: true,
        options: [{ label: '所有', value: 'all' }].concat(options.places),
      },
      {
        label: '科室',
        type: 'select',
        prop: 'department',
        propsClass: 'width100',
        placeholder: '请选择科室',
        multiple: true,
        filterable: true,
        options: [{ label: '所有', value: 'all' }].concat(options.departments),
      },
    ]);

    // 网络请求获取options,这里就简写了
    // *********************

    return {
      formData,
      needs,
      formRef,
    }
  }
  
})
</script>
复制代码

咱们只须要聚焦数据,就能够构造出一张表单。

React

也是类似的,并且较之Vue的更加灵活,除了咱们上述的这种经常使用表单,咱们能够把后台管理的搜索项也认为是表单

Form

import React from 'react';
import { ColProps, Form, FormInstance } from 'antd';
import { FormLayout } from 'antd/lib/form/Form';
import FormItem, { IFormItem } from '../FormItem';

interface IForm {
  form: FormInstance<any>;
  itemLayout?: {
    labelCol: ColProps;
    wrapperCol: ColProps;
  };
  layout?: FormLayout;
  options: IFormItem[];
  initialValues?: { [key: string]: any };
  onValuesChange?(changedValues: unknown, allValues: any): void;
}
// 这是个单独的表单校验模板
/* eslint-disable no-template-curly-in-string */
const validateMessages = {
  required: '${label}是必填项',
};
/* eslint-enable no-template-curly-in-string */

const FormComponent = (props: IForm): JSX.Element => {
  const {
    form, onValuesChange, initialValues, options, layout, itemLayout,
  } = props;

  return (
    <Form form={form} {...itemLayout} layout={layout} onValuesChange={onValuesChange} initialValues={initialValues} validateMessages={validateMessages} > {/* 内容 */} {options.map((item) => ( <FormItem key={item.value} {...item} /> ))} </Form>
  );
};
FormComponent.defaultProps = {
  layout: 'horizontal',
  itemLayout: {
    labelCol: {},
    wrapperCol: {},
  },
  initialValues: {},
  // 此处默认定义为空函数
  onValuesChange() {},
};

export default FormComponent;
export type { IFormItem };

复制代码

须要注意的点

  • form的引用实例由外部传入
  • 取值赋值经过formInstance作,由于和vue不同,react作父子双向绑定比较复杂(也多是我不太熟练的缘故),因此建议是不要作成受控组件

FormItem

import React from 'react';
import {
  Form, Radio, Select, Input, DatePicker, Switch,
} from 'antd';
import { Rule } from 'antd/lib/form';

const { Option } = Select;
const { RangePicker } = DatePicker;
export interface IFormItem {
  type: 'input' | 'radio' | 'select' | 'rangePicker' | 'datePicker' | 'switch';
  label: string;
  // 须要绑定的key值
  value: string;
  // 可选项
  placeholder?: string;
  options?: { label: string; value: string | number }[];
  otherConfig?: any;
  itemConfig? : any;
  rules?: Rule[];
  itemClass?: string;
}
// Form.Item彷佛也不容许HOC
const FormItemComponent = (props: IFormItem): JSX.Element => {
  const {
    type, label, value, rules, placeholder, otherConfig, options, itemClass, itemConfig,
  } = props;
  // 判断类型

  return (
    <Form.Item label={label} name={value} rules={rules} className={itemClass} {...itemConfig}> {(() => { switch (type) { case 'input': return <Input placeholder={placeholder} {...otherConfig} />; case 'radio': return ( <Radio.Group {...otherConfig}> {options?.map((item) => ( <Radio key={item.value} value={item.value}> {item.label} </Radio> ))} </Radio.Group> ); case 'select': return ( <Select {...otherConfig} placeholder={placeholder}> {options?.map((item) => ( <Option key={item.value} value={item.value}> {item.label} </Option> ))} </Select> ); case 'rangePicker': return <RangePicker {...otherConfig} />; case 'datePicker': return <DatePicker {...otherConfig} />; case 'switch': return <Switch {...otherConfig} />; default: return <div />; } })()} </Form.Item>
  );
};

export default FormItemComponent;

复制代码

这里要注意的点

使用

例以下面两个例子

2.png

import React, { useEffect, useState } from 'react';
import {
  Form
} from 'antd';
import FormComponent, { IFormItem } from '../../components/FormComponent';
const Welcome = (): JSX.Element => {
  const [form] = Form.useForm();
  const [saleList, setSaleList] = useState<Options[]>([]);
  const [firmList, setFirmList] = useState<Options[]>([]);
  const options: IFormItem[] = [{
    type: 'select',
    label: '厂商名称',
    value: 'clientId',
    options: firmList,
    itemClass: 'width25',
    otherConfig: {
      onChange: () => {
        // 选中触发搜索,具体的就不写了
        search();
      },
    },
  }, {
    type: 'select',
    label: '销售人员',
    value: 'saleId',
    options: saleList,
    itemClass: 'width25',
    otherConfig: {
      onChange: () => {
        // 选中触发搜索,具体的就不写了
        search();
      },
    },
  }];
  useEffect(() => {
    // 获取两个列表,具体的就不写了
    getFirmList();
    getSaleList();
  }, []);
  return (
    <FormComponent form={form} layout="inline" options={options} initialValues={{ clientId: '', saleId: '', }} />
  )
};
export default Welcome;
复制代码

3.png

import React, { useEffect, useState } from 'react';
import {
  Form
} from 'antd';
import FormComponent, { IFormItem } from '../../components/FormComponent';

const UserList = (): JSX.Element => {
  const initialValues = {
    name: '',
    email: '',
    account: '',
    password: '',
    rePassword: '',
    roleId: '',
  };
  const [userForm] = Form.useForm();
  const userOptions: IFormItem[] = [{
    type: 'input',
    label: '名称',
    value: 'name',
    rules: [
      {
        required: true,
      },
      Rules.dynamicLength(2, 20),
      Rules.chinese,
    ],
  }, {
    type: 'input',
    label: '邮箱',
    value: 'email',
  }, {
    type: 'input',
    label: '帐号',
    value: 'account',
    rules: [
      {
        required: true,
      },
      Rules.dynamicLength(2, 20),
      Rules.cen,
    ],
  }, {
    type: 'input',
    label: '密码',
    value: 'password',
    rules: [
      {
        required: true,
      },
      Rules.minLength(6),
      Rules.englishAndNumber,
    ],
  }, {
    type: 'input',
    label: '再次确认密码',
    value: 'rePassword',
    itemConfig: {
      dependencies: ['password'],
    },
    rules: [
      {
        required: true,
      },
      Rules.minLength(6),
      Rules.englishAndNumber,
      ({ getFieldValue }) => ({
        validator(_, value) {
          if (!value || getFieldValue('password') === value) {
            return Promise.resolve();
          }
          return Promise.reject(new Error('两次密码不一致'));
        },
      }),
    ],
  }, {
    type: 'select',
    label: '用户角色',
    value: 'roleId',
    options,
    rules: [
      {
        required: true,
      },
    ],
  }];
  return (
    <FormComponent form={userForm} options={userOptions} itemLayout={{ labelCol: { sm: { span: 5 }, }, wrapperCol: { sm: { span: 18 }, }, }} initialValues={initialValues} />
  )
};
export default UserList;
复制代码

over

相关文章
相关标签/搜索