数据驱动的公共表单组件

1,067 阅读3分钟

问题背景

以前刚用element-ui的时候,新增表单,对于每个form-item都是写死的,form-item里面的内容也是写死的,比如下面

<el-form  label-width="80px" :model="formLabelAlign">
  <el-form-item label="名称">
    <el-input v-model="formLabelAlign.name"></el-input>
  </el-form-item>
  <el-form-item label="活动区域">
    <el-input v-model="formLabelAlign.region"></el-input>
  </el-form-item>
  <el-form-item label="活动形式">
    <el-input v-model="formLabelAlign.type"></el-input>
  </el-form-item>
  ....
</el-form>

这样写有个问题,就是每次新增一个form-item都要手动复制一次,然后修改里面的label、propel-input绑定的v-model,而且for-item越多.vue就越大,文件就越臃肿

初代解决方案

熟练了以后,针对上面的例子,发现el-form-item可以通过v-for循环生成,因为每个form-item的结构和分发的内容都是一样的,于是自然想到了下面的解决方案:

<template>
  <el-form  :model="formLabelAlign">
    <el-form-item 
        v-for="item in FORM_ITEMS" 
        :key="item.prop" 
        :label="item.label"
        :prop="item.prop"
        :label-width="item.lableWidth"
    >
      <el-input v-model="formLabelAlign[prop]"></el-input>
    </el-form-item>
  </el-form>
</template>
<script>
export default {
    name: 'CommonForm',
    props: {
        formLabelAlign: {
            type: Object,
            require: true,
            defalut: () => ({})
        }
    },
    data() {
        return {
            FORM_ITEMS: [
                {prop: 'name', label: '名称', lableWidth: 100 },
                {prop: 'region', label: '活动区域', lableWidth: 100 },
                {prop: 'type', label: '活动形式', lableWidth: 100 },
            ]
        }
    },
}
</script>

上面通过data声明在FORM_ITEMS,每个item定义了一些想要的属性label、prop等等,循环生成我们想要form-item,这样一来我们的template并不需求去修改,每次新增只要在FORM_ITEMS新增一条数据即可

更多的场景

上面的例子适用的场景很单一,限制了我们里面的form-item的内容只是input,问题我们日常的业务场景表单包含了许多类型,比如下拉框,多选框,文本域,单选框,多选框,上传等等,那么我们上面的封装明显已经不适用

针对不同类型的form-item内容的解决方案

针对上面说的更多场景,我们很自然可以想到通过枚举去实现不同类型form-item的内容,像下面这样:

公共表单第二版

<template>
  <el-form :model="formLabelAlign">
    <el-form-item
      v-for="item in FORM_ITEMS"
      :key="item.prop"
      :label="item.label"
      :prop="item.prop"
    >
      <el-input
        v-if="item.el === 'input'"
        :placeholder="item.placeholder"
        v-model="formLabelAlign[item.prop]"
      ></el-input>

      <el-input
        v-if="item.el === 'textarea'"
        type="textarea"
        :rows="item.row || 5"
        :placeholder="item.placeholder"
        v-model="formLabelAlign[item.prop]"
      >
      </el-input>

      <el-switch
        v-if="item.el === 'switch'"
        v-model="formLabelAlign[item.prop]"
      ></el-switch>

      <el-slider
        v-if="item.el === 'slider'"
        v-model="formLabelAlign[item.prop]"
      ></el-slider>

      <el-select v-if="item.el === 'select'">
        <el-option
          v-for="option in item.options"
          :key="option.label"
          :label="option.label"
          :value="option.value"
        ></el-option>
      </el-select>

      <el-radio-group
        v-if="item.el === 'radio'"
        v-model="formLabelAlign[item.prop]"
      >
        <el-radio
          v-for="radio in item.radios"
          :key="radio.label"
          :label="radio.label"
          v-mode="radio.value"
          >{{ radio.label }}</el-radio
        >
      </el-radio-group>
      ...等等等
    </el-form-item>
  </el-form>
</template>
<script>
export default {
  name: "CommonForm",
  props: {
    formLabelAlign: {
      type: Object,
      require: true,
      defalut: () => ({}),
    },
  },
  data() {
    return {
      FORM_ITEMS: [
        { prop: "name", label: "名称", lableWidth: 100, el: "input" },
        { prop: "region", label: "活动区域", lableWidth: 100, el: "input" },
        { prop: "type", label: "活动形式", lableWidth: 100, el: "input" },
        {
          prop: "info",
          label: "信息介绍",
          lableWidth: 100,
          el: "textarea",
          row: 3,
        },
        { prop: "switch", label: "是否启动", lableWidth: 100, el: "switch" },
        { prop: "percent", label: "比例", lableWidth: 100, el: "slider" },
        {
          prop: "types",
          label: "类型选择",
          lableWidth: 100,
          el: "radio",
          radios: [
            { value: 0, label: "yes" },
            { value: 1, label: "no" },
          ],
        },
        {
          prop: "city",
          label: "城市选择",
          lableWidth: 100,
          el: "radio",
          radios: [
            { value: 0, label: "北京" },
            { value: 1, label: "深圳" },
            { value: 2, label: "珠海" },
          ],
        },
      ],
    };
  },
};
</script>

简单的思路就是我们通过枚举各种可能elment-ui表单元素,通过el这个属性来决定这次要渲染是什么类型的标签元素,主要你不嫌麻烦,可以在template枚举所有的element-ui元素

遗留的问题:

  • 真实开发不一定每个form-item里面只是一个input或者checkbox, 有可能是input和button的组合,也有可能form-item里面直接嵌套了另外一个表单 比如下面:
  <el-form-item label="活动名称">
    <el-input v-model="form.name"></el-input>
     <el-radio-group v-model="radio">
    	<el-radio :label="3">备选项</el-radio>
    	<el-radio :label="6">备选项</el-radio>
    	<el-radio :label="9">备选项</el-radio>
     </el-radio-group>
  </el-form-item>
   <el-form-item label="活动区域">
    <el-form :model="formData">
    	....
    </el-form>
  </el-form-item>

第三版公共表单组件

上面的场景我们的上面封装的版本2已不能用了,这个时候应该怎么处理呢,很自然的我们想到了具名插槽slot,比如不适应的某个表单项, 把prop传给slot作为他的插槽名称,于是我又把公共表单改成了下面的样子:

<template>
  <el-form :model="formLabelAlign">
    <el-form-item
      v-for="item in FORM_ITEMS"
      :key="item.prop"
      :label="item.label"
      :prop="item.prop"
    >
      <el-input
        v-if="item.el === 'input'"
        :placeholder="item.placeholder"
        v-model="formLabelAlign[item.prop]"
      ></el-input>

      ...上面的内容缩略

     <slot :slot="item.prop" />

    </el-form-item>
  </el-form>
</template>
<script>
export default {
  name: "CommonForm",
  props: {
    formLabelAlign: {
      type: Object,
      require: true,
      defalut: () => ({}),
    },
  },
  data() {
    return {
      FORM_ITEMS: [
        { prop: "name", label: "姓名", lableWidth: 100, el: "input" },
        { prop: "alias", label: "花名", lableWidth: 100, el: "custom" },
      ],
    };
  },
};
</script>

如何使用我们的第三版表单组件

上面的FORM_ITEMS为了演示我们都是在data里面写死,实际上需要通过props传入,需要改成这样

<template>
  <el-form :model="formLabelAlign">
    <el-form-item
      v-for="item in formItems"
      :key="item.prop"
      :label="item.label"
      :prop="item.prop"
    >
      <el-input
        v-if="item.el === 'input'"
        :placeholder="item.placeholder"
        v-model="formLabelAlign[item.prop]"
      ></el-input>

      ...上面的内容缩略

     <slot :slot="item.prop" />

    </el-form-item>
  </el-form>
</template>
<script>
export default {
  name: "CommonForm",
  props: {
    formLabelAlign: {
      type: Object,
      require: true,
      defalut: () => ({}),
    },
    formItems: {
      type: Array,
      require: true,
      
    },
  },

};
</script>

具体使用

<template >
    <CommonForm :form-label-align="formData" :form-items="FORM_ITEMS">
        <!-- 因为我们应该在公共组件注册过这个插槽, alias这项不符合我们的格式,这里就可以自定义 -->
        <template slot="alias">
            <h3>{{formData.alias}}</h3>
            <el-input v-model="formData.alias" />
        </template>
    <CommonForm>
</template>
<script>
import CommonForm from "./common-form";
export default {
  data() {
    return {
      formData: {
        name: "dogeWin",
        alias: "道格温·狗胜",
      },
      FORM_ITEMS: [
        { prop: "name", label: "姓名", lableWidth: 100, el: "input" },
        { prop: "alias", label: "花名", lableWidth: 100, el: "custom" },
        {
          prop: "info",
          label: "信息介绍",
          lableWidth: 100,
          el: "textarea",
          row: 3,
        },
        { prop: "switch", label: "是否启动", lableWidth: 100, el: "switch" },
        { prop: "percent", label: "比例", lableWidth: 100, el: "slider" },
      ],
    };
  },
  components: {
    CommonForm,
  },
};
</script>

第四版公共表单组件

还有一种方式可以自定义传入的内容,函数式组件functional componet,这里直接贴出实现,有兴趣的可以去研究下

<template>
  <el-form :model="formLabelAlign">
    <el-form-item
      v-for="item in FORM_ITEMS"
      :key="item.prop"
      :label="item.label"
      :prop="item.prop"
    >
      <el-input
        v-if="item.el === 'input'"
        :placeholder="item.placeholder"
        v-model="formLabelAlign[item.prop]"
      ></el-input>

      ...上面的内容缩略
    <!-- 如果传入的item有render方法,那么就由我们的函数式组件去渲染,绑定params为我们传入的formData -->
     <functionalComponent v-if="item.render"  :params="formLabelAlign" />

     <slot :slot="item.prop" />
    </el-form-item>
  </el-form>
</template>
<script>
// 这个函数组件十分简单,就是把传入的render作为自己的render,params是我们传入的formData
const functionalComponent = {
  functional: true,
  props: {
    render: Function,
    params: Object,
  },

  render(h, ctx) {
    const params = { ...ctx.props.params };
    return ctx.props.render.call(ctx, h, params);
  }
}
export default {
  name: "CommonForm",
  props: {
    formLabelAlign: {
      type: Object,
      require: true,
      defalut: () => ({}),
    },
    formItems: {
      type: Array,
      require: true,
      
    },
  },
  components: {
      functionalComponent
  }
};
</script>

第四版公共表单组件的使用

<template >
  <CommonForm :form-data="formData" :form-items="FORM_ITEMS" />
</template>
<script>
import CommonForm from "./common-form";
export default {
  data() {
    return {
      formData: {
        name: "dogeWin",
        alias: "道格温·狗胜",
      },
      FORM_ITEMS: [
        { prop: "name", label: "姓名", lableWidth: 100, el: "input" },
        { prop: "alias", label: "花名", lableWidth: 100, 
        	// jsx语法,使用请先安装对于版本的jsx插件
            render(h, params) {
                return (
                    <div>
                        <h3>{params.alias}</h3>
                        <el-input value={params.alias} onInput={e => params.alias = e} />
                    <div>
                )
            }
        },
      ],
    };
  },
  components: {
    CommonForm,
  },
};
</script>

总结

通过我们的渐进式学习,一个实用的公共表单组件就完成了,我直接就是一个好家伙~~=!