基於react hooks,antd4 配置生成表單並自動排列

記憶的森林發表於2021-05-08

react後臺專案,大多都是表單處理,比如下列4種常見1*n佈局 (如果手工編碼,大量的Row,Col, Form.Item的巢狀,排列,如果加上聯動處理,程式碼將十分臃腫,不易維護)

  1. 一行一列
    form1.png

  2. 一行兩列
    fom2.png

  3. 一行三列
    form3.png

  4. 一行四列
    form4.png

對於這列表單開發, 完全可以基於配置生成, 我們可以定義一個陣列,陣列的每一項都是一個表單項, 對於一行一列的排列, 我們可以自上而下一行一個元件進行render , 虛擬碼如下

       return arr.map((item, idx) => itemRender(item, idx, 24))

對於一行n列 (n=2/3/4 , 參考antd grid佈局, 一行最多不超過4個表單項 https://ant.design/components/grid-cn/
基於24 柵格系統,可以我們計算出每個元件佔用的柵格數24/n , 基於此,我們可以動態建立Grid,自上而下一行一組進行render實現一行多列布局 ,虛擬碼如下

        const len = group.length;
        const span = 24 / len;

        return (
          <Row key={idx}>
            {arr.map((item, subIndex) => itemRender(item, subIndex, span))}
          </Row>
        );

上述 itemRender 用於render表單元件, antd4表單項通常這麼寫 ,一個Form.Item 包裹一個表單控制元件,參考如下

    <Form.Item
        label="Username"
        name="username"
        rules={[{ required: true, message: 'Please input your username!' }]}
      >
        <Input />
      </Form.Item>

基於js我們可以抽離出如下配置項, 1. render什麼元件,例如Input/Select/等,另外配置元件props 2. Form.Item 配置 , 我們可以設計如下通用js物件表示這些資訊

  {
    type?: React.ComponentType | string; // 元件型別, 比如Input 等
    name?: string; //Form.Item的name
    label?: string; // Form.Item的label
    elProps?: Record<string, unknown>; // 元件的props配置 , 比如type為Input, elProps則會配置到Input
    itemProps?: Record<string, unknown>;  // Form.Item的props配置,除了上面name,lable,rules三個常用的,其他的可以放在這裡配置
    rules?: Rule[]; // Form.Item的rules
};

根據上面的js物件配置資訊,我們可以實現itemRender動態建立元件和佈局

const itemRender = (item: Item, key: number | string, span = 24) => {
  if (typeof item !== 'object' || !item) return null;

  const { type, name, rules, label, elProps = {}, itemProps = {}, render, ...props } = item;

  return (
    <Col span={span} key={key}>
     <Form.Item name={name} label={label} rules={rules} {...itemProps}>
          {React.createElement(type, { ...props, ...elProps } as React.Attributes)}
        </Form.Item>
    </Col>
  );
};

為了更方便實現表單聯動和支援render任意元件(不僅僅是表單), 我們可以擴充套件js加上render和getJSON 方法(當然叫getConfigJs更合適)

 {
    type?: React.ComponentType | string; // 元件型別, 比如Input 等
    name?: string; //Form.Item的name
    label?: string; // Form.Item的label
    render?: () => React.ReactNode; //自定義 render
    getJSON?: () => Item | null; // 動態返回Item配置
    elProps?: Record<string, unknown>; // 元件的props配置 , 比如type為Input, elProps則會配置到Input
    itemProps?: Record<string, unknown>;  // Form.Item的props配置,除了上面name,lable,rules三個常用的,其他的可以放在這裡配置
    rules?: Rule[]; // Form.Item的rules
};

自此, 一個通用的antd form-render 就編寫完了, 可以參考https://github.com/leonwgc/antd-form-render

安裝

npm / yarn 安裝:

$ npm install --save antd-form-render
$ yarn add antd-form-render

功能

  • 配置一維陣列實現 1 行 n 列 (自動佈局,自上向下,自左向右佈局,參考汽車自動擋駕駛) n可以是1/2/3/4 ,預設1
  • 配置二維陣列實現 1行n列 (手動佈局,每一行顯示幾列根據陣列長度決定,參考汽車手動擋駕駛)
  • 天然支援表單聯動 ,參考下面示例1性別選擇程式碼
  • 支援自定義render, 當antd元件無法滿足需求,可以自定義返回任意react node
  • 支援動態返回js物件, 參考下面示例1性別選擇程式碼,性別男下面動態生成輸入框,女則為下拉框
  • 資料收集,name作為key ,相應表單控制元件的值為value

實現 1 行 1 列

import React, { useState } from 'react';
import FormRender from 'antd-form-render';
import { Form, Button, Space, Input, Radio, Select } from 'antd';

export default function App() {
  const [data, setData] = useState({});

  // 定義form
  const [form] = Form.useForm();

  // 一維陣列定義layout,從上往下一行放一個表單控制元件
  const layout = [
    {
      type: Input,
      label: '手機號',
      placeholder: '請輸入',
      name: 'tel',
      // 對Input的配置 , elProps對type指定的元件配置
      elProps: {
        maxLength: 11,
      },
      // 對Form.Item的配置
      itemProps: {
        rules: [
          { required: true, message: '請輸入' },
          { pattern: /^1\d{10}$/, message: '手機號必須為11位數字' },
        ],
      },
    },
    {
      type: Input.Password,
      label: '密碼',
      placeholder: '請輸入',
      name: 'pwd',
      itemProps: {
        rules: [{ required: true, message: '請輸入' }],
      },
    },
    {
      type: Input.Password,
      label: '確認密碼',
      placeholder: '請輸入',
      name: 'confirmPwd',
      itemProps: {
        rules: [
          { required: true, message: '請輸入' },
          ({ getFieldValue }) => ({
            validator(_, value) {
              if (!value || getFieldValue('pwd') === value) {
                return Promise.resolve();
              }
              return Promise.reject(new Error('兩次密碼不一致'));
            },
          }),
        ],
      },
    },
    {
      type: Radio.Group,
      label: '性別',
      name: 'gender',
      elProps: {
        options: [
          { label: '男', value: '男' },
          { label: '女', value: '女' },
        ],
      },
    },
    {
      // 根據條件動態返回object
      getJSON() {
        return data.gender === '男'
          ? {
              type: Input,
              label: '興趣愛好(男)',
              placeholder: '請輸入興趣愛好',
              name: 'hobby',
              itemProps: {
                rules: [{ required: true, message: '請輸入興趣愛好' }],
              },
            }
          : data.gender === '女'
          ? {
              type: Select,
              label: '興趣愛好(女)',
              placeholder: '請選擇興趣愛好',
              name: 'hobby',
              itemProps: {
                itemProps: {
                  rules: [{ required: true, message: '請選擇興趣愛好' }],
                },
              },
              elProps: {
                options: [
                  { label: '畫畫', value: '畫畫' },
                  { label: '唱歌', value: '唱歌' },
                  { label: '跳舞', value: '跳舞' },
                ],
              },
            }
          : null;
      },
    },
    {
      type: Input.TextArea,
      name: 'desc',
      label: '簡介',
      elProps: {
        placeholder: '個人簡介',
        rows: 4,
      },
      itemProps: {
        rules: [
          {
            required: true,
          },
        ],
      },
    },
    {
      // 自定義render
      render() {
        return (
          <Form.Item>
            <Space>
              <Button htmlType="submit" type="primary">
                確定
              </Button>
              <Button htmlType="reset">重置</Button>
            </Space>
          </Form.Item>
        );
      },
    },
  ];

  return (
    <Form
      form={form}
      onValuesChange={(v) => {
        setData((p) => ({ ...p, ...v }));
      }}
    >
      <FormRender layoutData={layout} />
    </Form>
  );
}

實現 1 行 n 列如下 ,比如一行 2 列(子陣列的長度決定列數,長度能被 24 整除)

const layout = [
  [
    {
      type: Input,
      label: '11',
      placeholder: '請輸入',
      name: '11',
    },
    {
      type: Input,
      label: '12',
      placeholder: '請輸入',
      name: '12',
    },
  ],
  [
    {
      type: Input,
      label: '21',
      placeholder: '請輸入',
      name: '21',
    },
    {
      type: Input,
      label: '22',
      placeholder: '請輸入',
      name: '22',
    },
  ],
];

實現 1 行 2/3/4 列如下

// 一維陣列,設定了cols 為1/2/3/4 ,實現自動從左至右,從上到下的 1*cols 1行多列自動佈局

const layout3 = [];

for (let i = 0; i < 11; i++) {
  layout3.push({
    type: Input,
    label: `輸入框${i + 1}`,
    placeholder: '請輸入',
    name: `name${i}`,
  });
}

<FormRender layoutData={layout3} cols={cols}></FormRender>;

配置說明

// 元件
export default function FormRenderer({ layoutData, cols }: FormRenderProps): React.ReactNode;

// 元件props
export declare type FormRenderProps = {
    layoutData: Array<Item>; // 1/2維陣列
    cols: null | 1 | 2 | 3 | 4; // 自動佈局1行顯示幾列 default 1
};

// 陣列配置項
export declare type Item = {
    type?: React.ComponentType | string; // 元件型別, 比如Input 等
    name?: string; //Form.Item的name
    label?: string; // Form.Item的label
    render?: () => React.ReactNode; //自定義 render
    getJSON?: () => Item | null; // 動態返回Item配置
    elProps?: Record<string, unknown>; // 元件的props配置 , 比如type為Input, elProps則會配置到Input
    itemProps?: Record<string, unknown>;  // Form.Item的props配置,除了上面name,lable,rules三個常用的,其他的可以放在這裡配置
    rules?: Rule[]; // Form.Item的rules
};

執行示例, yarn start / npm start 檢視 demo , 效果如下

antd-form-render.png

相關文章