前两章学习复习了自定义组件所需的两个基础知识点,为自定义组件Form的学习做了基础铺垫。如何自己去写一个自定义组件Form表单我们需要知道的如下:
一、Form表单组件功能需求
- 实现数据的model绑定
- 表单验证规则
- 全局验证
二、分析elementUI结构与技术实现
ui组件一般大多数人就会直接找element,现在我们分析一下elementUI的form表单。 首先,看一下element的官方案例:
<el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<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>
<el-button type="primary" @click="submitForm('ruleForm')">立即创建</el-button>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
<script>
export default {
data() {
return {
ruleForm: {
name: '',
region: '',
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' }
]
}
};
},
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();
}
}
}
</script>
拿element官网的基础案例来讲,有明确的嵌套关系,form在最外层,里面是formItem,最里面是Input。
- form的任务:全局的管理数据模型,校验规则和全局校验
- formItem: 显示标签-label、执行校验和显示校验结果
- Input:收集数据(绑定数据模型、通知formItem执行校验)
三、自定义的Form表单组件逻辑思路
1、环境技术
- vue-cli 3.x
- node.10.x
- vuejs 2.x
2、项目目录结构
index
form
formItem
qInput
3、遇到的问题点
- qinput如何实现数据绑定?
- formItem何时执行校验,校验的数据和规则如何得到?
- qinput已经绑定数据,为什么form还要v-model绑定?
- form 怎么进行全局校验?
4、效果
四、自定义Form表单代码实现
A.首页创建一个qinput.vue,格式如下:
<div>
<!--双绑 :value @input-->
<input :type="type" :value="value" @input="onInput" v-bind="$attrs">
</div>
这里type不能写死,实现双绑需要value @input ,value值从父组件formItem传递进来,@input事件监听,但是什么事情都不需要做,只需要做事件转发。代码如下:
export default {
// 1.双绑
// 2.通知校验
name: 'yqInput',
inheritAttrs: false,
props: {
type: {
type: String,
default: 'text'
},
value: {
type: String,
default: ''
}
},
methods: {
onInput (e) {
//
this.$emit('input', e.target.value)
}
}
}
这里的emit,不去改值,仅派发事件是一个单向数据流的概念,父组件formItem传值过来后,input组件去显示,将来值放生了变化以后,只需要把变化告诉父组件formItem,这也是双绑的策略。
attrs是为了接收父组件传过来的参数,此类参数是props未声明的,原生特性可以用attrs,并且用v-bind绑定,v-bind会展开attrs。副作用是父组件也会继承属性,所以需要设置inheritAttrs: false, 不继承属性。 效果如下:
B.创建一个index.vue,引入input组件,绑定数据,格式如下:
<template>
<div>
<h3>表单</h3>
<hr>
<yq-input v-model="model.username" placeholder="请输入用户名"></yq-input>
{{model}}
</div>
</template>
<script>
import YqInput from './yqInput'
export default {
name: 'index.vue',
components: {
YqInput
},
data () {
return {
model: {
username: 'yq',
password: ''
}
}
}
}
</script>
效果如下:
C.按照最初设计的逻辑布局格式,创建一个formItem.vue,在formItem中,需要有label标题、报错信息,label标题、报错信息需要v-if判断一下不是必须显示的,如下:
<template>
<div>
<!--label-->
<label v-if="label">{{label}}</label>
<slot></slot>
<p v-if="errorMsg">{{errorMsg}}</p>
</div>
</template>
<script>
export default {
name: 'formItem',
props: {
label: {
type: String,
default: ''
},
prop: { // 用于获取指定字段的值和校验规则
type: String,
default: ''
}
},
data () {
return {
errorMsg: ''
}
}
}
</script>
同时,在index中引用FormItem组件,如下:
<template>
<div>
<h3>表单</h3>
<hr>
<form-item label="用户名" prop="username">
<yq-input v-model="model.username" placeholder="请输入用户名"></yq-input>
</form-item>
{{model}}
</div>
</template>
<script>
import YqInput from './yqInput'
import FormItem from './formItem'
export default {
name: 'form.vue',
components: {
YqInput,
FormItem
},
data () {
return {
model: {
username: 'yq',
password: ''
}
}
}
}
</script>
D. 创建最外层Form组件,添加provide(为了子代都可以获取祖代Form的参数),接收传入的model与rules,如下:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'form.vue',
provide () {
return {
form: this
}
},
props: {
model: {
type: Object,
required: true
},
rules: {
type: Object
}
},
data () {
return {
}
}
}
</script>
同时,在index中引用Form组件,绑定model与rules属性,加入登录按钮,添加全局验证的点击事件,如下:
<template>
<div>
<h3>表单</h3>
<hr>
<yq-form :model="model" :rules="rules">
<form-item label="用户名" prop="username">
<yq-input v-model="model.username" placeholder="请输入用户名"></yq-input>
</form-item>
<form-item>
<button @click="onlogin">登录</button>
</form-item>
</yq-form>
</div>
</template>
<script>
import YqInput from './yqInput'
import FormItem from './formItem'
import YqForm from './form'
export default {
name: 'index.vue',
components: {
YqInput,
FormItem,
YqForm
},
data () {
return {
model: {
username: 'yq',
password: ''
},
rules: {
username: [
{required: true, message:'请输入用户名'}
]
}
}
},
methods: {
onlogin () {
// 全局校验
}
}
}
</script>
下面做的验证,按照element整理出来的思路,我们在Form组件中注入值,formItem中做验证validate,首先在fomeItme通过inject(省略之前已写过的代码)拿出Form组件中注入值,如下:
...
export default {
name: 'formItem',
inject: ['form'],
...
fomeItme的子代组件input要通知校验
methods: {
onInput (e) {
// 仅派发事件
this.$emit('input', e.target.value)
// 通知校验
this.$parent.$emit('validate')
}
但是parent会与父组件产生明显的耦合,element的官方用minxins,minxins的补充在文档的最后。
同时fomrItem自己监听,并执行一个validate方法,如下:
mounted () {
this.$on('validate', () => {
this.validate()
})
},
methods: {
validate () {
// 1.获取值和校验方法
const rules = this.form.rules[this.prop]
const value = this.form.model[this.prop]
}
}
校验规则的首先安装一个库:async-validator,做一些校验规则,对异步的支持非常好,element也是用的这个。github.com/tmpfs/async…
npm i -S async-validator
在formItme中导入这个库,
import Schema from 'async-validator'
在方法中创建Schema的示例,如下:
validate () {
// 1.获取值和校验方法
const rules = this.form.rules[this.prop]
const value = this.form.model[this.prop]
// 2.创建Schema实例,格式: {username: rules}
const schema = new Schema({[this.prop]: rules})
// 3.执行校验,校验对象, 回掉函数
return schema.validate({[this.prop]: value}, (errors) => {
if (errors) {
this.errorMsg = errors[0].message
} else {
this.errorMsg = ''
}
})
}
return为了告诉外围组件,执行的结果true or false, validate方法回返回一个promise执行结果, 效果:
这是单项校验,下面把全局校验添加上,那么全局校验在点击的时候使用,element在使用是用了ref并且有一个validate的事件,那么在index组件中加上ref(省略相同的代码),如下:
...
<yq-form :model="model" :rules="rules" ref="loginForm">
...
onlogin () {
// 全局校验
this.$refs['loginForm'].validate(isValid => {
if (isValid) {
alert('可以登录')
} else {
alert('报错')
}
})
}
...
在form组件中添加一个validate的事件,做全局校验,接收一个回调,校验失败还是成功要通过回调函数传出去,而且不是所有的项都需要校验,必须有prop属性的值才能校验,如下改动:
methods: {
validate (cb) {
// 全局校验
// 1.不是所有的项都需要校验,把没有prop属性的过滤掉
// 返回true就留下,执行留下数组中的所有校验方法,执行组件实例的validate方法,用map执行validate方法
// tasks是一个promise的map执行结果数组
const tasks = this.$children.filter(item => item.prop).map(item => item.validate())
// 必须全部通过,Promise.all结果数组全部通过才能执行then
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false))
}
}
效果:
promise:es6.ruanyifeng.com/#docs/promi…
现在基本的form表单的绑定数据和验证都完成了,剩下的就自行美化样式。
补充 minxins修改parent的高耦合性问题:
1.首页新建一个minxins.js的文件,用于递向上寻找组件派发事件,用minxins的混入语法,把方法混入到vue的methods方法中,用于组件的扩展,代码如下:
export default {
methods: {
dispatch (componentName, eventName, params) { // 组件名称,事件名称,参数
var parent = this.$parent || this.$root // 先找父组件,找不到找根
var name = parent.$options.__proto__.name
while (parent && (!name || name !== componentName)) { //判断名字是否与组件名字一样
parent = parent.$parent
if (parent) {
name = parent.$options.componentName
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params))
}
}
}
}
2.在main.js中引入minxins.js的文件
import Mixin from 'components/uiPc/form/mixins'
Vue.mixin(Mixin)
3.修改formItem组件,如下:
onInput (e) {
// 仅派发事件
this.$emit('input', e.target.value)
// 通知校验
// this.$parent.$emit('validate')
this.dispatch('formItem', 'validate', e.target.value)
}