实现动态表单

需求背景

项目是一个电商类站点。

售卖的商品都是虚拟化物品或服务。

当用户在购买站点售卖商品时,需要填写一些信息。

而根据商品类目的不同,所需要填写的信息也会有所不同。

比如在商品类目 A,用户需要填写邮箱,名称即可。

而在商品类目 B,用户需要填写邮箱,平台,平台账号,平台密码等信息。

随着商品类目的增加,所需要的信息收集表单也会越来越多。

目前,这些信息收集表单都是由前端开发人员手动维护的。

这会导致以下问题:

  • 表单创建依赖开发人员,随着商品类目增加,开发成本和维护成本将越来越高。
  • 表单出错或不完整,运营人员无法及时更改,导致用户无法购买商品。
  • 因为极度依赖开发人员进行配置,会导致商品上架将无法及时响应,让业务运营受限。

因此,我们需要一个通用的信息收集表单,可以让运营人员自行配置,而无需开发介入。

需求分析

需求目标

构建一个可动态配置的信息收集表单系统。可让运营人员自行配置,而无需开发人员介入。

需求详情

  • 可以动态配置表单字段。
  • 支持常见的表单选项,如文本框,单选框,复选框,下拉框等。
  • 可以配置选项的属性,如占位提示,格式,长度等。
  • 将是可复用,可拓展的,便于后续增加新选项类型。
  • 支持表单验证。

预期效果

除需要增加新的选项类型外,其他的功能都可以复用,无需开发人员介入。运营人员可以自由配置表单内容,可以随时更改。运营人员上新更加便捷,可以自行操作,加速商品上架。

设计方案

基于以上需求,设计方案如下:

  • 放弃使用 state 管理字段数据,改而使用浏览器原生的表单进行收集数据。
  • 每个表单组件只会接收 props 配置,不接收 callback 回调。
  • 组件字段数据将由浏览器原生的表单管理。
  • 通过监听 submit 事件,获取表单信息和创建订单。

将表单划分为四个层级:

  • Form
    • 是整个表单的根组件。
    • 负责表单渲染,表单数据收集,表单验证,表单提交等。
    • 表单排列遵从 行列 格式。
  • Form Row
    • 是表单的行。
    • 仅负责行的排列渲染。
    • 不参与数据收集,验证提交和具体组件实现。
  • Form Column
    • 是表单的列。
    • 仅负责列的排列渲染。
    • 不参与数据收集,验证提交和具体组件实现。
    • 列内容为:前缀组件,内容组件,后缀组件三部分。
  • Form Item
    • 是表单的具体组件。
    • 负责具体组件的功能实现。
    • 仅接受 props 配置。
    • 不接受 callback 回调。

设计实现

因为需要监听 submit 事件来获取表单信息和创建订单。而在页面中,表单和提交按钮并不在同个位置。是分开的。

因此,将使用 form 元素,包裹整个页面,让表单和提交按钮都在 form 元素中。

但页面中布局会比较复杂,因此,使用了占位符技术,当渲染页面时,表单将替代占位符。

以上功能被封装。

下面是简单示例:

import { Form, FormProvider } from 'dynamicform'

function PageContent(props) {
  const onSubmit = (submitter, data) => {
    // do something...
  }
  const onChange = (formData) => {
    // do something...
  }

  return (
    <Form
      rows={formConfig}
      CustomColumn={CustomColumn}
      onSubmit={onSubmit}
      onChange={onChange}
    >
      <PageHeader />
      <main>
        <aside>
          <Form.Placeholder />
        </aside>
        <div>
          支付明细
          <button type='submit'>提交</button>
        </div>
      </main>
      <PageFooter />
    </Form>
  )
}

export default function Page(props) {
  return (
    <FormProvider>
      <PageContent {...props} />
    </FormProvider>
  )
}

以上例子中:

  • rows 属性是表单的配置。
  • CustomColumn 属性是自定义的表单列组件。
  • onSubmit 属性是表单提交时的回调。
  • onChange 属性是表单数据变化时的回调。

表单配置示例

一份简单的,含有用户邮箱和用户手机号,采用一行两列排列的表单配置如下:

;[
  {
    id: '1743386518708',
    gap: 10,
    columns: [
      {
        id: '1743386518709',
        width: '50%',
        prepend: {
          id: '1743386518710',
          type: 'icon',
          props: {
            alt: 'email icon',
            icon: 'https://image.xxx.xx/email.png',
          },
        },
        component: {
          id: '1743386518711',
          type: 'input',
          props: {
            name: 'email',
            placeholder: 'Email',
            mode: 'email',
          },
        },
      },
      {
        id: '1743386518712',
        width: '50%',
        prepend: {
          id: '1743386518713',
          type: 'icon',
          props: {
            alt: 'phone icon',
            icon: 'https://image.xxx.xx/phone.png',
          },
        },
        component: {
          id: '1743386518714',
          type: 'input',
          props: {
            name: 'phone',
            placeholder: 'Phone',
            mode: 'tel',
          },
        },
      },
    ],
  },
]

自定义列

在四个层级中,我们可以仅关注 Form Column 和 Form Item 这两层。而 Form
Row 和 Form 这两层,我们可以不关注,交由内部自动处理。

自定义列的代码如下:

import Image from 'next/image'
import { lazy, Suspense } from 'react'

const components = {
  input: lazy(() => import('./Input')),
  checkbox: lazy(() => import('./Checkbox')),
  icon: lazy(() => import('./Icon')),
  select: lazy(() => import('./Select')),
  radios: lazy(() => import('./Radios')),
}

const isNotEmptyProps = (obj) => obj && obj?.props && Object.keys(obj.props).length > 0

const transformList = (list) => (Array.isArray(list) ? list : list ? [list] : [])

const filterComponent = (list) => transformList(list).filter(isNotEmptyProps)

const renderComponent = (data) =>
  transformList(data).map((component, index) => {
    const Component = components[component.type]
    return Component ? (
      <Component
        key={component.id || `${component.type}-${index}`}
        {...component.props}
      />
    ) : null
  })

export default function Column(props) {
  const prepend = filterComponent(props.prepend)
  const append = filterComponent(props.append)
  const component = filterComponent(props.component)
  const hasInput = component.some((component) => component.type === 'input')
  const needBackground = hasInput || component.some((component) => ['input', 'select'].includes(component.type))

  return (
    <div
      className={`cart-form-custom-column relative my-2 ${props.width === '100%' ? '' : 'only:mr-1 only:pr-1'}`}
      style={{ width: props.width }}
    >
      <span className='text-nowrap text-[13px] text-yellow-300'>{props.tips}</span>
      <div
        className={
          'relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center justify-between gap-1 rounded-md ' +
          (needBackground ? 'bg-[#151825] px-2 focus:bg-[#151825]' : '')
        }
      >
        <Suspense fallback={<></>}>{renderComponent(prepend)}</Suspense>
        <Suspense fallback={<></>}>{renderComponent(component)}</Suspense>
        <Suspense fallback={<></>}>{renderComponent(append)}</Suspense>
      </div>
    </div>
  )
}

在自定义列中,负责了以下功能:

  • 接收 props 配置。
  • 处理 props 并提取出 prependappendcomponent 三个部分。
  • 渲染 prependappendcomponent 三个部分。

自定义表单组件

在自定义列中,我们已经将 prependappendcomponent 三个部分都渲染出来了。

而这三个部分内容都是一致的。因此,我们只需要实现一次即可。

这里我们以 Input 组件为例,实现代码如下:

import { memo } from 'react'

export default memo(function Input(props) {
  return (
    <input
      name={props.name}
      type={props.mode}
      placeholder={props.placeholder}
      min={props.min}
      max={props.max}
      minLength={props.minLength}
      maxLength={props.maxLength}
      required
      pattern={props.pattern}
      className='h-[45px] w-auto bg-transparent px-1 text-[#fff] placeholder:text-[16px] placeholder:text-[#8392B1] autofill:bg-transparent focus:outline-none'
    />
  )
})

可以看到,Input 组件仅需接收 props 配置即可。而不需要使用 state 管理状态,不需要使用 callback
回调传递数据。这些功能都由浏览器原生的表单管理。

总结

以上就是支付页动态表单的实现。通过以上方案,我们可以实现一个可动态配置的信息收集表单系统。可以让运营人员自行配置,而无需开发人员介入。同时,我们也可以通过自定义列和自定义表单组件,实现更多的功能。


实现动态表单
http://www.inksha.com/archives/shi-xian-dong-tai-biao-dan
作者
inksha
发布于
2025年04月01日
许可协议