【摸魚神器】UI庫秒變低程式碼工具——表單篇(二)子控制元件

金色海洋(jyk) 發表於 2022-07-17

上一篇介紹了表單控制元件,這一篇介紹一下表單裡面的各種子控制元件的封裝方式。

主要內容

  • 需求分析
  • 子控制元件的分類
  • 子控制元件屬性的分類
  • 定義 interface。
  • 定義子控制元件的的 props。
  • 定義 json 檔案。
  • 基於 UI庫 進行二次封裝,實現依賴 json 渲染。
  • 通過 slot 、 “字典”,實現自定義子控制元件。
  • 做個工具維護 json 檔案。(下篇介紹)

需求分析

表單裡面需要各種各樣的子控制元件,像文字、數字、選擇、日期等常見的需求,可以由內部提供元件解決,但是其他各種“奇奇怪怪”的需求怎麼辦呢?

如果還是由“內部”提供元件的話,那肯定是行不通的,因為以往的經驗教訓告訴我們,內部不斷擴充子控制元件的結果,必然會導致內部程式碼越來越臃腫,以至後期無法維護,最終崩盤!

所以必須支援自定義擴充套件!感謝 Vue 和 UI庫,提供基礎的技術支援,讓擴充套件變得非常容易。

我們先對錶單子控制元件進行一下分類,然後為其設計一套介面,即定義一套規則,這樣才好方便做長期維護。

子控制元件的分類

我們對常見的元件進行分析,得到了下面的分類:

表單子控制元件的分類

上圖涵蓋了一些常用控制元件,但是很顯然並不全面,比如沒有金額類的控制元件,輸入金額也是需要一些輔助的,比如金額的大小寫的切換等,不過這些應該用擴充套件的方式實現。

屬性的分類

元件的分類可以做的“規整”一些,但是元件的屬性的分類,就比較有難度了,我們可以把元件需要的屬性分為三個主要部分:程式碼裡需要的、共用的、擴充套件的。

  • 低程式碼需要的屬性
    需要在程式碼裡面使用的屬性,比如欄位名稱、控制元件型別、預設值、防抖延遲等,集中在一起,通過 props 的方式傳遞。

  • 共用屬性
    各個元件(或者大部分元件)都需要的屬性,比如浮動提示、size、是否顯示清空按鈕等,作為一級屬性,通過 props 的方式傳遞。

  • 擴充套件屬性
    某個元件需要的屬性,比如數字元件需要 max、min、step等。通過 $attrs 的方式傳遞。

100表單子控制元件的屬性.png

其中擴充套件屬性最為複雜,如果按照物件導向的方式來設計的話,結構就會非常複雜,會複雜到什麼程度呢?可以參考當初 asp.net 裡面 webform 的繼承結構:

十三年前做的一張圖

(controll是控制元件(元件)的意思,下面分出來WebControll和 repeater 兩個子類,然後又,,,算了不說了,是不是看著就很累的樣子?)

定義介面

現在是 JS 環境,我們沒有必要生搬硬套,而是可以利用JS的靈活性來做簡潔設計:

表單子控制元件的介面

我們給表單子控制元件的 props 定義一個interface:(雖然暫時用不上)

  • IFormItemProps
/**
 * 表單控制元件的子控制元件的 props。
 */
export interface IFormItemProps {
  /**
   * 低程式碼需要的資料
   */
  formItemMeta: IFormItemMeta,
  /**
   * 子控制元件備選項,一級或者多級
   */
  optionList: Array<IOptionList | IOptionTree>,
  /**
   * 表單的 model,含義多個屬性
   */
  model: any,
  /**
   * 是否顯示清空的按鈕
   */
  clearable: boolean,
  /**
   * 浮動提示資訊
   */
  title: string,
  /**
   * 子控制元件的擴充套件屬性
   */
  [key: string]: any
}
  • IFormItemMeta 的定義:
/**
 * 子控制元件的低程式碼需要的資料
 */
export interface IFormItemMeta {
  /**
   * -- 欄位ID、控制元件ID
   */
  columnId?: number | string,
  /**
   * -- 欄位名稱
   */
  colName: string,
  /**
   * -- 欄位的中文名稱,標籤
   */
  label?: string,
  /**
   * -- 子控制元件型別,number,EControlType
   */
  controlType: EControlType | number,
  /**
   * 子控制元件的預設值
   */
  defValue: any,
  /**
   * -- 一個控制元件佔據的空間份數。
   */
  colCount?: number,
  /**
   * 訪問後端API的配置資訊,有備選項的控制元件需要
   */
  webapi?: IWebAPI,
  /**
   * -- 防抖延遲時間,0:不延遲
   */
  delay: number,
  /**
   * 防抖相關的事件
   */
  events?: IEventDebounce,
}

規則定義之後呢,總會發現有特例的屬性,比如 select 的 option。程式碼裡面需要使用 option 去繫結元件,應該放在“低程式碼需要的屬性”裡面。

但是實際使用的時候發現,放在“共用屬性”裡面會更方便。

然後在做“維護JSON的小工具”的時候,發現需要放在“擴充套件屬性”裡面維護,這樣維護程式碼更容易實現。

綜合考慮之後,就出現了一個不符合規則的屬性 —— optionList。

定義元件的 props。

按照介面實現一下 props 的定義。

import type { PropType } from 'vue'

import type {
  IOptionList,
  IOptionTree,
  IFormItemProps
} from '../types/20-form-item'

/**
 * 基礎控制元件的共用屬性,即表單子控制元件的基礎屬性
 */
export const itemProps = {
  formItemMeta: {
    type: Object as PropType<IFormItemProps>,
    default: () =>  {return {}}
  },
  /**
   * optionList:IOptionList | IOptionTree,控制元件的備選項,單選、多選、等控制元件需要
   */
  optionList: {
    type: Object as PropType<Array<IOptionList | IOptionTree>>,
    default: () =>  {return []}
  },
  /**
   * 表單的 model,整體傳入,便於子控制元件維護欄位值。
   */
  model: {
    type: Object
  },
  /**
   * 是否顯示可清空的按鈕,預設顯示
   */
  clearable: {
    type: Boolean,
    default: true
  },
  /**
   * 浮動的提示資訊,部分控制元件支援
   */
  title: {
    type: String,
    default: ''
  }
}

其他屬性以及擴充套件屬性,可以通過 $attrs 傳遞和繫結,這樣可以方便各種擴充套件。

定義 json 檔案。

我們來定義一個示例用的 json檔案。

{
    "formItemMeta": {
      "columnId": 90,
      "colName": "kind",
      "label": "分類",
      "controlType": 107,
      "isClear": false,
      "defValue": 0,
      "colCount": 7
    },
    "placeholder": "分類",
    "title": "編號",
    "optionList": [
      {"value": 1, "label": "文字類"},
      {"value": 2, "label": "數字類"},
      {"value": 3, "label": "日期類"},
      {"value": 4, "label": "時間類"},
      {"value": 5, "label": "選擇類"},
      {"value": 6, "label": "下拉類"}
    ]
}

基於 UI庫 封裝,實現依賴 json 渲染。

首先要感謝強大的UI庫,實現了大部分的功能,我們只需要再稍微封裝一下即可,只有少數幾個元件需要我們補充點程式碼。

文字類

  • template
  <el-input
    v-model="value"
    v-bind="$attrs"
    :id="'c' + formItemMeta.columnId"
    :name="'c' + formItemMeta.columnId"
    :title="title"
    :clearable="clearable"
    @blur="run"
    @change="run"
    @clear="run"
    @keydown="clear"
  >
  </el-input>

使用 v-bind="$attrs" 繫結擴充套件屬性

  • ts
  import { defineComponent } from 'vue'
  import { ElInput } from 'element-plus'
  // 引入元件需要的屬性、控制類
  import { itemProps, itemController } from '@naturefw/ui-elp'

  export default defineComponent({
    name: 'nf-el-form-item-text',
    inheritAttrs: false,
    components: {
      ElInput
    },
    props: {
      modelValue: [String, Number],
      ...itemProps // 基礎屬性
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const {
        value,
        run,
        clear
      } = itemController(props, context.emit)

      return {
        value,
        run,
        clear
      }
    }
  })

使用 ...itemProps 定義屬性。

是不是很簡單。

可能你會問了,這不是封裝了個寂寞嗎,你看看裡面空蕩蕩的,完全沒有封裝的必要嘛。

確實,對於文字這類簡單的元件,確實沒有封裝的必要,直接使用UI庫提供的元件即可。

那麼為啥好要封裝一下呢?

首先為了統一風格,不管是簡單的,還是複雜的,都按照統一方式封裝一下,這樣便於維護和擴充套件。

日期類

  • template
  <el-date-picker
    ref="domDate"
    v-model="value"
    v-bind="$attrs"
    :type="dateType"
    :name="'c' + formItemMeta.columnId"
    :format="format"
    :value-format="valueFormat"
    :title="title"
    :clearable="clearable"
  >
  </el-date-picker>
  • ts
  import { defineComponent } from 'vue'
  // 引入元件需要的屬性 引入表單子控制元件的管理類
  import { itemProps, itemController } from '@naturefw/ui-elp'
  
  /**
   * 日期
   */
  export default defineComponent({
    name: 'nf-el-from-item-date',
    inheritAttrs: false,
    props: {
      ...itemProps, // 基礎屬性
      format: {
        type: String,
        default: 'YYYY-MM-DD'
      },
      'value-format': {
        type: String,
        default: 'YYYY-MM-DD'
      },
      modelValue: [String, Date, Number, Array]
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const { value } = itemController(props, context.emit)

      // 根據型別判斷是否為陣列,判斷是否 使用範圍。
      let dateType = 'date'
      if (props.formItemMeta.controlType == '125' ) {
        dateType = 'daterange'
        if (!Array.isArray(value.value)) {
          value.value = []
        }
      } else {
        if (Array.isArray(value.value)) {
          value.value = ''
        }
      }

      return {
        dateType, // 控制元件型別
        value // 控制元件值
      }
    }
  })

可以增設屬性,然後根據需求設定預設值,這樣方便統一風格。

選擇類

  • template
  <el-select
    v-model="value"
    v-bind="$attrs"
    :id="'c' + formItemMeta.columnId"
    :name="'c' + formItemMeta.columnId"
    :clearable="clearable"
    :multiple="multiple"
    :collapse-tags="collapseTags"
    :collapse-tags-tooltip="collapseTagsTooltip"
  >
    <el-option
      v-for="item in optionList"
      :key="'select' + item.value"
      :label="item.label"
      :value="item.value"
      :disabled="item.disabled"
    >
    </el-option>
  </el-select>
  • ts
  import { defineComponent, computed } from 'vue'
  // 引入元件需要的屬性 引入表單子控制元件的管理類
  import { itemProps, itemController } from '@naturefw/ui-elp'

  export default defineComponent({
    name: 'nf-el-from-select',
    inheritAttrs: false,
    props: {
      ...itemProps, // 基礎屬性
      'collapse-tags': {
        type: Boolean,
        default: true
      },
      'collapse-tags-tooltip': {
        type: Boolean,
        default: true
      },
      modelValue: [String, Number, Array]
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const multiple = computed (() => props.formItemMeta.controlType === 161)
  
      return {
        ...itemController(props, context.emit)
      }
    }
  })

template 裡面增加了 el-option 部分,通過對 optionList 的遍歷,實現了選項的渲染。

其他元件也是一樣的方式進行封裝,就不一一介紹了。

封裝 el-form-item

el-table 通過 el-form-item 來載入子元件,所以我們也可以封裝一下:

  <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].formItemMeta.label"
          :prop="itemMeta[ctrId].formItemMeta.colName"
          :rules="ruleMeta[ctrId] ?? []"
          :label-width="itemMeta[ctrId].formItemMeta.labelWidth??''"
          :size="size"
          v-show="showCol[ctrId]"
        >
          <component
            :is="formItemKey[itemMeta[ctrId].formItemMeta.controlType]"
            :model="model"
            v-bind="itemMeta[ctrId]"
          >
          </component>
        </el-form-item>
      </transition>
    </el-col>
  </el-row>
  • el-row、el-col:實現多列
  • transition:元件聯動的時候的動畫效果
  • component:動態載入子控制元件
  • formItemKey 子控制元件的字典,key-value形式,key就是控制元件編號,value是元件。這樣就可以根據控制元件的編號載入對應的子控制元件了。

使用 slot 和 字典 實現擴充套件自定義子控制元件。

這裡要感謝強大的 vue3,提供了插槽這種很靈活的擴充套件方式。以及元件的形成管理程式碼。

說到擴充套件,想必大家想到的是插槽,我們也支援使用插槽的擴充套件方式,不過我覺得,既然定義了介面,那麼不用的話,是不是有點浪費。

我們可以定義元件實現介面,然後併入字典(formItemKey),這樣表單控制元件就可以從字典裡面載入我們自己定義的元件了,更便於管理和擴充套件。

原始碼和演示

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/