【react框架】结合antd做表单组件的一些心得记录

这篇具有很好参考价值的文章主要介绍了【react框架】结合antd做表单组件的一些心得记录。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

作为一个前端最常遇见的需求场景就是写表单、写表格。写多了会逐渐的积累一些开发心得,此文章根据我使用vue和react的经验记录了一些东西,抛砖引玉的给大家看看。


功能的实现尽量先看第三方组件库上是否已经提供

举例react项目,在做表单的很多时候,我都是从antd上把其中一个form组件例子复制下来,然后再看看提供了哪些api就开干了。

这样做其实会忽略很多细节方面的东西。

比如我想在rules校验的时候拿到其他item的值,可能会通过form的实例调用getFieldValue,但其实在rules的使用中已经默认传入了该方法:

rules={[
  ({ getFieldValue }) => ({
    validator(_, value) {
      // 可以通过getFieldValue('password')获取对应其他item的值
    },
  }),
]}

类似的例子还有很多,所以有些需要的功能其实你细看整个form组件的页面说明后,说不定就给你找到了你想要的。

这里提供一个注册表单的例子,里面很多小细节可以注意下,没准你还有新发现:

<Form labelCol={{ span: 6 }} wrapperCol={{ span: 16 }} onFinish={onFinish}>
  <Form.Item
    label="用户名"
    name="username"
    rules={[
      { required: true, message: '请输入用户名' },
      { type: 'string', min: 5, max: 20, message: '字符长度在 5-20 之间' },
      { pattern: /^\w+$/, message: '只能是字母数字下划线' },
    ]}
  >
    <Input />
  </Form.Item>
  <Form.Item
    label="密码"
    name="password"
    rules={[{ required: true, message: '请输入密码' }]}
  >
    <Input.Password />
  </Form.Item>
  <Form.Item
    label="确认密码"
    name="confirm"
    dependencies={['password']} // 依赖于 password ,password 变化,会重新触发 validator
    rules={[
      { required: true, message: '请输入密码' },
      ({ getFieldValue }) => ({
        validator(_, value) {
          if (!value || getFieldValue('password') === value) {
            return Promise.resolve()
          } else {
            return Promise.reject(new Error('两次密码不一致'))
          }
        },
      }),
    ]}
  >
    <Input.Password />
  </Form.Item>
  <Form.Item label="昵称" name="nickname">
    <Input />
  </Form.Item>
  <Form.Item wrapperCol={{ offset: 6, span: 16 }}>
    <Space>
      <Button type="primary" htmlType="submit">
        注册
      </Button>
      <Link to={LOGIN_PATHNAME}>已有账户,登录</Link>
    </Space>
  </Form.Item>
</Form>

其实不仅仅是表单组件,其他组件也是一样的道理,所以建议没事可以多去组件库官网上翻翻,留些整体印象


当一个页面有多个表单组件时,就要优先考虑把值存在状态管理中

这种场景太常见了,一个页面有多个表单组件。我看很多时候大家都是把每个组件的数据状态维护在各自的组件中,其实不太好。

  • 指不定你下个页面还可以返回上一步的表单填写页,如果都把数据统一放在状态管理维护,回显就很容易了。
  • 各个表单组件可能是会联动的,例如a组件的某个item的值会改变b组件的一些item的显隐,有状态管理就比较容易去做。

当有一个复杂的多表单页面时,建议做页面缓存

场景举例:

  • 复杂的多表单页面A —> 复杂的多表单页面B,A页面下一步到B页面,然后再返回A页面,A页面回显
  • 移动端中复杂的多表单页面A ----> 某项输入项具体选择的页面C,A页面点击某项输入项到C页面,然后再返回A页面,A页面回显并且定位在刚刚的位置。

这些情况还用redux去做回显就很笨了,增加代码复杂度。

vue有官方的keepalive,react有网友给我推荐了keepalive-react-component,我个人还没使用过。


如果一些表单比较简单且能确保后续不会有复杂功能的拓展,可以使用业务组件

例如后台管理的一些列表搜索页的搜索表单区域,这种一般就不会做的很复杂,很适合用业务组件。

【react框架】结合antd做表单组件的一些心得记录

但当你觉得某个表单现在看来简单,但以后被改复杂的可能性很大时,就不要考虑使用业务组件了。

react表单业务类组件例子:

/* eslint-disable react-hooks/exhaustive-deps */
/* 用于列表页的搜索过滤区 类组件
necessaryField 必须返回字段值
initiative 主动第一次触发搜索

formatter: int

btnChildren 按钮插槽

config :{
    formItems: [],
    colSpan: [], // 
    formLayout
}
*/
import React, { Component } from 'react'
import {
    Button,
    Form,
    Input,
    Select,
    DatePicker,
    Row,
    Col,
    InputNumber,
} from 'antd'
import { ReloadOutlined } from '@ant-design/icons'

import moment from 'moment'

import './TableFiltersSearch.less'
import { isEffectVar, deepClone, validateNumber } from './utils'

export default class TableFiltersSearchClass extends Component {
    constructor(props) {
        super(props)
        this.state = {
            formItems: [],
            colSpan: [21, 3], // 表单和按钮区的宽度设置
            formLayout: 'inline',
            formItemLayout: {
                labelCol: {
                    flex: '1',
                },
                wrapperCol: {
                    flex: '1',
                },
            },
        }
    }
    formRef = React.createRef()
    submitRef = React.createRef()
    // 布局

    componentDidMount() {
        /* 处理搜索区配置 */
        this.init()
    }

    componentDidUpdate() {
        // this.init()
    }

    init = () => {
        if (this.props.config) {
            // 先对子项做处理
            this.setState({
                formItems: this.itemConfigDeal(this.props.config.itemConfigs),
            })
            // 默认值处理
            this.defaultFormDeal(this.props.config.itemConfigs)
            // 是否需要主动初始化触发
            if (this.props.initiative) {
                this.queryHandle()
            }
            // 表单和按钮区的宽度设置
            if (this.props?.config?.colSpan) {
                this.setState({
                    colSpan: this.props.config.colSpan,
                })
            }
            if (this.props?.config?.formLayout) {
                this.setState({
                    formLayout: this.props.config.formLayout,
                })
            }
            if (this.props?.config?.formItemLayout) {
                this.setState({
                    formItemLayout: this.props.config.formItemLayout,
                })
            }
        }
    }

    /* 子项的处理 */
    itemConfigDeal = (configs) => {
        return configs.map((item, index) => {
            if (item.display === false) {
                return null
            }
            if (!item.type || item.type === 'input') {
                // 普通输入框
                return (
                    <Form.Item
                        label={item.label}
                        name={item.name}
                        key={index}
                        rules={item.rules ? item.rules : []}
                    >
                        <Input
                            style={{
                                width: item.width ? item.width : '220px',
                            }}
                            placeholder={
                                item.placeholder ? item.placeholder : '请输入'
                            }
                            allowClear
                            onChange={(e) => {
                                this.inputOnChange(e, item)
                            }}
                            disabled={
                                item.disabled !== undefined
                                    ? item.disabled
                                    : false
                            }
                            // {...item.attr}
                        />
                    </Form.Item>
                )
            }
            // 数字输入框
            if (!item.type || item.type === 'inputNumber') {
                return (
                    <Form.Item
                        label={item.label}
                        name={item.name}
                        key={index}
                        rules={item.rules ? item.rules : []}
                    >
                        <InputNumber
                            style={{ width: item.width ? item.width : '220px' }}
                            placeholder={
                                item.placeholder ? item.placeholder : '请输入'
                            }
                            allowClear
                            disabled={
                                item.disabled !== undefined
                                    ? item.disabled
                                    : false
                            }
                            // onChange={e => e.target.value = validateNumber(e.target.value)}
                        />
                    </Form.Item>
                )
            }

            // 时间返回选择器
            if (item.type === 'dateRange') {
                return (
                    <Form.Item
                        label={item.label}
                        name={item.name}
                        key={index}
                        rules={item.rules ? item.rules : []}
                    >
                        <DatePicker.RangePicker
                            style={{ width: item.width ? item.width : '220px' }}
                            separator="至"
                            format="YYYY-MM-DD"
                            disabled={
                                item.disabled !== undefined
                                    ? item.disabled
                                    : false
                            }
                        />
                    </Form.Item>
                )
            }
            // 单选下拉框
            if (item.type === 'select') {
                return (
                    <Form.Item
                        label={item.label}
                        name={item.name}
                        key={index}
                        rules={item.rules ? item.rules : []}
                    >
                        <Select
                            style={{ width: item.width ? item.width : '220px' }}
                            allowClear
                            placeholder={
                                item.placeholder ? item.placeholder : '请选择'
                            }
                            disabled={
                                item.disabled !== undefined
                                    ? item.disabled
                                    : false
                            }
                        >
                            {item.option?.length > 0 &&
                                item.option.map((o, i) => {
                                    return (
                                        <Select.Option key={i} value={o.value}>
                                            {o.label}
                                        </Select.Option>
                                    )
                                })}
                        </Select>
                    </Form.Item>
                )
            }

            // 多选下拉框
            if (item.type === 'selectMulti') {
                return (
                    <Form.Item
                        label={item.label}
                        name={item.name}
                        key={index}
                        rules={item.rules ? item.rules : []}
                    >
                        <Select
                            style={{ width: item.width ? item.width : '220px' }}
                            mode="multiple"
                            allowClear
                            placeholder={
                                item.placeholder ? item.placeholder : '请选择'
                            }
                            disabled={
                                item.disabled !== undefined
                                    ? item.disabled
                                    : false
                            }
                            onChange={(value) => {
                                item.onChange && item.onChange(value)
                            }}
                        >
                            {item.option?.length > 0 &&
                                item.option.map((o, i) => {
                                    return (
                                        <Select.Option
                                            key={i}
                                            value={o.value}
                                            maxTagCount="responsive"
                                        >
                                            {o.label}
                                        </Select.Option>
                                    )
                                })}
                        </Select>
                    </Form.Item>
                )
            }
            return ''
        })
    }

    defaultFormDeal = (configs) => {
        configs.forEach((item, index) => {
            if (isEffectVar(item.default)) {
                this.formRef.current.setFieldsValue({
                    [item.name]: item.default,
                })
            }
        })
    }

    /* 重置 */
    resetHandle = () => {
        this.formRef.current.resetFields()
        this.props.resetFn && this.props.resetFn() // 重置的回调
    }

    /* 查询 */
    queryHandle = (extraData) => {
        let data = this.formRef.current.getFieldsValue(true) // 返回的居然是浅拷贝 T^T
        data = deepClone(data)
        this.formRef.current
            .validateFields()
            .then((values) => {
                this.queryDoing(data, extraData)
            })
            .catch((errorInfo) => {
                return false // 返回不出去,默认有undefined判断
            })
    }

    // 查询做的事
    queryDoing = (data, extraData) => {
        // 记录日期格式的子项的key
        let dateTypeKeys = this.props.config.itemConfigs.map((item) => {
            if (item.type === 'dateRange') {
                return item.name
            }
        })
        Object.keys(data).forEach((key) => {
            // 把时间处理成后端需要的入参
            if (!isEffectVar(data[key])) {
                data[key] = ''
            }
            if (dateTypeKeys.includes(key)) {
                let dateArr = [...data[key]]
                if (dateArr.length) {
                    // 原始数据只有null或者[x,x]
                    data[key][0] = moment(dateArr[0]).format('YYYYMMDD')
                    data[key][1] = moment(dateArr[1]).format('YYYYMMDD')
                }
            }
        })
        // 如果必须要返回所有字段,空的就返回undefined
        if (this.props.necessaryField) {
            this.props.config.itemConfigs.forEach((item) => {
                if (!isEffectVar(data[item.name])) {
                    data[item.name] = undefined
                }
            })
        }
        console.log('经过二次加工提交的原生数值', data)
        this.props.queryFn && this.props.queryFn(data, extraData) // 重置的回调
    }

    getData = () => {
        let data = this.formRef.current.getFieldsValue(true) // 返回的居然是浅拷贝 T^T
        data = deepClone(data)
        return data
    }

    validateFields = async () => {
        const res = await this.formRef.current
            .validateFields()
            .catch((err) => null)

        if (!res) {
            return null
        } else {
            return this.getData()
        }
    }

    /* input格式化 */
    inputOnChange = (e, item) => {
        let value = e.target.value
        if (item.formatter === 'int') {
            value = value.replace(/[^\d]/g, '')
        }
        this.formRef.current.setFieldsValue({
            [item.name]: value,
        })
    }

    onFinish = (values) => {
        console.log('Success:', values)
        this.queryHandle()
    }

    onFinishFailed = (errorInfo) => {
        console.log('Failed:', errorInfo)
    }

    setFieldsValue = (data) => {
        console.log('赋值', data)
        this.formRef.current.setFieldsValue({
            ...data,
        })
    }

    render() {
        let { formItems, colSpan, formLayout, formItemLayout } = this.state
        let { btnChildren } = this.props
        return (
            <div className="tfs-container">
                {/* {...formItemLayout} */}
                <Row>
                    <Col span={colSpan[0]}>
                        <Form
                            {...formItemLayout}
                            layout={formLayout}
                            ref={this.formRef}
                            onFinish={this.onFinish}
                            onFinishFailed={this.onFinishFailed}
                            className={
                                'tfs-table' + formLayout === 'horizontal'
                                    ? 'tfs-horizontal-table'
                                    : ''
                            }
                        >
                            {/* 循环渲染子项 */}
                            {formItems.length > 0 &&
                                formItems.map((item) => {
                                    return item
                                })}
                            <Button
                                ref={this.submitRef}
                                style={{ display: 'none' }}
                                htmlType="submit"
                            ></Button>
                        </Form>
                    </Col>
                    {colSpan[1] !== 0 && (
                        <Col span={colSpan[1]}>
                            {/* 按钮区 */}
                            <div className="tfs-btn-container">
                                <Button
                                    type={'link'}
                                    onClick={this.resetHandle}
                                >
                                    重置 <ReloadOutlined />
                                </Button>

                                <Button
                                    type={'primary'}
                                    className="it2-primary-button"
                                    htmlType="submit"
                                    onClick={() => {
                                        this.submitRef.current.click()
                                    }}
                                    loading={this.props.isSearching}
                                >
                                    查询
                                </Button>

                                {/* 按钮插槽 */}
                                {btnChildren ? btnChildren : null}
                            </div>
                        </Col>
                    )}
                </Row>

                {/* 纵向表单的自定义按钮 */}
                {colSpan[1] === 0 && formLayout === 'horizontal' && (
                    <Row>{btnChildren ? btnChildren : null}</Row>
                )}
            </div>
        )
    }
}

所需工具函数

import moment, { isMoment } from "moment";

/* 是否是有效值 */
export function isEffectVar(val) {
    return ![null, "", undefined].includes(val);
}

export function deepClone(target) {
    let result;
    if (typeof target === "object") {
        if (Array.isArray(target)) {
            result = [];
            for (let i in target) {
                result.push(deepClone(target[i]));
            }
        } else if (target === null) {
            result = null;
        } else if (isMoment(target)) {
            result = target.clone();
        } else if (target.constructor === RegExp) {
            result = target;
        } else {
            result = {};
            for (let i in target) {
                result[i] = deepClone(target[i]);
            }
        }
    } else {
        result = target;
    }
    return result;
}

export function validateNumber(value) {
    // 使用正则表达式验证输入值是否为数字
    const reg = /^\d*$/;
    if (!reg.test(value)) {
        // 如果输入值不是数字,则清空输入框
        return '';
    }
    return value;
}

使用配置例子:

formConfig: {
    colSpan: [24, 0],
    formLayout: 'horizontal',
    formItemLayout: {
        labelCol: {
            span: 7,
        },
        wrapperCol: {
            span: 17,
        },
    },
    itemConfigs: [
        {
            label: '场景',
            name: 'signScenType', 
            type: 'selectMulti',
            option: [
                { label: '采购', value: 'P' },
                { label: '销售', value: 'S' },
                { label: '其他', value: 'O' },
            ],
            width: '500px',
            rules: [{ required: true, message: '请输入内容' }],
            onChange: (value) => {
            },
        },
        {
            label: 'aaaaa',
            name: 'signOtherScen', // signOtherScen?
            width: '500px',
            rules: [{ required: true, message: '请输入内容' }],
            display: false,
        },
        {
            label: '签署量(份/年)',
            name: 'signVolume',
            formatter: 'int',
            width: '500px',
            rules: [
                { required: true, message: '请输入内容' },
                ({ getFieldValue }) => ({
                    validator(_, value) {
                        if (value) {
                            if (Number(value) > 1000000000) {
                                return Promise.reject(
                                    new Error('超出最大数值')
                                )
                            }
                        }
                        return Promise.resolve()
                    },
                }),
            ],
        },{
            label: '联系人手机号',
            name: 'contactNumber',
            formatter: 'int',
            width: '500px',
            rules: [{ required: true, message: '请输入内容' }],
        },
        {
            label: 'bbbbbb',
            name: 'FDD',
            width: '500px',
            disabled: true,
            default: 'aaa',
            rules: [{ required: true, message: '请输入内容' }],
        },
    ],
},

配置写的比较倡促,以后来写个完整的

vue的我之前也写过一个vue2版本的组件库例子:【业务组件二次封装】


表单做搜索区与表格怎么做联动

后台中有种页面场景很常见,就是一个表单搜索区+分页表格区+分页区(就如上面例子的图片)。这种组件怎么设计才好,并且尽量做到三者之间解耦。

还是用react举例(需要配合ahooks第三方库):

我们去设计一个这样的页面时,会用一个容器组件,里面引入表单组件,表格组件和分页组件。表单组件的值驱动着表格数据的获取(分页组件待会再说),我们首先看看表单组件怎么驱动最好。

咱们就举例一个最简单的表单,就只有一个搜索输入项,我们想做到的是:

  • 刷新页面,表单原来填写的值能够回显
  • 容器组件和表格组件都能拿到表单的值,且尽量解耦

如果我们把每次搜索时表单的值都更新在url上,其他组件通过router的hook就可以拿到表单值,页面刷新后表单组件也能获取到表单值进行回填。

import React, { FC, useEffect, useState } from 'react'
import type { ChangeEvent } from 'react'
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
import { Input } from 'antd'
import { LIST_SEARCH_PARAM_KEY } from '../constant'

const { Search } = Input

const ListSearch: FC = () => {
  const nav = useNavigate()
  const { pathname } = useLocation()

  // 输入值
  const [value, setValue] = useState('')
  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setValue(event.target.value)
  }

  // 获取 url 参数,回填表单数据
  const [searchParams] = useSearchParams()
  useEffect(() => {
    const curVal = searchParams.get(LIST_SEARCH_PARAM_KEY) || ''
    setValue(curVal)
  }, [searchParams])

  // 点击搜索
  function handleSearch(value: string) {
    // 跳转页面,增加 url 参数
    nav({
      pathname,
      search: `${LIST_SEARCH_PARAM_KEY}=${value}`, // 去掉了 page pageSize
    })
  }

  return (
    <Search
      allowClear
      placeholder="输入关键字"
      value={value}
      onChange={handleChange}
      onSearch={handleSearch}
    />
  )
}

export default ListSearch

除了表单组件驱动表格数据,还有分页组件的分页数据也驱动着表格数据的获取,驱动方式也和表单组件一样即可:

import React, { FC, useEffect, useState } from 'react'
import { Pagination } from 'antd'
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom'
import { LIST_PAGE_SIZE, LIST_PAGE_PARAM_KEY, LIST_PAGE_SIZE_PARAM_KEY } from '../constant/index'

type PropsType = {
  total: number
}

const ListPage: FC<PropsType> = (props: PropsType) => {
  const { total } = props
  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(LIST_PAGE_SIZE)

  // 从 url 参数中找到 page pageSize ,并且同步到 Pagination 组件中
  const [searchParams] = useSearchParams()
  useEffect(() => {
    const page = parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || '') || 1
    setCurrent(page)
    const pageSize = parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || '') || LIST_PAGE_SIZE
    setPageSize(pageSize)
  }, [searchParams])

  // 当 page pageSize 改变时,改变 url 对应参数
  const nav = useNavigate()
  const { pathname } = useLocation()
  function handlePageChange(page: number, pageSize: number) {
    searchParams.set(LIST_PAGE_PARAM_KEY, page.toString())
    searchParams.set(LIST_PAGE_SIZE_PARAM_KEY, pageSize.toString())

    nav({
      pathname,
      search: searchParams.toString(), // 除了改变 page pageSize 之外,其他的 url 参数要带着
    })
  }

  return (
    <Pagination current={current} pageSize={pageSize} total={total} onChange={handlePageChange} />
  )
}

export default ListPage

我们可以把表格数据的获取放在容器组件中,只需要监听路由的变化即可,还可以专门抽离成一个hook:

import { useSearchParams } from 'react-router-dom'
import { useRequest } from 'ahooks'
import { getQuestionListService } from '../services/question'
import {
  LIST_SEARCH_PARAM_KEY,
  LIST_PAGE_PARAM_KEY,
  LIST_PAGE_SIZE_PARAM_KEY,
  LIST_PAGE_SIZE,
} from '../constant/index'

// 可能多个表单表格页面功能是一样的,可以复用,用传入的类型来区分,但是为了业务拓展性,建议还是分开写hook
type OptionType = {
  isStar: boolean
  isDeleted: boolean
}

function useLoadQuestionListData(opt: Partial<OptionType> = {}) {
  const { isStar, isDeleted } = opt
  const [searchParams] = useSearchParams()

  const { data, loading, error, refresh } = useRequest(
    async () => {
      // 从url中解构出入参
      const keyword = searchParams.get(LIST_SEARCH_PARAM_KEY) || ''
      const page = parseInt(searchParams.get(LIST_PAGE_PARAM_KEY) || '') || 1
      const pageSize = parseInt(searchParams.get(LIST_PAGE_SIZE_PARAM_KEY) || '') || LIST_PAGE_SIZE

      const data = await getQuestionListService({ keyword, isStar, isDeleted, page, pageSize })
      return data
    },
    {
      refreshDeps: [searchParams], // 刷新的依赖项
    }
  )

  return { data, loading, error, refresh } // 返回接口数据、是否正在请求中的状态、错误情况、手动更新函数(参数不变)
}

export default useLoadQuestionListData

代码设计来自双越老师的视频


推荐的表单库

react中推荐react-hook-form,formik,这俩个都很强大,但是其实antd提供的表单已经能满足大部分的需求了。

vue目前我只知道饿了么。


关于低代码表单

这玩意个人认为只能用于非常固定的业务场景,例如一般的问卷调查,他就是非常固定的一些表单输入项,后期也不会加复杂的东西,那就非常适合用低代码去搭一个后台。

我还遇到过一个用法,就是之前待过的一个公司里后台管理很多表单用的也是低代码生成的。机制是这样的这些表单配置都是在后台配出来的,然后前端每次渲染页面的时候会通过接口拉取表单的json配置数据,前端开发人员把配置数据传入对应的低代码组件,然后这个低代码组件有一些拓展功能要开发人员自己调试。这样做出来的表单的一个好处是,当项目上线了,突然要改表单的某些选项,直接后台通过低代码把对应的输入项做调整就可以了,完全不用再去改项目代码,反应迅速。

掘金上有篇讨论可以看看【低代码:现在我怎么样了】

关于低代码的开源项目参考:

  • 这个比较成熟商用级别的:https://lowcode-engine.cn/index
  • 这个比较适合用来学习简单低代码的搭建:https://buqiyuan.gitee.io/vite-vue3-lowcode/#/

关于动态表单

动态表单我个人的方案是【业务组件二次封装】里,把所有要显示的表单项id维护在一个数组中,公共组件根据这个数组去显示对应的表单。

具体可以看PzFormGeneral.vue里的displayList部分

这种方式的好处就是很灵活,基本满足所有场景,不好的地方就是每个能触发动态表单切换的表单项都要维护一个数组,稍显麻烦。

另外一种方式是从渡一教育哪里看过来的,每个组件的配置都有一个属性,这个属性是一个函数,他内部处理对应逻辑返回下一个要显示或者隐藏的表单项配置。

就类似链表的设计了,好处就是简单,都维护在配置项中,不好的地方在于链表关系,无法做到第2项影响第4项之后的表单项。


一些数据上的处理技巧

数组转字符串

有时候我们表单中的某个输入项拿到的值是一个数组,但是后端需要的是用逗号分开区分的字符串。咱们可以直接:

let arr = [1,2,3]
console.log(arr.toString()) // '1,2,3'
console.log(arr.join(',')) // '1,2,3'

日期的转换

咱们用组件库表单的数据获取api拿到的日期输入项的数据一般都是带有组件定义的格式的,例如ant-design拿到的一般是个Moment对象,后端可能需要的是yyyy-dd-mm的格式,这些我们都可以提前准备好公共函数统一处理的。

类似的不止日期啦,学会举一反三文章来源地址https://www.toymoban.com/news/detail-470791.html

到了这里,关于【react框架】结合antd做表单组件的一些心得记录的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 在react antd中动态生成多个 form表单组,包括一个动态添加/删除表单项的功能和一个提交表单的功能

    在这个示例中,我们首先使用 Form.useForm() 创建一个表单实例。接着,我们使用 Form.List 组件来动态生成多个表单项。在 Form.List 组件中,我们使用 fields.map 方法循环渲染每个表单项,并使用 Form.Item 组件包裹每个表单项。在 Form.Item 组件中,我们使用 label 属性指定标签,使用

    2024年02月15日
    浏览(57)
  • React修改Antd组件的样式

    修改默认的antd组件,需要使用 global 修改后Steps样式 为什么需要这样写呢? 因为我们启动了 CSS Modules ,它是一种技术流的组织css代码的策略,它将为css提供默认的局部作用域。因为构建工具会在编译的时候自动把我们的类名加上一个哈希字符串,例如上面我们写的类名为t

    2024年02月11日
    浏览(54)
  • React antd upload组件上传视频并实现视频预览

    记录问题:antd的upload组件文档中对于视频的上传预览没有明确的文档demo,在这里记录一下 项目需求:支持图片及视频的上传并实现预览,点击上传后不会立即请求接口上传资源,后续点击确定再上传 上代码

    2024年02月04日
    浏览(46)
  • React + Typescript + Antd:封装通用的字典组件DXSelect

    在开发中,我们经常遇到这样的场景,在表单中,有个下拉框,选择对应的数据。 那么这个下拉框的选项,就是字典。一搬的做法是,通过antd的Select来实现,代码如下:

    2024年02月15日
    浏览(79)
  • 【react + antd】antd如何自定义请求使用antd的upload组件实现图片上传且可预览可删除

    官网给出的案例无法使用封装好的请求方式上传图片,以及 无法满足上传图片后获取接口url、名称等信息的的业务需求 。这个时候需要用到customRequest这个api。 但是很遗憾,官网没有给出具体案例。 不过——博主自己试出来了( ̄︶ ̄) 要使用upload,特别重要的属性就是file

    2024年02月17日
    浏览(52)
  • react Hook+antd封装一个优雅的弹窗组件

    前言 在之前学vue2的时候封装过一个全局的弹窗组件,可以全局任意地方通过this调用,这次大创项目是用react技术栈,看了一下项目需求,突然发现弹窗还是比较多的,主要分为基础的弹窗以及form表单式的弹窗,如果只是无脑的去写代码,那些项目也没啥必要了。正好react和

    2024年02月13日
    浏览(41)
  • React使用antd的图片预览组件,点击哪个图片就预览哪个的设置

    使用了官方推荐的相册模式的预览,但是点击预览之后,每次都是从图片列表的第一张开始预览,而不是点击哪张就从哪张开始预览: 所以这里我就封装了一下,对初始化预览的列表进行了逻辑处理: 当点击开始预览的时候,要找到当前图片在预览图列表中的索引,然后设

    2024年02月13日
    浏览(47)
  • React antd如何实现<Upload>组件上传附件再次上传已清除附件缓存问题

    最近遇到一个React上传组件的问题,即上传附件成功后,文件展示处仍然还有之前上传附件的缓存信息,需要解决的问题是,要把上一次上传的附件缓存在上传成功或者取消后,可以进行清除 经过一顿试错,终于解决了这个问题。 showUploadList,是可选参数,即是否展示upload

    2024年02月04日
    浏览(59)
  • 【React】如何简单快速地修改antd组件UI内部样式如字体颜色

    最近刚开始学习react 在写一个登录的页面 发现组件的颜色不太合适,默认是黑色字体 那我想修改成白色字体以适应我的页面 运用多种css文件打包策略太过复杂 对我这种小白不友好 两行代码搞定 实现需求 通过:global加上!important 在Umi项目中,在global.less文件夹下面,通过roo

    2024年02月13日
    浏览(48)
  • react18+antd5.x(1):Notification组件的二次封装

    antdesign已经给我们提供了很好的组件使用体验,但是我们还需要根据自己的项目业务进行更好的封装,减少我们的代码量,提升开发体验 开起来和官网的使用没什么区别,但是我们在使用的时候,进行了二次封装,更利于我们进行开发 MyNotification.jsx,是我们的业务页面 Notif

    2024年02月11日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包