自己手动造轮子封装 vForm 组件

1,017 阅读9分钟

自己手动造轮子封装 vForm 组件

1. 繁琐的第三方框架带来的困扰

我们在使用第三方工具进行开发的时候常常会遇到这样的问题,就是第三方类库提供 的API 会非常的繁琐,以至于我们每次都需要书写很多的** HTML 代码**。例如我们书写一个表单,当我们使用 ElementUI 框架的时候,我们可能会看到这样的代码。

<el-form ref="form" :model="form" label-width="80px">
  <el-form-item label="活动名称">
    <el-input v-model="form.name"></el-input>
  </el-form-item>
  <el-form-item label="活动区域">
    <el-select v-model="form.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="活动时间">
    <el-col :span="11">
      <el-date-picker type="date" placeholder="选择日期" v-model="form.date1" style="width: 100%;"></el-date-picker>
    </el-col>
    <el-col class="line" :span="2">-</el-col>
    <el-col :span="11">
      <el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;"></el-time-picker>
    </el-col>
  </el-form-item>
  <el-form-item label="即时配送">
    <el-switch v-model="form.delivery"></el-switch>
  </el-form-item>
  <el-form-item label="活动性质">
    <el-checkbox-group v-model="form.type">
      <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
      <el-checkbox label="地推活动" name="type"></el-checkbox>
      <el-checkbox label="线下主题活动" name="type"></el-checkbox>
      <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
    </el-checkbox-group>
  </el-form-item>
  <el-form-item label="特殊资源">
    <el-radio-group v-model="form.resource">
      <el-radio label="线上品牌商赞助"></el-radio>
      <el-radio label="线下场地免费"></el-radio>
    </el-radio-group>
  </el-form-item>
  <el-form-item label="活动形式">
    <el-input type="textarea" v-model="form.desc"></el-input>
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="onSubmit">立即创建</el-button>
    <el-button>取消</el-button>
  </el-form-item>
</el-form>
export default {
    data() {
      return {
        ruleForm: {
          name: '',
          region: '',
          date1: '',
          date2: '',
          delivery: false,
          type: [],
          resource: '',
          desc: ''
        },
        rules: {
          name: [
            { required: true, message: '请输入活动名称', trigger: 'blur' },
            { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
          ],
          region: [
            { required: true, message: '请选择活动区域', trigger: 'change' }
          ],
          date1: [
            { type: 'date', required: true, message: '请选择日期', trigger: 'change' }
          ],
          date2: [
            { type: 'date', required: true, message: '请选择时间', trigger: 'change' }
          ],
          type: [
            { type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' }
          ],
          resource: [
            { required: true, message: '请选择活动资源', trigger: 'change' }
          ],
          desc: [
            { required: true, message: '请填写活动形式', trigger: 'blur' }
          ]
        }
      };
    },
    methods: {
      submitForm(formName) {
        this.$refs[formName].validate((valid) => {
          if (valid) {
            alert('submit!');
          } else {
            console.log('error submit!!');
            return false;
          }
        });
      },
      resetForm(formName) {
        this.$refs[formName].resetFields();
      }
    }
  }


怎么样?是不是觉得我们构建一个表单,这件事情变得提别的繁琐了呢?其实这只是一个例子而已,在我们编码的时候,我们会发现,自己话费了大量的时间去编写重复的代码,例如我们写一个 表单,表单内包含的元素,例如input,textarea ,我们都会常常用到,并且会无数次的重读书写。
那么有没有一种方法能让我们**逃离繁琐的API 和 HTML 代码 **快速的构建一个表单应用呢?

2. 关于数据驱动组件,组件解析呈现视图的思考。

带着我们上面提出的问题,我们可以做出以下大胆的思考
既然组件的输入输出都是数据, 那么由此可见数据在组件化的应用中的重要性,也就是说,数据就是组件的灵魂和核心,有了数据组件就可以工作,组件工作就是为了处理数据,进来的是数据A出去的是数据 B ,假如我们针对某些应用中常见的场景,提炼出超级组件(后面我们称为设计器),这样的话,我们是不是可以由数据去驱动我们的设计器,由设计器分配数据驱动第三方框架提供的基础组件,然后再由设计器去收集处理结果。这样一来我们coder 只需要关注 设计器的输入输出(也就是数据)就可以了,而没有必要去和框架的基础组件打交道。
在我们的构想中,貌似在我们的脑海中出现了这样的一种简单的过程

数据 A (驱动数据)    =>   设计器    =>     数据 B (输出数据)


**

3. 如何手动造轮子,构建出设计器

在这里我们就拿 form 组件为例, 我们思考form 组件中会出现一些列的 表单应用, 这些表单应用被数据化之后,什么数据类型才能更好的描述呢? 思考一下,emmmmmm...  没错,就是JSON 数据了,这种键值对数据,适合描述表单元素的属性。 再有每个表第元素是不是会有一个name,那么如果我们这样用数据描述一个表单呢? 首先它会是一个JSON 的数据类型,而这个JSON 里面第一个对象会描述表单元素的集合(map),第二个对象会描述这个表单元素最后输出数据的集合(rel)。那么,这样一个模型是不是出现在了我们的脑海里。

export default const formData = {
	map:{
  	userName: {
    	type:'input',
      label:'用户名',
      inputType:'text',
      maxlength:10,
      minlength:3
    }
  },
  rel:{
  	userName:''
	}
}


如上只是一个
数据模型**,也是我们希望的数据API, 那么我们怎么去由上面的数据模型去设计 设计器 组件呢。
首先我们的这个设计器组件应该囊括足够多的表单组件。包括 input , select, radio, textarea, 还要各种常用的表单项目组件
然后我们是不是可以根据type 的类型去确定最终渲染的组件呢?
下面以vue 为例讲解一下开发思路

<form class = "v-form">
  <label
    class="form-item"
    v-for = '(item,key) in formData.map'
    :key = '"formData"+key'   
  >
    <template v-if = 'item.type == "input"'>
      <span> {{ item.label }} </span>
    	<input :name = "key" v-model = "formData.rel[key]" :placehoder="请输入+‘item.label’">
    </template>
    <template v-if = 'item.type == "select"'>
      select...
     </template>
  </label>
</form>
export default {
	name:'VForm',
  props: {
    initData: {
      default() {
        return {
          map:{},
          rel:{}
        }
      }
    }
  }
}

小伙伴们看了上面的代码,自己是不是有一些思考呢。现在我们再看看如何在外部使用这个组件

<v-form :formData = 'formData'>  </ v-form>
export default {
	name:'VForm',
  data() {
  	return {
    	formData: {
      	map:{
          userName: {
            type:'input',
            label:'用户名',
            inputType:'text',
            maxlength:10,
            minlength:3
          }
        },
        rel:{
          userName:''
        }
      }
    }
  }
}

这样一来API 是不是变得特别简单呢? 小伙伴们思考一下  emmmmm.............

4. 组件的灵活性

细心的小伙伴很容易就在使用这个设计器的时候发现很多问题, 比如我们的表单应用如果不是很规则的表单应用呢?

image.png

如 图片中所示,我们的表单中多了中间层的按钮 还有 上面内嵌的表单,甚至我们还会有删除等等一些逻辑出现,这样的一个复杂表单,我们刚刚的思路是不是很难解决了呢? 是不是只要有一点不一样,那么我们的设计器组件就要废掉不能用了呢? 这样一来我们的设计器太死板了。
带着这个问题我们继续思考,怎么样才能使我们的组件变得更加灵活呢?  熟悉vue API 的小伙伴们自然会想起slot(插槽) 这个预设组件, 对就是它, 同样的思路, 我们可以在每一个表单项目结束的地方添加一个插槽文件,而且要用具名插槽。这样我们就可以在固定的位置插入我们不一样的自定义代码了。
这样我们就可以修改我们之前的设计器组件,让他具备这个功能

<form class = "v-form">
  <label
    class="form-item"
    v-for = '(item,key) in formData.map'
    :key = '"formData"+key'   
  >
    <template v-if = 'item.type == "input"'>
      <span> {{ item.label }} </span>
    	<input :name = "key" v-model = "formData.rel[key]" :placehoder="请输入+‘item.label’">
    	
    </template>
    <template v-if = 'item.type == "select"'>
      select...
     </template>
    <slot :name = "key" ></slot>
  </label>
</form>

这样一来,我们的组件是不是更加灵活了呢, 这样我们就可以使用设计器去生成属于规范组件的部分,而那些不规范的组件,就使用插槽来处理

<v-form :formData = 'formData'> 
	<p slot = "userName"> 我是 userName 表单项 后面的不规范表单  </p>  
</ v-form>

哈哈哈, 是不是觉得手痒痒了呢? 小编在此抛转引玉, 还是希望更多的建议和意见,让我们coder 的工作更加轻松简单!下面贴上我自己封装的 基于移动端 vant-UI 封装的vForm 组件

<template>
  <div class="v-form">
    <div
      class="form-item"
      v-for = '(item,key) in initData.map'
      :key = '"initData"+key'
      :type='item.inputType'
    >
      <template v-if = 'item.type == "input"'>
        <div class="textarea-label" :style="{marginBottom:'16px'}" v-if=" item.inputType == 'textarea'"><van-icon v-if="item.icon" :name="item.icon" :style="{color:'#5399ff'}"/>  {{item.label}}</div>
        <van-field
          :left-icon="(item.icon && item.inputType != 'textarea') ? item.icon : ''" 
          :type='item.inputType || "text"' 
          :rows='item.rows || 1' 
          clickable
          :class="{'align-left': item.inputType == 'textarea'}"
          :label-width="item.labelWidth || (item.inputType == 'textarea' ? '0' :'90px')"
          :label="item.inputType == 'textarea' ? '' : item.label" 
          v-model="initData.rel[key]" 
          :placeholder='item.label?"请输入"+item.label:"请输入内容"'
          >
          <template v-if ='item.append'>
            <p slot="button">{{item.append}}</p>
          </template>
        </van-field>
      </template>
      <template v-if = 'item.type == "date" || item.type == "datetime"'>
        <date-picker :icon='item.icon || ""' :type='item.type' :defaultValue='initData.rel[key]' @getDate='getDate' :name='key' :label='item.label'/>
      </template>
      <template v-if = 'item.type == "area"'>
        <v-area :name = 'key' :defaultValue='initData.rel[key]' :label='item.label' @getArea='getArea'></v-area>
      </template>
      <template v-if = 'item.type == "radioButton"'>
        <v-radio-button :style="{padding:'6px 0'}" v-model="initData.rel[key]" :label='item.label' :list='item.list' :listName='item.listName' :listId='item.listId'></v-radio-button>
      </template>
      <template v-if = 'item.type == "select"'>
        <v-select :icon='item.icon || ""' :label-width="item.labelWidth || '90px'" :defaultValue='initData.rel[key]' :label='item.label' @onSelect="handleSelect" :list='item.list' :listId='item.listId' :listName='item.listName' :name='key'></v-select>
      </template>
      <template v-if = 'item.type == "slider"'>
        <v-rate :label='item.label' v-model="initData.rel[key]"></v-rate>
      </template>
      <template v-if = 'item.type == "upload"'>
        <div class="v-form-upload">
          <p><van-icon v-if="item.icon" :name="item.icon" :style="{color:'#5399ff'}"/>  {{item.label}}:</p>
          <v-upload :fileList='item.fileList' :limit='item.limit' :limitSize='item.limitSize' :action='item.action'/>
        </div>
      </template>
      <slot :name='key'></slot>
    </div>
  </div>
</template>
import datePicker from './v-form/v-date-picker'
import vSelect from './v-form/v-select'
import vUpload from './v-upload'
import vArea from './v-form/v-area'
import vRadioButton from './v-radio-button'
import vRate from './v-rate'
export default {
    components: { datePicker, vSelect, vUpload, vArea, vRadioButton, vRate },
    methods: {
      getDate(param) {
        this.initData.rel[ param.key ] = param.value;
      },
      getArea(param) {
        this.initData.rel[ param.key ] = param.value;
      },
      handleSelect(param) {
        this.initData.rel[ param.key ] = param.value;
      }
    },
    mounted() {
      for (let o in this.initData.map) {
        if(this.initData.map[o].render){
          this.initData.map[o].render()
        }
      }
    },
    props: {
      initData: {
        default() {
          return {
            map:{},
            rel:{}
          }
        }
      }
    },
    data() {
      return {
        value:''
      }
    }
}
<style lang="less">
.v-form{
  .van-cell__title{
    color: #000;
  }
  .van-field__control{
    text-align: right;
    color: #9b9b9b;
  }
  .form-item{
    width: 96%;
    margin: 0 auto;
    margin-top: 14px;
    .date-picker-wrap{
      width: 100vw;
      position: fixed;
      bottom: 0;
      left: 0;
      z-index: 99;
    }
    .van-cell{
      border:1px solid #cacaca;
      border-radius:6px;
    }
    .van-uploader__upload{
      border: 2px dashed @mainColor;
    }
    .van-uploader__upload-icon{
      color: @mainColor;
    }
    .v-form-upload{
      p{
        padding: 10px;
        padding-left: 0; 
      }
    }
    .align-left{
      textarea{
        text-align: left
      }
    }
  }
}
</style>

小伙伴们有什么疑问可以直接加我微信交流

image.png