自己手动造轮子封装 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. 组件的灵活性
细心的小伙伴很容易就在使用这个设计器的时候发现很多问题, 比如我们的表单应用如果不是很规则的表单应用呢?
如 图片中所示,我们的表单中多了中间层的按钮 还有 上面内嵌的表单,甚至我们还会有删除等等一些逻辑出现,这样的一个复杂表单,我们刚刚的思路是不是很难解决了呢? 是不是只要有一点不一样,那么我们的设计器组件就要废掉不能用了呢? 这样一来我们的设计器太死板了。
带着这个问题我们继续思考,怎么样才能使我们的组件变得更加灵活呢? 熟悉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>
小伙伴们有什么疑问可以直接加我微信交流