結合 UI 框架實現可配置 Vue 表單元件淺析

AllenChinese發表於2018-08-13

—— 封面攝於濟州島民宿

常規的表單

如果我們用 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

結合 UI 框架實現可配置 Vue 表單元件淺析

三個引數的簡單用法:

<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 框的選項動態新增表單項

有時候我們會有像標題描述的需求,當一個下拉選單選中後,自動的新增或者改變表單項。

結合 UI 框架實現可配置 Vue 表單元件淺析

實現: 我這邊會在 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個,那是否有配置化的必要;再比如,成員間是否認可這樣的寫法,也是需要商量的。但是一旦形成文件規範,那麼回頭來看,配置化帶來的可維護性、易錯誤定位等好處,就顯得不用付出那麼多成本。

相關文章