《前端那些事》從0到1開發動態表單

樹醬但願將前端的樂趣帶給你們 本文已收錄 github.com/littleTreem… 喜歡就star✨html

前沿:中後臺應用中表單需求頗多,左手一個表單,右手又是一個表單,無窮無盡,若是用模版一個個來寫,不單寫起來費時費力,並且看起來也是天花亂墜,因而這個時候你會去設想,那有沒有什麼方式能夠去替換瑣碎的手寫表單模版的方式呢?讓表單是「配出來」的,而不是擼出來的,讓你輕鬆解決 form 表單,也再也不爲表單而煩惱。答案就是:動態表單前端

1.傳統表單模版

一個表單須要什麼?無疑是包含了form數據的收集、驗證及提交等等功能,讓咱們看看下面這個基於iview組件庫的form表單vue

這個簡單的表單,若是咱們用手寫模版的方式擼出來,模版部分就是以下所示👇git

數據初始化定義和驗證提交邏輯以下 github

以上就完成一個具有數據收集、驗證、提交、重製的表單,可是相對應問題也來了,這裏用模板並非最好的選擇,代碼過於冗長,也存在重複代碼,若是個人項目中十幾個表單甚至更多,我豈不是都要去寫怎麼多代碼去維護這類表單,會不會顯得太冗餘,接下來進入咱們今天的主角:動態表單,讓咱們看看怎麼讓他「動」💃起來算法

2 動態表單

2.1 我所指望的表單

我指望的表單是能夠配出來的,經過JSON來動態渲染生成相應的表單,表單中涉及的組件(好比Input、Select)能夠經過獲取JSON的配置所需的去渲染,上一小節提到的模版渲染顯然就不適用此次場景了,雖然vue官方推薦在絕大多數狀況下使用模板來建立你的temlate,可是一些場景仍是須要用到渲染函數render 官方文檔點我👈json

2.2 關於渲染函數

咱們先看看這個例子,Vue.js 的 mount 函數,將h()生成的VNode節點函數,渲染成真實 DOM 節點,並掛載到根節點上前端工程化

這個h()函數本質上是createElement 函數,這個函數的做用就是生成一個 VNode節點(虛擬節點),它不是一個實際的 DOM 元素。叫createNodeDescription(建立節點描述),咱們是經過它所包含的信息會來告訴 Vue 頁面上須要渲染什麼樣的節點,再經過diff算法能夠追蹤dom的變化api

拓展:你可能會好奇爲啥是叫h()函數,而不是createElement()的簡稱c()數組

h出自hyperscript首字母,最原始的定義是「Create HyperText with JavaScript」,而HyperText則是出自咱們熟悉的則HTML 是 hyper-text markup language 的縮寫(超文本標記語言),因此能夠理解爲Hyperscript是指生成HTML的 script 腳本

createElment函數接受三個參數,分別是:

  • 參數一:標籤名、組件的選項對象、函數等等(必選);
  • 參數二:設置這個對象的樣式、屬性、傳的組件的參數、綁定事件等(可選);
  • 參數三:該節點下的其餘節點,即子級虛擬節點,能夠是字符串形式或數組形式,也須要使用createElement構建。

下面用一個簡單例子說明渲染函數的使用👇

上面例子的模版渲染和渲染函數渲染的結果是同樣的,固然模板本質上也是經過 Compile 編譯 獲得 渲染函數render(),因此說其實渲染函數更高效,更快,減小了編譯的時間,關於編譯能夠看這篇vue 編譯過程,由templete編譯成render函數

渲染函數render與模板template的區別

  • render(高)的性能要比tempate(低)要高。
  • template簡單,能夠直觀看出內容想要表述的含義,可是不夠靈活而;render渲染函數則是經過createElement的方式建立VNode,適合開發複發性強的組件。

扯完渲染函數,接下來介紹下動態表單的思路

3 動態表單的實現

這裏使用的是iview組件庫的基礎上實現的動態表單,建立的組件都是基於iview來實現的,下面是具體的流程圖

3.1配置表單配置內容

我用第一節的例子來配置一個JSON格式的表單配置(由於配置文件過長,改用文字)

const formOption = {
  ref: 'formValidate',
  style: { //表單樣式,非必須
    width: '300px',
    margin: 'auto',
  },  
  className: 'form',
  formProps: { //非必須
    'label-width': 80,
  },
  formData: {//所要監聽的表單字段數據,必須
     name: '',
     city: '',
     sex: 'male',
  },
  formItem: [ //iview form表單的每一個formItem,必須
    {
      type: 'input',
      label: '名稱', //對應formItem的label
      key: 'name', //key對應formData中的字段
      props: {
        placeholder: '請輸入名稱',
      },
      rules: {  //表單檢測規則,非必須
        required: true,
        message: '請填寫名稱',
        trigger: 'blur',
      },
    },
    {
      type: 'select',
      label: '城市', //對應formItem的label
      key: 'city', //key對應formData中的字段
      props: {
        placeholder: '請輸入名稱',
      },
     children: [{ label: 'xml', value: '1' },
        { label: 'json', value: '2' },
        { label: 'hl7', value: '3' }
      ],
      rules: {  //表單檢測規則,非必須
        required: true,
        message: '請選擇城市',
        trigger: 'blur',
      },
    }, 
    {
      type: 'radioGroup',
      key: 'type',
      label: 'sex',
      children: [
        {
          text: 'female',
          label: 'female',
        },
        {
          text: 'male',
          label: 'male',
        },
      ],
      events: {
        'on-change': (vm, value) => {
          vm.$emit('on-change', value);
        },
      },
    }
  ],
  events: events('formValidate'),//表單按鈕組                                       
}
複製代碼

還有相應的事件按鈕統一在events中處理(可複用)

3.2 render函數渲染組件

第一節例子涉及到表單組件分別是Input、Select、radioGroup、formItem。分別是定義它們的render函數

  • 暴露不一樣組件渲染的api

  • Input組件渲染函數

集合iview組件庫Input的API,包括props屬性、events事件、slot插槽、methods方法等來定義渲染函數,具體實現以下圖所示

function generateInputComponent(h, formData = {}, obj, vm) {
    const key = obj.key? obj.key : ''
    let children = []

    if (obj.children) { //input有子集,走這裏
        children = obj.children.map(item => {
            let component
            if (item.type == 'span') {  //複合型輸入框狀況 
                component = h('span', {
                    slot: item.slot
                }, [item.text])         
            } else {
                let func = componentObj[item.type]
                component = func? func.call(vm, h, formData, item, vm) : null
            }
            return component
        })
    }

    return h('Input', {
        props: {
            value: key? formData[key] : '',
            ...obj.props
        },
        style: obj.style,
        on: {
            ...translateEvents(obj.events, vm), //時間綁定
            input(val) {
                if (key) {
                    formData[key] = val
                }
            }
        },
        slot: obj.slot                                                                 
    }, children)
}
 //事件bind
function translateEvents(events = {}, vm, formData = {}) {
    const result = {}
    for (let event in events) {
        result[event] = events[event].bind(vm, vm, formData);
    }

    return result 
}                  
複製代碼
  • Select 組件渲染函數
function generateSelectComponent(h, formData = {}, obj, vm) {
    const key = obj.key? obj.key : ''

    let components = []

    if (obj.children) {
        components = obj.children.map(item => {
            if (item.type == 'optionGroup') {
                return h('OptionGroup', {
                    props: item.props? item.props : item
                }, item.children.map(child => {
                    return h('Option', {
                        props: child.props? child.props : child
                    })
                }))
            } else {
                return h('Option', {
                    props: item.props? item.props : item
                })
            }
        })
    }

    return h('Select', {
        props: {
            value: formData[key],
            ...obj.props
        },
        style: obj.style,
        on: {
            ...translateEvents(obj.events, vm),
            input(val) {
                if (key) {
                    formData[key] = val
                }
            }
        },
        slot: obj.slot
    }, components)
}
複製代碼

這裏只是展現部分組件的實現方式,主要目的是梳理開發及應用的流程思路

  • events 按鈕生成
function generateEventsComponent(h, formData = {}, obj, vm) {
    const components = [];
    if(obj.submit) {
        const submit = h('Button', {
            props: obj.submit.props,
            style: obj.submit.style,
            class: obj.submit.className,
            on: {
                click() {
                   //提交前校驗
                    vm.$refs[obj.ref].validate((valid) => {
                        if (valid) {
                            obj.submit.success.call(vm, formData, vm)
                        } else {
                            obj.submit.fail.call(vm, formData, vm)
                        }
                    })
                }
            }
        }, [obj.submit.text])

        components.push(submit)
    }
    if (obj.reset) {
        const reset = h('Button', {
            props: obj.reset.props,
            style: {
                ...obj.reset.style,
            },
            class: obj.reset.className,
            on: {
                click() {
                    vm.$refs[obj.ref].resetFields() //重置表單
                    obj.reset.success.call(vm, formData, vm); 
                }
            }
        }, [obj.reset.text])

        components.push(reset)
    }

    return h('div',{
        class: 'vue-events',
        style: {
            ...obj.style
        }
    }, components)
}
複製代碼
  • formBuild動態表單組件的定義

實現好組件的動態生成邏輯,這個時候須要一個入口(formBuild.js),就是根據配置去映射相應的組件並生成合並,組合成爲最終要的表單

// form-build.js
import componentObj from './utils'

export default {
    props: {
        options: {
            type: Object,
            required: true
        },
    },
    render(h) {
        const options = this.options
        const formData = options.formData
        
        if (!options.formItem) {
            return h('div')
        }

        const components = options.formItem.map(item => {
            let func = componentObj[item.type]
            let subComponent = func? func.call(this, h, formData, item, this) : null
            let component = componentObj.formItem(h, item, subComponent, formData)
            return componentObj.col(h, item, component)
        })

        const childComp = [];

        const fromComp = h('Form', {
                ref: options.ref,
                style: options.style ? options.style : '',
                props: {
                    model: formData,
                    ...options.formProps
                },
                class: 'vue-generate-form'
            }, [
                h('Row', {
                    props: options.rowProps
                }, components)
           ]);
        
        childComp.push(fromComp);

        if (options.events) {
            const eventComo = componentObj.events(h, formData, obj.events , vm)                         
          childComp.push(eventComp)
        }
        return h('div', [childComp]);
    }
}
複製代碼

還須要定義vue的插件安裝

2.3 如何使用

  • 注意事項
  1. 某些組件(例如 button) iview 並無提供相似於 on-click 這樣的事件。可使用 DOM 元素原生事件代替,例如 click
  2. 全部表單數據都要在formData裏定義

4.總結

以上就能夠經過render渲染函數來完成動態表單工具的實現,本文主要是經過一種思路去介紹整個開發,動態表單有多種實現方式,固然你可能也有疑惑

  • 如何支持多種UI組件庫的動態表單配置?

你能夠參考下開源的form-create(支持3種 UI 框架:Iview、ElementUI、Ant-design-vue)是如何實現的 form-create工具庫

  • 如何開發在線編輯配置的動態表單工具?

可視化表單設計工具也很香,有興趣的童鞋能夠了解 vue-ele-form-generator

文章思路來源:vue-form-make

往期文章

相關文章
相關標籤/搜索