结合 UI 框架实现可配置 Vue 表单组件浅析

—— 封面摄于济州岛民宿

常规的表单

如果我们用 UI 框架做管理系统时候,关于表单的代码我们不会陌生,大致是这样的,比如这是一个 iView 框架下的综合性表单:

<template>
    <Form :model="formItem" :label-width="80">
        <FormItem label="Input">
            <Input v-model="formItem.input" placeholder="Enter something..."></Input>
        </FormItem>
        <FormItem label="Select">
            <Select v-model="formItem.select">
                <Option value="beijing">New York</Option>
                <Option value="shanghai">London</Option>
                <Option value="shenzhen">Sydney</Option>
            </Select>
        </FormItem>
        <FormItem label="Radio">
            <RadioGroup v-model="formItem.radio">
                <Radio label="male">Male</Radio>
                <Radio label="female">Female</Radio>
            </RadioGroup>
        </FormItem>
    </Form>
</template>
<script>
    export default {
        data () {
            return {
                formItem: {
                    input: '',
                    select: '',
                    radio: 'male'
                }
            }
        }
    }
</script>
复制代码

配置化表单

而我想要的方式是这样的:

模板
<template slot="modalContent">
    <AutoForm
        :fileds="projectFields"
        :model="projectFormData"
        :formName="projectFormData"
        class="my-form"
    />
</template>
<script>
// @ is an alias to /src
import { mapState } from 'vuex'
import { projectFields } from '@/utils/fieldsMap'
export default {
    data ()  {
        return {
            // 表单配置列表
            projectFields: projectFields
        }
    },

    computed: {
        ...mapState({
          // 项目列表页编辑表单
          projectFormData: state => state.project.projectFormData
        })
    },
}
</script>
复制代码

表单项的数据来源我会利用 Vuex 的 state 里管理:

数据
import { projectFormData } from '@/api/project'

const state = {
      // 项目列表页编辑
      projectFormData: {
        projectInput: '',
        projectSelect: '',
        projectRadio: ''
      }
}

// getters
const getters = { }

// action
const actions = {
    // 表单项数据获取
    // 表单项数据提交
}

// mutations
const mutations = { }

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}
复制代码

表单项的配置也是通过单文件(fieldsMap.js)管理,方便维护:

表单项配置
// 表单配置项
// 注意:tag 和 type 需要根据使用的 UI 框架来匹配。
const projectFields = {
    projectInput: {
        label: '项目Input',
        tag: 'Input',
        type: 'text',
        placeholder: '请输入项目Input'
    },
    projectSelect: {
        label: '项目下拉Select',
        tag: 'Select',
        options: [
          {
            key: 'beijing',
            value: 'beijing'
          },
          {
            key: 'hangzhou',
            value: 'hangzhou'
          }
        ]
    },
    projectRadio: {
        label: '项目Radio',
        tag: 'RadioGroup',
        options: [
          {
            label: '是'
          },
          {
            label: '否'
          }
        ]
    },
}
复制代码

OK,整个一个配置表单的文件结构,使用方式就是这样子,总结一下大致是三部曲:

  • 引入 <AutoForm /> 组件。
  • fieldsMap.js 中配置表单项,包括 label、type、tag、options等。
  • Vuex state 中添加数据来源。

剩下的关键是 <AutoForm /> 组件是如何实现配置化,其实本质是动态生成表单项(根据配置文件)的过程,对于 iView 来说,就是动态的生成 FormItem,来拼成一个完整的表单。这时我们就需要用到 vue 提供的 render Api了。

首先查看一下官方文档 render 截图:

render

三个参数的简单用法:

<script>
    Vue.component('Line', {
        render: function(h) {
            h('div', {
                props: {} // 传递数据
            },'文本 or 子节点')
        }
    })
</script>
复制代码

了解基础用法后,我们来看下 <AutoForm /> 组件的实现:

在上代码之前,我们先看一下 iView 表单的结构,从外层到内层,Form 容器固定——FormItem 数量动态——Input 类型动态,组件最终是返回一个 Form;根据配置项的数量来决定 FormItem 的数量,动态创建;根据配置项的 tagtype 来决定表单的类型;当然有些例如 Select 的表单项会有 options 下拉选项,也需要单独生成。

根据上面的分析,那总结关于这个 <AutoForm /> 组件,大致有 FormRenderitemsRendercomponentUse、类型(InputRenderRadioRenderSelectRender)、options (optionsRender) 五个点。

AutoForm.vue
<script>
export default {
  name: 'Form',
  functional: true,
  render (h, context) {
    let fileds = context.props.fileds // 表单配置 from fieldsMap.js
    let model = context.props.model // 表单数据 from state
    let formName = context.props.formName // 表单名称唯一
    /**
     * 渲染 FormItem
     */
    function itemsRender () {
      let res = []
      // 遍历配置项动态生成 FormItem
      Object.keys(fileds).forEach((ele, i) => {
        res.push(
          h('FormItem',
            {
              props: {
                label: fileds[ele].label // FormItem label 属性
              }
            },
            componentUse(fileds[ele], ele) // 子节点表单类型,利用 componentUse 函数控制
          )
        )
      })

      return res
    }

    /**
     * 表单分发选择
     * @param { Object } _item - 当前 fields 配置项
     * @param { String } _model - 当前配置项名
     */
    function componentUse (_item, _model) {
      let typeMap = {
        'Input': InputRender,
        'RadioGroup': RadioRender,
        'Select': SelectRender
      }
      let component = typeMap[_item.tag](_item, _model)

      return [component]
    }

    // Input
    function InputRender (_item, _model) {
      return h('Input',
        {
          props: {
            'v-model': `${formName}.${_model}`,
            'placeholder': _item.placeholder,
            'type': _item.type
          },
          on: {
            // iView 组件提供的方法,实现数据双向绑定
            'on-blur': (e) => {
              model[_model] = e.target.value
            }
          }
        }
      )
    }

    // Radio
    function RadioRender (_item, _model) {
      return h('RadioGroup',
        {
          props: {
            'v-model': `${formName}.${_model}`
          },
          on: {
            'on-change': (e) => {
              model[_model] = e === '是' ? 1 : 0
            }
          }
        },
        _item.options ? optionsRender(_item.options, 'Radio') : []
      )
    }

    // Select
    function SelectRender (_item, _model) {
      return h('Select',
        {
          props: {
            'v-model': `${formName}.${_model}`
          },
          on: {
            'on-change': (e) => {
              model[_model] = e
            }
          }
        },
        _item.options ? optionsRender(_item.options, 'Option') : []
      )
    }

    // 有多选 options 配置 optionsRender
    // Radio
    // Select
    function optionsRender (_options, _tag) {
      let itemRes = []
      _options.forEach((_option, i) => {
        if (_tag === 'Radio') {
          itemRes.push(
            h(_tag,
              {
                props: {
                  'label': _option.label
                }
              }
            )
          )
        } else if (_tag === 'Option') {
          itemRes.push(
            h(_tag,
              {
                props: {
                  'key': _option.key,
                  'value': _option.value
                }
              }
            )
          )
        }
      })

      return itemRes
    }

    let items = itemsRender(h)
    return h(
      'Form',
      {
        class: context.data.staticStyle,
        style: context.data.staticStyle,
        props: context.props
      },
      items
    )
  }
}
</script>

复制代码

好了,有了上面的铺路,你就可以在项目的任何页面使用配置表单了,这样你就不用重复去 copy 结构代码了,使得页面中的代码看着清爽;更重要的是分文件管理的方式,有利于维护。其实分页列表也可以参考这样的方式。

一个含分页列表和基础表单的文件可以是这样的:

<template>
  <div class="hc-project-management">
    <CommonList
      :addSearch="addSearch"
      :columns="columns"
      :data="projectList"
      :pageBean="pageBean"
      :statePath="statePath"
    />
    <MyModal
      :isShow="modal.isShow"
      :title="modal.title"
    >
      <template slot="modalContent">
        <AutoForm
          :fileds="projectFields"
          :model="projectFormData"
          :formName="projectFormData"
          class="my-form"/>
      </template>
    </MyModal>
  </div>
</template>
复制代码
如何根据 Select 框的选项动态新增表单项

有时候我们会有像标题描述的需求,当一个下拉菜单选中后,自动的添加或者改变表单项。

实现: 我这边会在 watch 中监听 state 中数据变化来添加配置项

  watch: {
    // 通过这种语法来watch
    'projectFormData': {
      handler: function (val, oldVal) { // 不能使用箭头函数 this 指向会出问题
        if (val.projectSelect) {
          this.projectFields = Object.assign({}, this.projectFields, { projectTextarea: {
            label: '项目textarea',
            tag: 'Input',
            type: 'textarea',
            placeholder: '请输入textarea'
          }})
        }
        console.log(val)
      },
      // 深度观察
      deep: true
    }
  },
复制代码
说两句

  其实配置化还是常规写法,都是需要根据自身业务和开发成员等综合考虑的,比如在配置化时,那么就需要和组员约定好一个添加表单的流程和写法,这个是相对固定的,不像常规的那么自由;又比如,本身我们这个项目表单的数量只有2、3个,那是否有配置化的必要;再比如,成员间是否认可这样的写法,也是需要商量的。但是一旦形成文档规范,那么回头来看,配置化带来的可维护性、易错误定位等好处,就显得不用付出那么多成本。

转载于:https://juejin.im/post/5b710c036fb9a0099a091939

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值