【摸魚神器】UI庫秒變低程式碼工具——表單篇(一)設計

金色海洋(jyk) 發表於 2022-06-29

前面說了列表的低程式碼化的方法,本篇介紹一下表單的低程式碼化。

內容摘要

  • 需求分析。
  • 定義 interface。
  • 定義表單控制元件的 props。
  • 定義 json 檔案。
  • 基於 el-form 封裝,實現依賴 json 渲染。
  • 實現多列、驗證、分欄等功能。
  • 使用 slot 實現自定義擴充套件。
  • 自定義子控制元件。(下篇介紹)
  • 表單子控制元件的設計與實現。(下篇介紹)
  • 做個工具維護 json 檔案。(下下篇介紹)

需求分析

表單是很常見的需求,各種網頁、平臺、後臺管理等,都需要表單,有簡單的、也有複雜的,但是目的一致:收集使用者的資料,然後提交給後端。

表單控制元件的基礎需求:

  • 可以依賴 JSON 渲染。
  • 依賴 JSON 建立 model。
  • 便於使用者輸入資料。
  • 驗證使用者輸入的資料。
  • 便於程式設計師實現功能。
  • 可以多列。
  • 可以分欄。
  • 可以自定義擴充套件。
  • 其他。

el-form 實現了資料驗證、自定義擴充套件等功能(還有漂亮的UI),我們可以直接拿過來封裝,然後再補充點程式碼,實現多列、分欄、依賴 JSON 渲染等功能。

設計 interface

首先把表單控制元件需要的屬性分為兩大類:el-form 的屬性、低程式碼需要的資料。

表單控制元件需要的屬性的分類

整理一下做個腦圖:

表單控制元件需要的屬性.png

表單控制元件的介面

我們轉換為介面的形式,再做個腦圖:

表單控制元件的介面.png

然後我們定義具體的 interface

IFromProps:表單控制元件的介面 (包含所有屬性,對應 json 檔案)

/**
 * 表單控制元件的屬性
 */
export interface IFromProps {
  /**
   * 表單的 model,物件,包含多個欄位。
   */
  model: any,
  /**
   * 根據選項過濾後的 model,any
   */
  partModel?: any,
  /**
   * 表單控制元件需要的 meta
   */
  formMeta: IFromMeta,
  /**
   * 表單子控制元件的屬性,IFormItem
   */
  itemMeta: IFormItemList,
  /**
   * 標籤的字尾,string
   */
  labelSuffix: string,
  /**
  * 標籤的寬度,string
  */
  labelWidth: string,
  /**
  * 控制元件的規格,ESize
  */
  size: ESize,
  /**
  * 其他擴充套件屬性
  */
  [propName: string]: any

}
  • model:表單資料,可以依據 JSON 建立。
  • partModel:元件聯動後,只保留可見元件對應的資料。
  • formMeta:低程式碼需要的屬性集合。
  • itemMeta:表單子控制元件需要的屬性集合。
  • 其他:el-table 元件需要的屬性,可以使用 $attrs 進行擴充套件。

本來想用這個介面約束元件的 props,但是有點小問題:

  • 如果用 Option API 的話,不支援這種形式的介面。
  • 如果使用 Composition API 的話,雖然支援,但是隻能在元件內部定義 interface,暫時不支援從外部檔案引入。

介面檔案應該可以在外部定義,然後引入元件。如果不能的話,那就尷尬了。

所以只好暫時放棄對元件的 props 進行整體約束。

IFromMeta:低程式碼需要的屬性介面

/**
 * 低程式碼的表單需要的 meta
 */
export interface IFromMeta {
  /**
   * 模組編號,綜合使用的時候需要
   */
  moduleId: number | string,
  /**
   * 表單編號,一個模組可以有多個表單
   */
  formId: number | string,
  /**
   * 表單欄位的排序、顯示依據,Array<number | string>,
   */
  colOrder: Array<number | string>,
  /**
   * 表單的列數,分為幾列 number,
   */
  columnsNumber: number
   /**
   * 分欄的設定,ISubMeta
   */
  subMeta: ISubMeta,
  /**
   * 驗證資訊,IRuleMeta
   */
  ruleMeta: IRuleMeta,
  /**
   * 子控制元件的聯動關係,ILinkageMeta
   */
  linkageMeta: ILinkageMeta
}
  • moduleId 模組編號,以後使用
  • formId 表單編號,一個模組可以有多個表單
  • colOrder 陣列形式,表單裡包含哪些欄位?欄位的先後順序如何確定?就用這個陣列。
  • columnsNumber 表單控制元件的列數,表單只能單列?太單調,支援多列才是王道。

ISubMeta:分欄的介面

/**
 * 分欄表單的設定
 */
export interface ISubMeta {
  type: ESubType, // 分欄型別:card、tab、step、"" (不分欄)
  cols: Array<{ // 欄目資訊
    title: string, // 欄目名稱
    colIds:  Array<number> // 欄目裡有哪些控制元件ID
  }>
}

UI庫提供了 el-card、el-tab、el-step等元件,我們可以使用這幾個元件來實現多種分欄的形式。

IRule、IRuleMeta、:資料驗證的介面

el-form 採用 async-validator 實現資料驗證,所以我們可以去官網(https://github.com/yiminghe/async-validator)看看可以有哪些屬性,針對這些屬性指定一個介面(IRule),然後定義一個【欄位編號-驗證陣列】的介面(IRuleMeta)


/**
 * 一條驗證規則,一個控制元件可以有多條驗證規則
 */
export interface IRule {
  /**
   * 驗證時機:blur、change、click、keyup
   */
  trigger?:  "blur" | "change" | "click" | "keyup",
  /**
   * 提示訊息
   */
  message?: string,
  /**
   * 必填
   */
  required?: boolean,
  /**
   * 資料型別:any、date、url等
   */
  type?: string,
  /**
   * 長度
   */
  len?: number, // 長度
  /**
   * 最大值
   */
  max?: number,
  /**
   * 最小值
   */
  min?: number,
  /**
   * 正則
   */
  pattern?: string
}

/**
 * 表單的驗證規則集合
 */
export interface IRuleMeta {
  /**
   * 控制元件的ID作為key, 一個控制元件,可以有多條驗證規則
   */
  [key: string | number]: Array<IRule>
}

ILinkageMeta:元件聯動的介面

有時候需要根據使用者的選擇顯示對應的一組元件,那麼如何實現呢?其實也比較簡單,還是做一個key-value ,欄位值作為key,需要顯示的欄位ID集合作為value。這樣就可以了。

/**
 * 顯示控制元件的聯動設定
 */
export interface ILinkageMeta {
  /**
   * 控制元件的ID作為key,每個控制元件值對應一個陣列,陣列裡面是需要顯示的控制元件ID。
   */
  [key: string | number]: {
    /**
     * 控制元件的值作為key,後面的陣列裡存放需要顯示的控制元件ID
     */
    [id: string | number]: Array<number>
  }
}
  • 根據選項,顯示對應的元件

聯動的表單.png

定義表單控制元件的 props。

interface 都定義好了,我們來定義元件的 props(實現介面)。

這裡採用 Option API 的方式,因為可以從外部檔案引入介面,也就是說,可以實現複用。

import type { PropType } from 'vue'

import type {
  IFromMeta // 表單控制元件需要的 meta
} from '../types/30-form'

import type { IFormItem, IFormItemList } from '../types/20-form-item'

import type { ESize } from '../types/enum'
import { ESize as size } from '../types/enum'
  
/**
 * 表單控制元件需要的屬性
 */
export const formProps = {
  /**
   * 表單的完整的 model
   */
  model: {
    type: Object as PropType<any>,
    required: true
  },
  /**
   * 根據選項過濾後的 model
   */
  partModel: {
    type: Object as PropType<any>,
    default: () => { return {}}
  },
  /**
   * 表單控制元件的 meta
   */
  formMeta: {
    type: Object as PropType<IFromMeta>,
    default: () => { return {}}
  },
  /**
   * 表單控制元件的子控制元件的 meta 集合
   */
  itemMeta: {
    type: Object as PropType<IFormItemList>,
    default: () => { return {}}
  },
  /**
   * 標籤的字尾
   */
  labelSuffix: {
    type: String,
    default: ':' 
  },
  /**
   * 標籤的寬度
   */
  labelWidth: {
    type: String,
    default: '130px'
  },
  /**
   * 控制元件的規格
   */
  size: {
    type: Object as PropType<ESize>,
    default: size.small
  }
}

在元件裡的使用方式

那麼如何使用呢?很簡單,用 import 匯入,然後解構即可。

  // 表單控制元件的屬性 
  import { formProps, formController } from '../map'

  export default defineComponent({
    name: 'nf-el-from-div',
    props: {
      ...formProps
      // 還可以設定其他屬性
    },
    setup (props, context) {
      略。。。
    }
})

這樣元件裡的程式碼看起來也會很簡潔。

定義 json 檔案

我們做一個簡單的 json 檔案:

{
  "formMeta": {
    "moduleId": 142,
    "formId": 14210,
    "columnsNumber": 2,
    "colOrder": [
      90,  101, 100,
      110, 111 
    ],
    "linkageMeta": {
      "90": {
        "1": [90, 101, 100],
        "2": [90, 110, 111] 
      }
    },
    "ruleMeta": {
      "101": [
        { "trigger": "blur", "message": "請輸入活動名稱", "required": true },
        { "trigger": "blur", "message": "長度在 3 到 5 個字元", "min": 3, "max": 5 }
      ]
    }
  },
  "itemMeta": {
    "90": {  
      "columnId": 90,
      "colName": "kind",
      "label": "分類",
      "controlType": 153,
      "isClear": false,
      "defValue": 0,
      "extend": {
        "placeholder": "分類",
        "title": "編號"
      },
      "optionList": [
        {"value": 1, "label": "文字類"},
        {"value": 2, "label": "數字類"}
      ],
      "colCount": 2
    },
    "100": {  
      "columnId": 100,
      "colName": "area",
      "label": "多行文字",
      "controlType": 100,
      "isClear": false,
      "defValue": 1000,
      "extend": {
        "placeholder": "多行文字",
        "title": "多行文字"
      },
      "colCount": 1
    },
    "101": {  
      "columnId": 101,
      "colName": "text",
      "label": "文字",
      "controlType": 101,
      "isClear": false,
      "defValue": "",
      "extend": {
        "placeholder": "文字",
        "title": "文字"
      },
      "colCount": 1
    },
    
    "110": {  
      "columnId": 110,
      "colName": "number1",
      "label": "數字",
      "controlType": 110,
      "isClear": false,
      "defValue": "",
      "extend": {
        "placeholder": "數字",
        "title": "數字"
      },
      "colCount": 1
    },
    "111": {  
      "columnId": 111,
      "colName": "number2",
      "label": "滑塊",
      "controlType": 111,
      "isClear": false,
      "defValue": "",
      "extend": {
        "placeholder": "滑塊",
        "title": "滑塊"
      },
      "colCount": 1
    } 
  }
}

溫馨提示:JSON 檔案不需要手擼哦。

基於 el-form 封裝,實現依賴 json 渲染。

準備工作完畢,我們來二次封裝 el-table 元件。

  <el-form
    :model="model"
    ref="formControl"
    :inline="false"
    class="demo-form-inline"
    :label-suffix="labelSuffix"
    :label-width="labelWidth"
    :size="size"
    v-bind="$attrs"
  >
    <el-row :gutter="15">
      <el-col
        v-for="(ctrId, index) in colOrder"
        :key="'form_' + ctrId + index"
        :span="formColSpan[ctrId]"
        v-show="showCol[ctrId]"
      ><!---->
        <transition name="el-zoom-in-top">
          <el-form-item
            :label="itemMeta[ctrId].label"
            :prop="itemMeta[ctrId].colName"
            :rules="ruleMeta[ctrId] ?? []"
            :label-width="itemMeta[ctrId].labelWidth??''"
            :size="size"
            v-show="showCol[ctrId]"
          >
            <component
              :is="formItemKey[itemMeta[ctrId].controlType]"
              :model="model"
              v-bind="itemMeta[ctrId]"
            >
            </component>
          </el-form-item>
        </transition>
      </el-col>
    </el-row>
  </el-form>
  • 通過 props 繫結 el-table 的屬性
    props 裡面定義的屬性,直接繫結即可,比如 :label-suffix="labelSuffix"

  • 通過 $attrs 繫結 el-table 的屬性
    props 裡面沒有定義的屬性,會儲存在 $attrs 裡面,可以通過 v-bind="$attrs"的方式繫結,既方便又支援擴充套件。

  • 使用動態元件(component)載入表單子元件。

  • 實現資料驗證,設定 rules 屬性即可,:rules="ruleMeta[ctrId] ?? []"

實現多列

使用 el-row、el-col 實現多列的效果。

el-col 分為了24個格子,通過一個欄位佔用多少個格子的方式實現多列,也就是說,最多支援 24列。當然肯定用不了這麼多。

所以,我們通過各種引數計算好 span 即可。篇幅有限,具體程式碼不介紹了,感興趣的話可以看原始碼。

  • 單列表單

單列的表單.png

  • 雙列表單

雙列的表單.png

  • 三列表單

三列的表單.png

  • 多列表單
    因為 el-col 的 span 最大是 24,所以最多支援24列。

  • 支援調整佈局
    三列表單裡面 URL元件就佔用了一整行,這類的調整都是很方便實現的。

分欄

這裡分為多個表單控制元件,以便於實現多種分欄方式,並不是在一個元件內部通過 v-if 來做各種判斷,這也是我需要把 interface 寫在單獨檔案裡的原因。

  <el-form
    :model="model"
    ref="formControl"
    :inline="false"
    class="demo-form-inline"
    :label-suffix="labelSuffix"
    :label-width="labelWidth"
    :size="size"
    v-bind="$attrs"
  >
    <el-tabs
      v-model="tabIndex"
      type="border-card"
    >
      <el-tab-pane
        v-for="(item, index) in cardOrder"
        :key="'tabs_' + index"
        :label="item.title"
        :name="item.title"
      >
        <el-row :gutter="15">
          <el-col
            v-for="(ctrId, index) in item.colIds"
            :key="'form_' + ctrId + index"
            :span="formColSpan[ctrId]"
            v-show="showCol[ctrId]"
          >
            <transition name="el-zoom-in-top">
              <el-form-item
                :label="itemMeta[ctrId].label"
                :prop="itemMeta[ctrId].colName"
                v-show="showCol[ctrId]"
              >
                <component
                  :is="formItemKey[itemMeta[ctrId].controlType]"
                  :model="model"
                  v-bind="itemMeta[ctrId]"
                >
                </component>
              </el-form-item>
            </transition>
          </el-col>
        </el-row>
      </el-tab-pane>
    </el-tabs>
  </el-form>
  • 分欄的表單(el-card)

card的表單.png

  • 分標籤的表單(el-tabs)

tab的表單.png

  • 分步驟的表單(el-steps)

step的表單.png

使用 slot 實現自定義擴充套件。

雖然表單控制元件可以預設一些表單子控制元件,比如文字、數字、日期、選擇等,但是客戶的需求是千變萬化的,固定的子控制元件肯定無法滿足客戶所有的需求,所以必須支援自定義擴充套件。

比較簡單的擴充套件就是使用 slot 插槽,el-table 裡面的 el-form-item 其實就是以 slot 的形式加入到 el-table 內部的。

所以我們也可以通過 slot 實現自定義的擴充套件:

     <nf-form
        v-form-drag="formMeta"
        :model="model"
        :partModel="model2"
        v-bind="formMeta"
      >
        <template v-slot:text>
          <h1>外部插槽 </h1>
          <input v-model="model.text"/>
        </template>
      </nf-form>

nf-form 就是封裝後的表單控制元件,設定屬性和 model 後就可使用了,很方便。
如果想擴充套件的話,可以使用 <template v-slot:text> 的方式,裡面的 【text】 是欄位名稱(model 的屬性)。

也就是說,我們是依據欄位名稱來區分 slot 的。

實現 interface 擴充套件子元件

雖然使用 slot 可以擴充套件子元件,但是對於子元件的結構複雜的情況,每次都使用 slot 的話,明顯不方便複用。

既然都定義 interface 了,那麼為何不實現介面製作元件,然後變成新的表單子元件呢?

當然可以了,具體方法下次再介紹。

關於 TypeScript

  • 為什麼要定義 interface ?
    定義 interface 可以比較清晰的表明結構和意圖,然後實現介面即可。避免過段時間自己都忘記含義。

  • JSON 檔案匯入後會自動解析為 js 的物件,那麼還用 interface 做什麼?
    這就比較尷尬了,也是我一直沒有采用 TS 的原因之一。
    TS只能在編寫程式碼、打包時做檢查,但是在執行時就幫不上忙了,所以對於低程式碼的幫助有限。

原始碼和演示

core:https://gitee.com/naturefw-code/nf-rollup-ui-controller

二次封裝: https://gitee.com/naturefw-code/nf-rollup-ui-element-plus

演示: https://naturefw-code.gitee.io/nf-rollup-ui-element-plus/

相關文章