likes
comments
collection
share

Vue2 Element Schema Form 配置式生成表单的实现

作者站长头像
站长
· 阅读数 24

前置知识点

为了实现一个Schema Form配置式表单的生成,需要了解一部分前置知识点。

Component

vue 提供了一个内置组件 Component,用来动态渲染组件,举个例子,本篇文章以Element UI 为例,假设我们全局注册了Element UI的组件,那么下面一段代码

<Component is="el-input"></Component>

将被渲染为el-input组件。

好了,最重要的实现知识点已经完了,只要了解了这一点,我这个Schema Form的实现思路,你就能完全看懂了,就这么简单。

但是为了我们表单的易用性,我们再了解一点儿其他的。

比如Component组件并非只能接收一个字符串,渲染全局组件,同时可以接收一个Component组件,也就是说,当我们Element UI 组件没有全局注册的时候,我们可以通过import { ElInput } from 'element',传递给Component组件,同样可以完成渲染一个el-input组件的功能。

$attrs

第二个知识点,vue属性透传,假如你有这么一个疑惑 —— 我对el-input进行了二次封装,那el-input接收的props我是否需要在二次封装的组件中进行props的定义,再逐一传递给el-input才能生效。

如果有这样的疑惑那么$attrs可以帮你,对于二次封装的组件,通过定义v-bind="$attrs",传递给父组件的属性即可透传给el-input,于是就有了这么一个疑问,$attrs可以绑定给v-bind,那么一个普通的对象可以吗,答案是可以的。

这是我们 schema form 变得好用的第二个重要的知识点。

$listeners

$attrs 相同,做事件透传的,通过v-on绑定。

以上就是我认为,实现Schema Form配置式生成表单所需要掌握的全部知识及扩展思考,接下来,我们简单完成一个配置式表单.

表单的结构是什么样的

1、我可能希望对表单进行一部分特殊的处理,所以,包一层div总是没错的,当然,这是我的习惯,你们可以自行选择。

2、既然是表单,那一定被el-form包裹。

3、为了配置式布局的需要,我希望引入el-row el-col栅格系统

可以参考我新的文章 基于Vue2、Element开发的自适应栅格组件 弄一个更简单易用的栅格组件

4、无论如何完善表单,必然不可能满足所有要求,那最简单的方式就是抛出slot

5、有些时候我希望渲染文本呢?有些时候我希望渲染字段的值而不涉及任何组件呢?

6、最后才是渲染对应的组件

那么,一个表单的结构化雏形就完成了。

<div class="schema-form">
    <el-form>
        <el-row>
            <el-col v-for="item in config" :key="item.key">
                <el-form-item>
                    <slot v-if="item.component === 'slot'" :name="item.slotName"></slot>
                    <div v-else-if="item.component === 'innerText'"></div>
                    <template v-else>
                        渲染对应组件,但需要对某些组件特殊处理
                    </template>
                </el-form-item>
            </el-col>
        </el-row>
    </el-form>
</div>

从上面的结构中,我们再来思考,我们的配置config数组的字段结构应该是什么样的。

配置数组数据结构

1、el-form 可能需要一些字段注入,作为最外层的组件,将传入schema form的字段都给它

2、el-form 需要传入一个数据源,这个数据源可以内部定义再传给外部,也可以传入一个对象,利用对象的特性做双向绑定,我选择了后者

3、el-col项可能有时不显示,所以config上扩展一个字段hidden,用于控制该项是否显示。

4、el-form-item的属性透传

5、innerText的样式定义

一些特殊的描述出来了,其余的再赘述,那么,config的数据结构就是这样

{
    key: '', // 当前字段的 key 值,同时会传到 el-form-item 的prop,不传数据验证和重置会失效
    label: '', // 当前 el-form-item 的label
    hidden: '', // 当前表单项是否展示
    labelWidth: '', // el-form-item 的label宽度
    component: '', // 支持 slot、innerText、Component,渲染成什么
    slotName: '', // compoment 为 slot 时,该值为对应slot的名字供外部使用
    innerClass: '', // component 为 innerText 时,给文本的样式,通常为全局样式
    innerStyle: '', // 同上
    innerText: '', // component 为 innerText 时,优先显示的文本,否则会显示当前的字段值
    itemProps: '', // 注入到 el-form-item 的属性
    props: '', // 当 component 为渲染组件时,注入到渲染组件当中的属性
    listeners: '', // 当 component 为渲染组件时,注入到渲染组件当中的事件
}

当把这些实现之后,其余需要扩展的功能可以自行实现,我这里也只是在业务中的扩展,并非完善的。

于是,我们的表单组件就变成了这样

<template>
  <div class="nx-form">
    <el-form
      ref="form"
      v-bind="$attrs"
      :model="source"
      class="nx-form"
    >
      <el-row :gutter="20">
        <el-col
          v-for="item in config"
          :key="item.key"
          :span="item.hidden ? 0 : item.span || 8"
        >
          <el-form-item
            v-if="!item.hidden"
            :label="item.label"
            :prop="item.key"
            :label-width="item.labelWidth || labelWidth || '120px'"
            v-bind="item.itemProps"
          >
            <slot v-if="item.component === 'slot'" :name="item.slotName"></slot>
            <div
              v-else-if="item.component === 'innerText'"
              :class="item.innerClass"
              :style="item.style"
            >
              {{ item.innerText || source[item.key] }}
            </div>
            <template v-else>
              <div class="nx-flex-align-center">
                <component
                  :is="item.component"
                  v-model="source[item.key]"
                  style="width: 100%;flex: 1;"
                  v-bind="item.props"
                  v-on="item.listeners"
                >
                  <template v-if="item.component === 'el-radio-group'">
                    <el-radio
                      v-for="(radio, radioIndex) in item.data"
                      :key="radioIndex"
                      style="margin-top: 10px;"
                      :label="radio.value"
                      :disabled="radio.disabled"
                    >
                      {{ radio.label }}
                    </el-radio>
                  </template>

                  <template v-if="item.component === 'el-checkbox-group'">
                    <el-checkbox
                      v-for="(checkbox, checkboxIndex) in item.data"
                      :key="checkboxIndex"
                      :label="checkbox.value"
                    >
                      {{ checkbox.label }}
                    </el-checkbox>
                  </template>

                  <template v-if="item.component === 'el-select'">
                    <el-option
                      v-for="(option, optionIndex) in item.data"
                      :key="optionIndex"
                      :label="option.label"
                      :value="option.value"
                    ></el-option>
                  </template>
                </component>
                <div
                  v-if="item.after"
                  class="nx-form__after"
                >
                  {{ item.after }}
                </div>
              </div>
              <div
                v-if="item.tips"
                class="nx-form__tips"
                v-html="item.tips"
              ></div>
            </template>

          </el-form-item>
          <!-- <div
            v-if="item.tips"
            :style="{ marginLeft: item.labelWidth || '120px' }"
            class="nx-form__tips"
            v-html="item.tips"
          ></div> -->
        </el-col>
      </el-row>
    </el-form>
  </div>
</template>
export default {
  name: 'NxForm',
  components: {},
  props: {
    config: {
      type: Array,
      default: () => []
    },
    source: {
      type: Object,
      default: () => ({})
    }
  },
  methods: {
    resetFields() {
      this.$refs.form.resetFields();
    },
    clearValidate() {
      this.$refs.form.clearValidate();
    },
    async validate() {
      const valid = await this.$refs.form.validate();
      return valid;
    }
  }
};

可以看到,我扩展了一个data字段,用来作为el-select el-checkbox-group el-radio-group的数据源,这些特殊的组件单独列出就行了,基本上也之后有这么几个。

methods里也只是为了方便添加了一些常用的form方法,基本不做逻辑处理。

现在看一下,这样的配置可以生成怎样的表单

注意:我这里定义了一部分全局样式,比如在schema form下的组件,全部占满整格,使其比较美观。

Vue2 Element Schema Form 配置式生成表单的实现

生成这样一个常用的筛选项表单

<template>
  <div>
    <nx-form
      ref="searchForm"
      :source="searchForm"
      :config="searchConfig"
    >
      <div slot="search">
        <el-button type="primary" @click="$refs.nxTable.search(1)">查询</el-button>
        <el-button @click="reset()">重置</el-button>
      </div>
    </nx-form>
  </div>
</template>
<script>
export default {
  data() {
     searchForm: {
        keyWord: '', 
        newsType: '', 
        newsStatus: '', 
        publishTime: [],
        createTime: []
     },
  },
  computed: {
      searchConfig() {
      return [
        {
          key: 'keyWord',
          component: 'el-input',
          span: 8,
          label: '关键字',
          props: {
            placeholder: '标题/创建人',
            clearable: true
          }
        },
        {
          key: 'newsType',
          component: 'el-select',
          span: 8,
          label: '类型:',
          props: {
            placeholder: '请选择',
            clearable: true
          },
          data: this.newsTypes
        },
        {
          key: 'newsStatus',
          component: 'el-select',
          span: 8,
          label: '状态:',
          props: {
            placeholder: '请选择',
            clearable: true
          },
          data: statusTypes
        },
        {
          key: 'publishTime',
          component: 'el-date-picker',
          label: '发布时间:',
          span: 8,
          props: {
            clearable: true,
            valueFormat: 'timestamp',
            type: 'datetimerange',
            defaultTime: ['00:00:00', '23:59:59'],
            rangeSeparator: '-',
            startPlaceholder: '请选择开始时间',
            endPlaceholder: '请选择结束时间'
          }
        },
        {
          key: 'createTime',
          component: 'el-date-picker',
          label: '创建时间:',
          span: 8,
          props: {
            clearable: true,
            valueFormat: 'timestamp',
            type: 'datetimerange',
            defaultTime: ['00:00:00', '23:59:59'],
            rangeSeparator: '-',
            startPlaceholder: '请选择开始时间',
            endPlaceholder: '请选择结束时间'
          }
        },
        {
          component: 'slot',
          slotName: 'search',
          span: 8,
          labelWidth: '0'
        }
      ];
    }
  }
}
</script>

其余的找了一些,也没啥特别的,就不贴了,简单来说,这个百来行的组件,应用在多个大型项目当中,易用性,扩展性,没有出现什么问题,配合我们自定义的其他table组件、dialog组件,十分钟就可以实现一个完整的B端增删改查。

值得一提的是,为了表单的易用性,我们基本上所有的自定义组件都支持了使用v-model做数据绑定,达到简单易用的效果。

结语

代码是找了一个很久的项目写的一个思路,之后的项目中都有改进优化,大致只是分享一个思路,如果有什么想法的,欢迎指正。

补充

有几位朋友对 listeners 比较疑惑,我没讲的太仔细,补充一下。

<component
  :is="item.component"
  v-model="source[item.key]"
  style="width: 100%;flex: 1;"
  v-bind="item.props"
  v-on="item.listeners"
>

可以看到每一个 itemlisteners 被透传到了 Component 组件上,这里用 el-input 举例,我们渲染一个单个的input。

[
    {
        key: 'input',
        component: 'el-input',
        span: 24,
        labelWidth: '120px',
        label: '测试 Input',
        props: {
            placeholder: '请输入...'
        },
        listeners: {
            change: (e) => {
            
            }
        }
    }
]

上面这段代码中,listeners 中传递了 change 事件,相当于

<el-input @change="change"></el-input>

主要是为了满足复杂情况,实际使用还是比较少的。

闲言碎语

来这篇文章的朋友们大多应该是因为另一篇文章下的争论,说实话,我很气愤,"若批评无自由,则赞美无意义",我实在不太理解,为什么技术社区要搞政治正确那一套,我是个暴躁老哥,最为痛恨的就是政治正确那一套,好就是好,坏就是坏,如果缺点不被指出,则社区论坛失去了意义,还是一个人在思考。

我刚入行技术很菜的时候,空有想法而难以实践,因为自身不知道知识点到底缺在哪儿,想搜索都不知道关键词,搜索出来之后还要在茫茫文章里屎里淘金,我不否定"写文章不是为了给你看的,我只是为了记录"这种说法,可是大部分人取一个震惊体的标题,引人进去,到头来发现不过是一个很一般甚至莫名其妙的东西,难道不会很气愤吗?

以前我就很感谢评论区的老哥们,对于一部分很震惊体的文章,我都会直接拉到最后看评论,看一下评论区的老哥们如何评价,基本上对于我来说省去了很多时间,因为确实很多文章,不值得一观;现在,我技术提升了一些,我逛着社区看到一个我做过已经很成熟的东西,我依然想要了解一下别人是如何实现的,是否有借鉴点能帮助我改进我的代码,以那篇争论的文章为例,说实话,我看到一半我就不想看了,我第一想法是"这什么破玩意儿",说实话,不是因为他菜而带来的辱骂,而是他明明有实力有想法能折腾,却让这样的东西存在了一年,还取了这么个震惊标题发出来,实在令我气愤,于是带上了优化实现技术点,语气不佳的回了一句,然后,争端开始了。

回到上面说的,技术社区是应该政治正确的地方吗?哈哈哈,如果社区是政治正确的,那我认为也没有存在的必要了,我入行以来,但凡对我有过帮助的,技术上对我有过指点的,哪怕只是对我代码格式规范提的意见,只是是好的,无论语气,我都会至少请一杯奶茶,或者请吃饭,所以我渐渐的和那些大佬关系越来越好,也从他们身上学到了更多的东西,我也遇到过那种政治正确的人,就是那种你不能逼逼他的代码,哪怕你配好了eslint,他也只会想办法去把这玩意儿弄掉,而不是规范化自己的代码,后来他被开了,我想如果我当初也这么政治正确的话,我也被开了。

其实我也曾经想过写文章分享很多知识点,一方面是因为自己在解决这些问题的时候看了很多文章,几乎都讲的很片面,另一方面也是对自己技术的一个记录,但总是写着写着就觉得不尽人意,也担心会写的不好误人子弟,我不怕有人批评我做的不好,我担心的是别人看了我的文章,还是什么都没弄出来,浪费别人的时间,给组员们的公用组件也一样,譬如axios的封装,vue2、vue3的、js、ts的,都是在背后测试了各种可能会出现的情况,模拟了很多场景,确定这样的实现至少可以覆盖到绝大部分场景,覆盖不到的场景也给了自定义的能力才会开放出来,也尽量少的加入一些过于私人化的东西,减少变成公司个人组件库,换一家公司重学一个组件库这种情况;我始终认为,一个被公开出来的东西,至少要解决掉大部分的问题,而不是明显可以感知到的,这个东西不配作为一个全局组件。

我不想做一个政治正确的人,甚至不想做一个"有素质"的人,所以,对于不好的习惯,不好的实现指正,得到的回复是PUA的话,如果是我的组员,我只会让他滚;我经常说,我不介意菜的人,我也不介意笨的人,我更不介意教又菜又笨的人,但是,你要是不愿意学,不考量我的建议就拒绝的话,我会很介意让你待在我的组内。