动态表单

一个基于 Vue3 的动态表单组件,支持自定义表单配置、验证规则和样式,适用于分步注册、分步信息收集等场景

示例

NOTE

  • 当表单项 ① 选择为公司 1 ----> 表单项 ② 为 input 输入框
  • 表单项 ① 选择为公司 2 ----> 表单项 ② 为 select 下拉选择框
  • 表单项 ① 选择为公司 3 ----> 表单项 ② 为 select 下拉选择框,下拉框赋值为默认物料并禁止选择
  • 表单项 ① 选择为公司 4 ----> 表单项 ② 为 select 下拉选择框,选项内取消默认物料
  • 当表单项 ② 选择为默认物料 ----> 展示表单项 ③ 上传组件和表单项 ④checkbox 选择框
  • 表单项 ④ 同时选择选项 2 和选项 3 ----> 展示表单项 ⑤ 文本域输入框
  • 表单项 ⑤ 输入数字 ----> 展示表单项 ⑥ 自定义表单项
loading

配置项说明:

DynamicForm 组件 Props

参数类型必填默认值说明
formStatusobjectnull表单配置对象,通过 createFormItem 创建的表单项,只传入第一个 createFormItem 生成的表单项就行,后续通过next函数返回下一个表单项
formStateobject{}表单数据对象,必须是响应式对象(ref 或 reactive)
formConfigobjectundefinedElement Plus 的 Form 组件配置项,如 rules、label-width 等(如果不传入,组件只渲染 el-form-item,当前组件最外层需要使用 el-form 包裹,如果传入,组件内部会自动使用 el-form 包裹)
layoutConfigobject{ cols: 1, gutter: 20 }表单布局配置,包含 cols(列数)和 gutter(间距)

createFormItem 函数参数配置

参数类型必填说明
typestring | function | null表单项类型,可以是字符串或返回字符串的函数。支持:'input'、'select'、'checkbox'、'radio'、'datePicker'、'inputNumber'、'switch'、'upload'、'rate'、'timeSelect'。如果设置为 null 或不设置,可以通过 payload.customSlot 传入自定义组件
payloadobject表单项的配置信息
nextfunction返回下一个表单项的函数,返回 null 表示没有下一项, 函数的形参current是当前表单项的配置信息, 可以通过current.parent获取到上一个表单项

payload 配置项

参数类型必填说明
labelstring表单项标签文本
propstring表单项的属性名,对应 formState 中的键名
valueany表单项的默认值
spannumber表单项占据的列宽(1-24),不设置时根据 layoutConfig.cols 自动计算(比如 layoutConfig.cols 设置的 3,默认就是一行三个,所有表单项 span 默认为 8,如果同一行的所有表单项 span 相加大于 24,会自动进行换行)
idstring表单项的唯一标识,用于 getAcientById 获取表单项
attrobject对应 Element Plus 官方组件的所有 props 配置项(比如 input 组件的 placeholder,clearable 等)
optionsarray | computed选项数组,用于 select/checkbox/radio 等组件,每项格式:{ label, value, attr? }
formItemAttrobject对应 ElFormItem 组件的所有配置项(比如 label-width,label-position 等)
slotobject自定义插槽配置,格式:{ [slotName]: ({ props, payload }) => VNode }(比如 input 组件的 prepend,append 等)
customSlotfunction自定义组件渲染函数,当 type 为 null 或不设置时生效,格式:({ props, payload }) => VNode

layoutConfig 配置项

参数类型必填默认值说明
colsnumber1每行显示的列数
gutternumber20列间距(px)

options 数组项配置

参数类型必填说明
labelstring选项的显示文本
valueany选项的值
attrobject选项对应的 Element Plus 组件属性配置

配置选项

createFormItem 参数

参数类型必填说明
typestring | null表单项类型,对应 Element Plus 组件名称,如 input、select 等。设置为 null 时可使用自定义组件
payloadobject表单项配置对象
nextfunction联动函数,当前表单项值变化时触发

payload 配置项

参数类型必填说明
labelstring表单项标签
propstring表单项属性名,用于收集表单数据
valueany表单项初始值
spannumber表单项占据的列数,默认为 1
attrobject传递给 Element Plus 组件的属性
rulesarray表单验证规则
slotobject插槽配置,可以包含 default、prefix、suffix 等
customSlotfunction自定义渲染函数,用于完全自定义表单项的渲染
idstring表单项唯一标识,用于获取祖先表单项
hiddenboolean | function是否隐藏表单项,可以是布尔值或返回布尔值的函数

layoutConfig 配置项

参数类型必填说明
colsnumber表单布局的列数,默认为 3
labelWidthstring | number表单项标签宽度
labelPosition'left' | 'right' | 'top'表单项标签位置
size'large' | 'default' | 'small'表单项尺寸

1. 基础使用

1.1 组件引入

javascript
import DynamicForm from './components/DynamicForm'
import { createFormItem } from './components/DynamicForm'

1.2 基本用法

vue
<template>
  <el-form :model="formState" label-width="100px">
    <!-- form-status 是第一个createFormItem生成的表单项,后续通过next函数返回下一个表单项 -->
    <DynamicForm :form-status="InputItem" :form-state="formState" :layout-config="layoutConfig" />
  </el-form>
</template>

<script setup>
import { ref } from 'vue'

const formState = ref({}) // 表单数据对象
const layoutConfig = {
  cols: 2, // 每行显示的列数
  gutter: 20 // 列间距
}
const InputItem = createFormItem({
  type: 'input',
  payload: {
    // ...配置
  },
  next: current => (current.payload.value === 'showSelect' ? SelectItem : null) // 下一个表单项
})
const SelectItem = createFormItem({
  type: 'select',
  payload: {
    // ...配置
  },
  next: current => null // 下一个表单项
})
</script>

2. 表单项配置

组件内部已经处理了当前支持的所有表单项v-model的双向绑定,比如 upload 组件的v-model:file-list,直接在 payload 中配置 value 即可,不需要再手动绑定,监听事件直接在payload.attr中配置即可,比如@change事件,需要写成onChange

2.1 基础表单项

javascript
// 基础配置
const basicInput = createFormItem({
  type: 'input',
  payload: {
    label: '输入框:',  // 标签文本
    prop: 'inputField',  // 表单项的属性名
  }
  next: current => null  // 下一个表单项
})
// 基础表单项完整配置
const basicSelect = createFormItem({
  type: 'select',  // 表单项类型
  payload: {
    label: '下拉选择框:',  // 标签文本
    prop: 'selectField',  // 表单项的属性名
    id: 'selectField',  // 表单项的唯一标识,用于getAcientById获取表单项
    span: 12,  // 表单项占据的列宽(1-24)
    value: '',  // 默认值
    attr: {  // el官方组件属性
      placeholder: '请选择',
      clearable: true,
      onChange: (value) => {
        console.log('onChange', value)
      }
    },
    // 只有select、checkbox、radio组件需要配置options,其他组件不需要配置
    options: [
      { label: '选项1', value: '1' },
      { label: '选项2', value: '2' }
    ],
    formItemAttr: {
      labelWidth: '100px'
    },
    slot: {
      prepend: ({ props, payload }) => {
        return <ElIcon><Edit /></ElIcon>
      }
    }
  },
  next: current => null  // 下一个表单项
})

2.2 支持的表单项类型

  • input: 输入框
  • select: 下拉选择框
  • checkbox: 多选框
  • radio: 单选框
  • datePicker: 日期选择器
  • inputNumber: 数字输入框
  • switch: 开关
  • upload: 上传组件
  • rate: 评分组件
  • timeSelect: 时间选择器

2.3 动态类型

javascript
const dynamicInput = createFormItem({
  type: formStatus => {
    // 根据条件返回不同的组件类型
    return formStatus.parent.payload.value === '100' ? 'input' : 'select'
  },
  payload: {
    // ...配置
  }
})

3. 表单联动

3.1 基于 next 函数的联动

javascript
const firstItem = createFormItem({
  type: 'select',
  payload: {
    // ...配置
  },
  next: current => {
    // 根据当前值决定下一个表单项
    if (current.payload.value === 'showNext') {
      return secondItem
    }
    return null
  }
})

3.2 基于 computed 的动态属性

注意,动态属性需要使用computed,否则不会自动更新

javascript
import { computed } from 'vue'

const dynamicItem = createFormItem({
  type: 'select',
  payload: {
    attr: computed(() => ({
      disabled: someCondition,
      placeholder: `请${someCondition ? '输入' : '选择'}内容`
    })),
    options: computed(() => {
      // 动态选项
      return [
        /* ... */
      ]
    })
  }
})

4. 布局配置

4.1 全局布局

已经做了自动换行的处理,比如cols设置为 2,默认一行两个表单项,如果这两个表单项设置的span相加大于 24,会自动进行换行

javascript
const layoutConfig = {
  cols: 2, // 每行显示的列数
  gutter: 20 // 列间距
}

4.2 单个表单项布局

javascript
const wideItem = createFormItem({
  type: 'input',
  payload: {
    span: 24 // 占据整行,不设置时根据layoutConfig.cols自动计算
    // ...其他配置
  }
})

5. 表单验证

同官方组件的使用方式,可以配置el-formrules,也可以在payload.formItemAttr.rules配置每个表单项的rules

javascript
const validatedItem = createFormItem({
  type: 'input',
  payload: {
    formItemAttr: {
      rules: [
        { required: true, message: '请输入内容', trigger: 'blur' },
        { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
      ]
    }
  }
})

6. 自定义插槽

javascript
const uploadItem = createFormItem({
  type: 'upload',
  payload: {
    slot: {
      default: ({ props, payload }) => {
        return (
          <ElButton type='primary'>
            <ElIcon>
              <UploadFilled />
            </ElIcon>
            <span>点击上传</span>
          </ElButton>
        )
      }
    }
  }
})

7. 自定义组件

如果需要使用自定义组件,可以将 type 设置为 null 或不设置,然后通过 payload.customSlot 传入自定义组件

javascript
const customItem = createFormItem({
  type: null, // 或者不设置 type
  payload: {
    label: '自定义组件:',
    prop: 'custom',
    value: '',
    customSlot: ({ props, payload }) => {
      // props 包含了通过 createBaseProps 处理后的属性,包括 v-model 的处理
      // payload 是表单项的完整配置信息
      return (
        <div class='custom-component'>
          <MyCustomComponent v-model={props.modelValue} {...props} />
          <div class='custom-footer'>
            <ElButton>自定义按钮</ElButton>
          </div>
        </div>
      )
    }
  }
})

8. 高级特性

8.1 祖先表单项关联

通过设置id属性,可以获取到任意祖先表单项,比如获取到formStatus

javascript
const item = createFormItem({
  payload: {
    id: 'unique-id' // 设置唯一ID
  }
})

// 在其他地方通过 ID 获取表单项
import { getAcientById } from './DynamicForm'
const relatedItem = getAcientById('unique-id')

注意事项

  1. 响应式处理

    • formState 必须是响应式对象(使用 refreactive
    • 动态属性(如 attr)建议使用 computed 处理,以提高性能
    • 表单项的值变化会自动同步到 formState
  2. 表单项配置

    • 表单项的 prop 属性必须唯一
    • id 在使用 getAncestorById 时必须唯一
    • hidden 属性支持动态计算,可以是布尔值或返回布尔值的函数
  3. 联动处理

    • next 函数在表单项值变化时触发
    • 避免在 next 函数中创建循环引用
    • 复杂的联动逻辑建议使用 computed 属性
  4. 性能优化

    • 使用 computed 处理复杂的动态属性
    • 避免在 next 函数中进行耗时操作
    • 合理使用 span 属性控制表单项布局
  5. 组件使用

    • Element Plus 组件会被异步加载
    • 自定义组件需要全局注册或在使用时导入
    • 使用插槽时注意作用域插槽的数据结构
  6. 表单验证

    • 支持 Element Plus 的所有验证规则
    • 可以通过 formRef.value.validate() 触发表单验证
    • 验证规则支持异步验证函数
  7. 样式定制

    • 可以通过 layoutConfig 统一配置表单样式
    • 支持通过 CSS 变量覆盖默认样式
    • 自定义组件可以使用 scoped 样式

js 组件

loading