27
目前在编写个人项目,有一个是管理平台,基本每个页面都有el-from,所以对el-form做了二次封装, 组件在个人开发使用不错,但不确定能满足各种业务需求,所以这里主要和大家分享一下设计思路。

设计组件

分析问题

el-form是element-ui库的表单组件,如果我们要将其进行二次封装,那么需要考虑几个问题:

  • 如何设计表单渲染字段
  • 不同类型的el-form-item怎么去渲染,比如input,select,或者自定义显示内容等
  • 表单联动怎么去处理
  • 事件绑定
  • 表单验证
  • 更多需求...

下面通过这些点,来实现封装一个二次的el-form组件。

从字段开始

图片描述

拿业务用到的表单来举例,在这个表单中的需求分析。

首先是el-form-item的类型:

  • tag类型显示
  • input输入
  • select选择
  • 按钮或者图片的显示或者绑定操作
  • textarea输入

然后再分析每个节点:

  • label宽度
  • 是否需要验证
  • placeholder显示
  • 验证规则
  • 绑定的相关事件
  • 是否可为readonly/disabled
  • 节点的class/样式 (一行显示一个或者多个)

初步分析,大概会有这些需求,接下来对单个问题一一来分析解决。

设计渲染字段列表

正常情况下,我们使用form,它的子项会是这样的,比如有input和select:

// input类型
<el-form-item label="活动名称" prop="name">
  <el-input v-model="ruleForm.name"></el-input>
</el-form-item>

// select类型
<el-form-item label="活动区域" prop="region">
  <el-select v-model="ruleForm.region" placeholder="请选择活动区域">
    <el-option label="区域一" value="shanghai"></el-option>
    <el-option label="区域二" value="beijing"></el-option>
  </el-select>
</el-form-item>

仔细一看,外面那一层壳是可以拿掉的,比如长这样:

<el-form-item label="label" prop="prop">
  // 如果是input类型 
  <el-input v-if="input" v-model="ruleForm.name"></el-input>
  // 如果是select类型 
  <el-select v-if="select" v-model="ruleForm.region" placeholder="请选择活动区域">
    <el-option label="区域一" value="shanghai"></el-option>
    <el-option label="区域二" value="beijing"></el-option>
  </el-select>
</el-form-item>

那由此我们可以设计出循环form的字段列表:

  • label (字段名)
  • value (字段值 prop,后面会有value代替)
  • type (字段类型 input/select/password/textarea等)

然后除了上面的例子我们还可以自己扩展一些字段:

  • event (绑定的方法)
  • list (列表 如果是select类型,需要有对于的list去渲染)
  • TimePickerOptions (时间配置 如果是时间类型,可以传入配置)
  • disabled (是否禁止)
  • filterable (是否可筛选)
  • clearable (是否可清除)
  • required (是否必填 根据这个字段,去设置对于的验证规则)
  • validator (自定义验证 验证时将使用自定义验证方法)
  • show (是否显示, 布尔值或者是函数,下面会对联动渲染详细分析)
  • 更多(根据需求和场景扩展更多字段)

然后完整的字段配置列表大概是这样的(个人使用场景,不需要使用到所有的设计字段):

        fieldList: [
          { label: '账号', value: 'account', type: 'input', required: true, validator: checkAccount },
          { label: '密码', value: 'password', type: 'password', required: true, validator: checkPwd },
          { label: '昵称', value: 'name', type: 'input', required: true },
          { label: '性别', value: 'sex', type: 'select', list: 'sexList', required: true },
          { label: '头像', value: 'avatar', type: 'slot', className: 'el-form-block' },
          { label: '手机号码', value: 'phone', type: 'input', validator: checkPhone },
          { label: '微信', value: 'wechat', type: 'input', validator: checkWechat },
          { label: 'QQ', value: 'qq', type: 'input', validator: checkQQ },
          { label: '邮箱', value: 'email', type: 'input', validator: checkEmail },
          { label: '描述', value: 'desc', type: 'textarea', className: 'el-form-block' },
          { label: '状态', value: 'status', type: 'select', list: 'statusList', required: true }
        ]

组件内部怎么操作,很简单,根据规则,一一对应循环字段,绑定属性就ok了,所以组件内部template就是这么点代码:

  <el-form
    ref="form"
    class="page-form"
    :class="className"
    :model="data"
    :rules="rules"
    :label-width="labelWidth"
  >
    <el-form-item
      v-for="(item, index) in getConfigList()"
      :key="index"
      :prop="item.value"
      :label="item.label"
      :class="item.className"
    >
      <!-- solt -->
      <template v-if="item.type === 'slot'">
        <slot :name="'form-' + item.value" />
      </template>
      <!-- 普通输入框 -->
      <el-input
        v-if="item.type === 'input' || item.type === 'password'"
        v-model="data[item.value]"
        :type="item.type"
        :disabled="item.disabled"
        :placeholder="getPlaceholder(item)"
        @focus="handleEvent(item.event)"
      />
      <!-- 文本输入框 -->
      <el-input
        v-if="item.type === 'textarea'"
        v-model.trim="data[item.value]"
        :type="item.type"
        :disabled="item.disabled"
        :placeholder="getPlaceholder(item)"
        :autosize="{minRows: 2, maxRows: 10}"
        @focus="handleEvent(item.event)"
      />
      <!-- 计数器 -->
      <el-input-number
        v-if="item.type === 'inputNumber'"
        v-model="data[item.value]"
        size="small"
        :min="item.min"
        :max="item.max"
        @change="handleEvent(item.event)"
      />
      <!-- 选择框 -->
      <el-select
        v-if="item.type === 'select'"
        v-model="data[item.value]"
        :disabled="item.disabled"
        :clearable="item.clearable"
        :filterable="item.filterable"
        :placeholder="getPlaceholder(item)"
        @change="handleEvent(item.event, data[item.value])"
      >
        <el-option
          v-for="(childItem, childIndex) in listTypeInfo[item.list]"
          :key="childIndex"
          :label="childItem.key"
          :value="childItem.value"
        />
      </el-select>
      <!-- 日期选择框 -->
      <el-date-picker
        v-if="item.type === 'date'"
        v-model="data[item.value]"
        :type="item.dateType"
        :picker-options="item.TimePickerOptions"
        :clearable="item.clearable"
        :disabled="item.disabled"
        :placeholder="getPlaceholder(item)"
        @focus="handleEvent(item.event)"
      />
      <!-- 信息展示框 -->
      <el-tag v-if="item.type === 'tag'">
        {{ $fn.getDataName({dataList: listTypeInfo[item.list], value: 'value', label: 'key', data: data[item.value]}) || '-' }}
      </el-tag>
    </el-form-item>
  </el-form>

自定义和联动

通过上面的操作,我们完成了基本的表单动态渲染,但是只是针对于模版型的场景,举个例子,如果表单中我要显示选择头像,或者需要增加一个第三方的控件,这个时候,上面的渲染规则就有些无能威力了,所以我们需要表单组件支持自定义渲染。

slot-组件自定义神器

不了解的同学请戳这个vue中的slot属性
在上面,组件的代码中有这样一段:

      <!-- solt -->
      <template v-if="item.type === 'slot'">
        <slot :name="'form-' + item.value" />
      </template>

这段代码的意思是:渲染类型为slot,插槽的名称为‘form-’前缀加上字段的值。
有什么用呢?我们回到使用组件的页面。

图片描述

在form中,这里有一个子项,需要显示图片和按钮,这个时候组件内部已经定义了插槽,并且有对于的名字,我们只需要在外部将对应的插槽传入到组件中,即可实现自定义内容,请看对应代码:

        <!-- 自定义插槽-选择头像 -->
        <template v-slot:form-avatar>
          <div class="slot-avatar">
            <img
              v-imgAlart
              :src="formInfo.data.avatar"
              style="height: 30px;"
            >
            <el-button
              v-waves
              type="primary"
              icon="el-icon-picture"
              size="mini"
              @click="handleClick('selectFile')"
            >
              {{ formInfo.data.avatar ? '更换头像' : '选择头像' }}
            </el-button>
          </div>
        </template>
      </page-form>

组件内插槽除了可以接收对应名字的内容外,还可以将组件中的数据通过插槽传输到外部,在外部使用组件数据渲染内容,自定义组件神器,请务必了解

表单联动

表单联动,推荐阅读element文章---再也不想写表单了
表单组件,重要点之一就是表单联动了,我们来分析一下联动的场景:

  1. 创建用户时,账号可以填写,编辑用户时,账号只能查看
  2. 创建时表单显示的字段只有三个,编辑时可查看到字段为十个
  3. 单选框,选择A,AA字段消失,选择B, AA,BB字段消失,选择C,所有字段都显示,并且变成必填
  4. 更多联动场景...

显示联动

1.字段定义为布尔值时

在字段定义的时候,有定义一个字段,show,我们可以将show字段定义为布尔值,然后在页面中watch定义数据的是否显示,比如这段代码:

    // 根据弹窗类型做数据联动
    'dialogInfo.type' (val) {
      const fieldList = this.formInfo.fieldList
      switch (val) {
        case 'userInfo':
          fieldList.forEach(item => {
            if (['user_old_pwd', 'user_new_pwd', 'user_new1_pwd'].includes(item.value)) {
              item.show = true
            } else {
              item.show = false
            }
          })
          break
        case 'password':
          fieldList.forEach(item => {
            if (!['user_old_pwd', 'user_new_pwd', 'user_new1_pwd'].includes(item.value)) {
              item.show = true
            } else {
              item.show = false
            }
          })
          break
      }
    }

2.字段定义为函数时

如果不想再组件外部操作,想把显示隐藏的渲染逻辑交给组件内部处理,那我们可以定义show字段为函数,比如这段代码:

{label: '名称', value: 'name', show: data => {
    return data.type === 'userInfo'
}}

这两种方式是我目前自定义组件场景中有用到的,根据需求,使用不同的方法

逻辑联动和事件中间件设计

1. 字段定义为函数时
逻辑联动表示form中某一项发生改变,其他一项或者多项会根据对应的数据发生相对的改变,比如下面这段代码(因为个人项目目前没有这种业务,所以并没有相关示例,代码来自element博客):

[
    {
      title: '活动类型',
      key: 'act_type',
      type: 'radio',
      props: {
        options: { 1: '拉新', 2: '冲单', 3: '回馈' }
      }
    },
    {
      title: '生效方式',
      key: 'effect_type',
      type: 'radio',
      props(form) {
        const value;
        const map = { 1: '立即', 2: '按时间', 3: '按条件' };
        if (form.act_type === 3) {
          value = 1;
        }
        return { 
          value: value,
          options: map 
        };
      }
    }
  ]

将需要联动的字段定义为方法,方法接收表单数据,方法根据数据进行对应的联动,这种方法可以基本完美解决各种问题,在组件内部则需要对属性添加一层判断,普通类型只需要进行绑定,typeof为function需要绑定运行的函数。

2. 定义事件字段,通过中间件派发到外部处理
定义事件字段,定义的字段列表中,我们可以设计一个event字段,当事件上绑定方法的时候,字段设计为这样:

{ label: '性别', value: 'sex', type: 'select', list: 'sexList', event: 'changeName', required: true }

示例代码为select类型,组件的input绑定代码上也需要绑定相关事件:

      <!-- 选择框 -->
      <el-select
        v-if="item.type === 'select'"
        v-model="data[item.value]"
        :disabled="item.disabled"
        :clearable="item.clearable"
        :filterable="item.filterable"
        :placeholder="getPlaceholder(item)"
        @change="handleEvent(item.event, data[item.value])"
      >
        <el-option
          v-for="(childItem, childIndex) in listTypeInfo[item.list]"
          :key="childIndex"
          :label="childItem.key"
          :value="childItem.value"
        />
      </el-select>

通过handleEvent中间件,绑定对应类型和对应数据,中间件函数的用处就负责进行页面和组件的通讯,组件内部穿出事件类型和数据,页面接收并且处理,不论多少的事件,只有一个中间件即可以解决。
组件内部中间件处理:

    // 绑定的相关事件
    handleEvent (evnet) {
      this.$emit('handleEvent', evnet)
    }

组件使用代码:

      <!-- form -->
      <page-form
        v-if="dialogInfo.type !== 'userTransfer'"
        :ref-obj.sync="formInfo.ref"
        :data="formInfo.data"
        :field-list="formInfo.fieldList"
        :rules="formInfo.rules"
        :label-width="formInfo.labelWidth"
        :list-type-info="listTypeInfo"
        @handleClick="handleClick"
        @handleEvent="handleEvent"
      >
        <!-- 自定义插槽-选择头像 -->
        <template v-slot:form-avatar>
          <div class="slot-avatar">
            <img
              v-imgAlart
              :src="formInfo.data.avatar"
              style="height: 30px;"
            >
            <el-button
              v-waves
              type="primary"
              icon="el-icon-picture"
              size="mini"
              @click="handleClick('selectFile')"
            >
              {{ formInfo.data.avatar ? '更换头像' : '选择头像' }}
            </el-button>
          </div>
        </template>
      </page-form>

页面中处理事件:

    // 触发事件
    handleEvent (event, data) {
      switch (event) {
        case 'changeName':
            console.log(data)
            // 触发相关联动逻辑
          break
      }
    }

两种方式,各有优劣,这里主要分享思路,大家根据自己业务选择合适的方案。

表单验证处理

表单验证,戳这个,上次写的验证还热乎的---->element-ui表单全局验证的方法

最后

项目地址

组件地址

相关文章

实现elementUI表单的全局验证

组件化页面:封装el-table


我在长安长安
1.2k 声望42 粉丝

一个很懒的假前端