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

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

主要內(nèi)容

  • 需求分析
  • 子控件的分類
  • 子控件屬性的分類
  • 定義 interface。
  • 定義子控件的的 props。
  • 定義 json 文件。
  • 基于 UI庫 進(jìn)行二次封裝,實現(xiàn)依賴 json 渲染。
  • 通過 slot 、 “字典”,實現(xiàn)自定義子控件。
  • 做個工具維護(hù) json 文件。(下篇介紹)

需求分析

表單里面需要各種各樣的子控件,像文本、數(shù)字、選擇、日期等常見的需求,可以由內(nèi)部提供組件解決,但是其他各種“奇奇怪怪”的需求怎么辦呢?

如果還是由“內(nèi)部”提供組件的話,那肯定是行不通的,因為以往的經(jīng)驗教訓(xùn)告訴我們,內(nèi)部不斷擴(kuò)充子控件的結(jié)果,必然會導(dǎo)致內(nèi)部代碼越來越臃腫,以至后期無法維護(hù),最終崩盤!

所以必須支持自定義擴(kuò)展!感謝 Vue 和 UI庫,提供基礎(chǔ)的技術(shù)支持,讓擴(kuò)展變得非常容易。

我們先對表單子控件進(jìn)行一下分類,然后為其設(shè)計一套接口,即定義一套規(guī)則,這樣才好方便做長期維護(hù)。

子控件的分類

我們對常見的組件進(jìn)行分析,得到了下面的分類:

表單子控件的分類

上圖涵蓋了一些常用控件,但是很顯然并不全面,比如沒有金額類的控件,輸入金額也是需要一些輔助的,比如金額的大小寫的切換等,不過這些應(yīng)該用擴(kuò)展的方式實現(xiàn)。

屬性的分類

組件的分類可以做的“規(guī)整”一些,但是組件的屬性的分類,就比較有難度了,我們可以把組件需要的屬性分為三個主要部分:代碼里需要的、共用的、擴(kuò)展的。

  • 低代碼需要的屬性
    需要在代碼里面使用的屬性,比如字段名稱、控件類型、默認(rèn)值、防抖延遲等,集中在一起,通過 props 的方式傳遞。

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

  • 擴(kuò)展屬性
    某個組件需要的屬性,比如數(shù)字組件需要 max、min、step等。通過 $attrs 的方式傳遞。

100表單子控件的屬性.png

其中擴(kuò)展屬性最為復(fù)雜,如果按照面向?qū)ο蟮姆绞絹碓O(shè)計的話,結(jié)構(gòu)就會非常復(fù)雜,會復(fù)雜到什么程度呢?可以參考當(dāng)初 asp.net 里面 webform 的繼承結(jié)構(gòu):

十三年前做的一張圖

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

定義接口

現(xiàn)在是 JS 環(huán)境,我們沒有必要生搬硬套,而是可以利用JS的靈活性來做簡潔設(shè)計:

表單子控件的接口

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

  • IFormItemProps
/**
 * 表單控件的子控件的 props。
 */
export interface IFormItemProps {
  /**
   * 低代碼需要的數(shù)據(jù)
   */
  formItemMeta: IFormItemMeta,
  /**
   * 子控件備選項,一級或者多級
   */
  optionList: Array<IOptionList | IOptionTree>,
  /**
   * 表單的 model,含義多個屬性
   */
  model: any,
  /**
   * 是否顯示清空的按鈕
   */
  clearable: boolean,
  /**
   * 浮動提示信息
   */
  title: string,
  /**
   * 子控件的擴(kuò)展屬性
   */
  [key: string]: any
}
  • IFormItemMeta 的定義:
/**
 * 子控件的低代碼需要的數(shù)據(jù)
 */
export interface IFormItemMeta {
  /**
   * -- 字段ID、控件ID
   */
  columnId?: number | string,
  /**
   * -- 字段名稱
   */
  colName: string,
  /**
   * -- 字段的中文名稱,標(biāo)簽
   */
  label?: string,
  /**
   * -- 子控件類型,number,EControlType
   */
  controlType: EControlType | number,
  /**
   * 子控件的默認(rèn)值
   */
  defValue: any,
  /**
   * -- 一個控件占據(jù)的空間份數(shù)。
   */
  colCount?: number,
  /**
   * 訪問后端API的配置信息,有備選項的控件需要
   */
  webapi?: IWebAPI,
  /**
   * -- 防抖延遲時間,0:不延遲
   */
  delay: number,
  /**
   * 防抖相關(guān)的事件
   */
  events?: IEventDebounce,
}

規(guī)則定義之后呢,總會發(fā)現(xiàn)有特例的屬性,比如 select 的 option。代碼里面需要使用 option 去綁定組件,應(yīng)該放在“低代碼需要的屬性”里面。

但是實際使用的時候發(fā)現(xiàn),放在“共用屬性”里面會更方便。

然后在做“維護(hù)JSON的小工具”的時候,發(fā)現(xiàn)需要放在“擴(kuò)展屬性”里面維護(hù),這樣維護(hù)代碼更容易實現(xiàn)。

綜合考慮之后,就出現(xiàn)了一個不符合規(guī)則的屬性 —— optionList。

定義組件的 props。

按照接口實現(xiàn)一下 props 的定義。

import type { PropType } from 'vue'

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

/**
 * 基礎(chǔ)控件的共用屬性,即表單子控件的基礎(chǔ)屬性
 */
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,整體傳入,便于子控件維護(hù)字段值。
   */
  model: {
    type: Object
  },
  /**
   * 是否顯示可清空的按鈕,默認(rèn)顯示
   */
  clearable: {
    type: Boolean,
    default: true
  },
  /**
   * 浮動的提示信息,部分控件支持
   */
  title: {
    type: String,
    default: ''
  }
}

其他屬性以及擴(kuò)展屬性,可以通過 $attrs 傳遞和綁定,這樣可以方便各種擴(kuò)展。

定義 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": "數(shù)字類"},
      {"value": 3, "label": "日期類"},
      {"value": 4, "label": "時間類"},
      {"value": 5, "label": "選擇類"},
      {"value": 6, "label": "下拉類"}
    ]
}

基于 UI庫 封裝,實現(xiàn)依賴 json 渲染。

首先要感謝強(qiáng)大的UI庫,實現(xiàn)了大部分的功能,我們只需要再稍微封裝一下即可,只有少數(shù)幾個組件需要我們補(bǔ)充點代碼。

文本類

  • 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" 綁定擴(kuò)展屬性

  • 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 // 基礎(chǔ)屬性
    },
    emits: ['update:modelValue'],
    setup (props, context) {
      const {
        value,
        run,
        clear
      } = itemController(props, context.emit)

      return {
        value,
        run,
        clear
      }
    }
  })

使用 ...itemProps 定義屬性。

是不是很簡單。

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

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

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

首先為了統(tǒng)一風(fēng)格,不管是簡單的,還是復(fù)雜的,都按照統(tǒng)一方式封裝一下,這樣便于維護(hù)和擴(kuò)展。

日期類

  • 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, // 基礎(chǔ)屬性
      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)

      // 根據(jù)類型判斷是否為數(shù)組,判斷是否 使用范圍。
      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 // 控件值
      }
    }
  })

可以增設(shè)屬性,然后根據(jù)需求設(shè)置默認(rèn)值,這樣方便統(tǒng)一風(fēng)格。

選擇類

  • 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, // 基礎(chǔ)屬性
      '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 的遍歷,實現(xiàn)了選項的渲染。

其他組件也是一樣的方式進(jìn)行封裝,就不一一介紹了。

封裝 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:實現(xiàn)多列
  • transition:組件聯(lián)動的時候的動畫效果
  • component:動態(tài)加載子控件
  • formItemKey 子控件的字典,key-value形式,key就是控件編號,value是組件。這樣就可以根據(jù)控件的編號加載對應(yīng)的子控件了。

使用 slot 和 字典 實現(xiàn)擴(kuò)展自定義子控件。

這里要感謝強(qiáng)大的 vue3,提供了插槽這種很靈活的擴(kuò)展方式。以及組件的形成管理代碼。

說到擴(kuò)展,想必大家想到的是插槽,我們也支持使用插槽的擴(kuò)展方式,不過我覺得,既然定義了接口,那么不用的話,是不是有點浪費。

我們可以定義組件實現(xiàn)接口,然后并入字典(formItemKey),這樣表單控件就可以從字典里面加載我們自己定義的組件了,更便于管理和擴(kuò)展。

源碼和演示

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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容