無星的前端之旅(二十一)—— 表單封裝

背景

咱們作的是後臺類型的管理系統,所以相對應的表單就會不少。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

相關文章
相關標籤/搜索