使用 vue 做了不少管理后台方面的业务,都使用了 ui 框架,或iview
,或Element
。用框架很香,但业务写多了也不免想自己试着实现一下框架中的一些组件,于是我选择了Form
组件,没有特别的理由,就是表单组件用的实在很频繁。我看了下Element
的源码,兼顾的很多,代码也比较多,所以就不照抄源码了,只简单来实现一个demo,同时也不写css代码,毕竟不是为了用于生产,只是了解原理。
看效果可以拉到最下面。
明确要实现的效果
Element
中一个最最基本的表单结构应该是这样的:
<el-form :model="form" :rules="rules" ref="loginForm">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username"></el-input>
</el-form-item>
</el-form>
包含三个组件:Form
、FormItem
和Input
。我准备实现的就是这三个部分,并且带表单验证功能。
实现Input
首先来实现一个 input 组件,取个名字就叫做AInput
吧,它的特点是可以使用v-model
进行数据绑定,所以在使用时应该是这样的:
<a-input v-model="username" />
那么问题来了,怎么使得自定义组件上的v-model
命令生效呢?vue文档给出了答案,可参考自定义组件的 v-model。
大致意思是说当自定义组件使用了v-model
命令时,会在组件内部解析为名为value
的prop和名为input
的事件。利用这个规则,可以这样来编写AInput
组件的代码:
<!-- AInput 组件代码 -->
<template>
<div>
<input :value="value" @input="$emit('input',$event.target.value)">
</div>
</template>
<script>
export default {
props: {
value: [String, Number]
}
}
</script>
相信这段代码大家都能看得懂,来稍微捋一下,当在 input 输入框输入时,触发一个自定义事件input
,并将输入值作为参数传入,此时父组件是可以监听input
这个自定义事件的,并可以拿到输入值,而且我们希望父组件可以将拿到的输入值通过 prop 方式传回给子组件,以更新子组件的 input 标签的 value 值。流程是这样的:
流程中的第二步父组件的操作其实就是v-model
起到的作用,所以我们需要按照规定,prop传入值的名字要为value
,注意 prop 也可以传入其他名字的值,只是要绑定到 input 标签 value 上的值必须叫做 "value",而自定义事件名也必须为input
。
简单分析之后,其实也可以了解到v-model
到底做了啥,无非就是取值再传值,当不使用v-model
时,它就是这样用的:
<template>
<div id="app">
<!-- <a-input v-model="username" /> -->
<!-- 此方式等效于使用 v-model -->
<a-input :value="username" @input="handleInput" />
</div>
</template>
<script>
import AInput from './components/Input'
export default {
components: {
AInput
},
data () {
return {
username: ''
}
},
methods: {
handleInput (val) {
this.username = val
}
}
}
</script>
补充:前面说了,一个组件上的 v-model 会把 value
用作 prop 且把 input
用作 event,这是默认情况下的。其实value
和input
这两个名字是可以修改的,可参考:model
实现FormItem
form-item
的作用是包裹input
等具体输入组件,所以我们肯定知道要使用slot
插槽,除此之外,它需要传入两个 prop,一个是label
,作用显而易见是标签文本,另外一个是prop
,它的作用是用来做验证的。按照条件我们实现如下代码:
<!-- FormItem 组件代码 -->
<template>
<div>
<label for="">{{label}}</label>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
label: String,
prop: String
}
}
</script>
不要怀疑,代码就是这么简单,只是还没写关于验证方面的东西,这个我们放到后面再说。现在使用时就可以这样了:
<a-form-item label="用户名" prop="username">
<a-input v-model="username" />
</a-form-item>
实现Form
首先不用考虑的就是Form
里肯定有插槽,然后它需要传入model
,这是表单数据对象,还要传入rules
,这个是表单验证规则。ref
不用多说,可以用来访问组件实例,不用我们实现。所以根据条件,我们实现Form
的代码如下:
<!-- Form 组件代码 -->
<template>
<div>
<form>
<slot></slot>
</form>
</div>
</template>
<script>
export default {
props: {
model: Object,
rules: Object
}
}
</script>
写完之后我都惊呆了,这么简单的么,就这样也能叫做表单组件吗?
我们来回想一下要完成的效果,一是输入时能够使用v-model
进行数据绑定,这个已经实现,二是能够对输入值进行规则校验,这个待完成。所以不用怀疑,如果你不要表单验证功能,那么上面代码已经可以了,无非就是再加上css代码,使得结构美观。现在可以使用表单如下:
<template>
<div id="app">
<a-form :model="form" :rules="rules" ref="form">
<a-form-item label="用户名" prop="username">
<a-input v-model="form.username" />
</a-form-item>
</a-form>
</div>
</template>
<script>
import AInput from './components/Input'
import AFormItem from './components/FormItem'
import AForm from './components/Form'
export default {
components: {
AInput,
AFormItem,
AForm
},
data () {
return {
form: {
username: ''
},
rules: {}
}
}
}
</script>
到这里,已经有内味了,而我们还没用上的几个prop:model
、rules
和prop
全是为了表单验证准备的。接下来就做表单验证功能。
实现表单验证
表单验证功能是放在FormItem
组件里的,所以接下来着重完善这个组件。
添加验证信息的显示位置
首先,警告信息的显示位置,一般是在输入框的下方,当有错误时才显示,无错误时隐藏,所以改造FormItem
代码如下:
<template>
<div>
<label for="">{{label}}</label>
<div>
<slot></slot>
<!-- 添加错误提示信息 -->
<p v-if="errStatus">{{errMessage}}</p>
</div>
</div>
</template>
<script>
export default {
props: {
label: String,
prop: String
},
data () {
return {
errStatus: false, // 错误状态,false or true
errMessag: '' // 错误信息
}
}
}
</script>
使用 async-validator 做验证
Element
中表单验证用到了第三方插件 async-validator,所以这里我也使用这个插件,关于它的使用可以自行查阅文档,这里有一个简单的使用说明:
// 引入插件
import schema from 'async-validator'
// 定义一个descriptor,这个就是我们平时写在 rules 中的规则
var descriptor = {
name: {
type: "string",
required: true,
validator: (rule, value) => value === 'muji',
},
}
// 将 descriptor 传入 schema,得到 validator 实例
var validator = new schema(descriptor)
// 将需要校验的对象和回调传入 validator.validate 方法中
validator.validate({name: "muji"}, (errors, fields) => {
if(errors) {
// validation failed, errors is an array of all errors
// fields is an object keyed by field name with an array of
// errors per field
// 校验未通过的情况,errors 是所有错误的数组
// fields 是一个 object,以字段作为 key 值,该字段对应的错误数组作为 value
// (其实 fields 就是把 errors 按照原对象的 key 值分组)
return handleErrors(errors, fields);
}
// validation passed
// 这里说明校验已通过
})
安装就不演示了,如果已经安装了Element
,也会默认安装async-validator
。
在 FormItem
中获取 rules
和 model
从上面async-validator
插件的使用说明中可以知道,我们需要在 FormItem
中获取 rules
和 model
。那么问题又来了,rules
和 model
是传入到Form
中的,怎么才能在FormItem
中获取Form
中的数据呢?
你可能会想为什么不直接将这两个值传入到FormItem
中呢?试想下真要这样的话,那每一个FormItem
都要重复传入这两个对象,代码一下子臃肿起来,对性能也造成一定影响。所以还是要相信Element
的设计思想,按照现有的来。
在Element
中解决这个问题的方法就是使用provide / inject
。之前我写过一篇专栏 vue组件通信总结,里面介绍了一些 vue 中的通信方式,但是没有写到provide / inject
,为此还有小伙伴在评论区提出来了,没写是因为vue文档中有这样一句话:
provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
那么我想说现在可以用了,因为我在写“组件库”啊,hh。使用它们很简单,我们只需在父组件中用provide
提供一个值,然后在子组件使用inject
就可以拿到父组件提供的值,在我们这个案例中,我们需要在Form
中提供值,在FormItem
中接收,代码如下:
<!-- Form 组件代码 -->
<!-- 添加 provide -->
<template>
<div>
<form>
<slot></slot>
</form>
</div>
</template>
<script>
export default {
// provide 与 data 等选项同级
provide () {
return {
aForm: this // 将 AForm 组件自身提供给下层组件
}
},
props: {
model: Object,
rules: Object
}
}
</script>
再在FormItem
组件中接收:
<!-- FormItem 组件代码 -->
<!-- 添加 inject -->
<template>
<div>
<label for="">{{label}}</label>
<div>
<slot></slot>
<p v-if="errStatus">{{errMessage}}</p>
</div>
</div>
</template>
<script>
export default {
inject: ['aForm'], // 接收 aForm
props: {
label: String,
prop: String
},
data () {
return {
errStatus: false,
errMessag: ''
}
}
}
</script>
现在就可以在 FormItem
中分别使用 this.aForm.model
和 this.aForm.rules
来获取 model
和 rules
了。
完善验证功能
上面铺垫的差不多了,可以来完善最后一步了。首先,我们要考虑下什么时候触发验证,显然是在输入框输入的时候。那么先要改造下 AInput
组件,如下:
<!-- AInput 组件代码 -->
<template>
<div>
<input :value="value" @input="onInput">
</div>
</template>
<script>
export default {
props: {
value: [String, Number]
},
methods: {
onInput (e) {
this.$emit('input', e.target.value)
this.$parent.$emit('validate') // 输入时,使 FormItem 自身触发 validate 事件
}
}
}
</script>
如代码中所示,当输入时,让 FormItem
自身触发 validate 自定义事件,$parent
指当前实例的父实例,此处指的就是FormItem
。此时FormItem
还需监听这个 validate 事件,以进行验证逻辑,所以最终 FormItem
的代码如下:
<!-- FormItem 组件代码 -->
<template>
<div>
<label for="">{{label}}</label>
<div>
<slot></slot>
<p v-if="errStatus">{{errMessage}}</p>
</div>
</div>
</template>
<script>
import Schema from 'async-validator' // 引入插件
export default {
inject: ['aForm'], // 接收 aForm
props: {
label: String,
prop: String
},
data () {
return {
errStatus: false,
errMessag: ''
}
},
mounted () {
// 监听自身触发的 validate 方法,进行验证逻辑
this.$on('validate', this.handleValidate)
},
methods: {
// 定义验证方法
handleValidate () {
// 获取此时正在输入的值
// prop 是通过 props传入的,表示当前要验证的字段,此例中指的就是 username
const value = this.aForm.model[this.prop]
// 获取 username 的验证规则
const rule = this.aForm.rules[this.prop]
// 描述对象
const descriptor = { [this.prop]: rule }
// 传入描述对象,创建验证实例
const validator = new Schema(descriptor)
// 校验
validator.validate({ [this.prop]: value }, errors => {
if (errors) {
this.errMessage = errors[0].message
this.errStatus = true
} else {
this.errMessage = ''
this.errStatus = ''
}
})
}
}
}
</script>
测试效果
简单测试一下完成的效果,下面举一个例子:
<!-- Form 表单组件测试demo -->
<template>
<div id="app">
<a-form :model="form" :rules="rules" ref="form">
<a-form-item label="用户名" prop="username">
<a-input v-model="form.username" />
</a-form-item>
<a-form-item label="密码" prop="password">
<a-input v-model="form.password" />
</a-form-item>
<a-form-item label="确认密码" prop="checkPass">
<a-input v-model="form.checkPass" />
</a-form-item>
</a-form>
</div>
</template>
<script>
import AInput from './components/Input'
import AFormItem from './components/FormItem'
import AForm from './components/Form'
export default {
components: {
AInput,
AFormItem,
AForm
},
data () {
var validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'))
} else {
if (this.form.checkPass !== '') {
this.$refs.form.validateField('checkPass')
}
callback()
}
}
var validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.form.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
}
return {
form: {
username: '',
password: '',
checkPass: ''
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
password: [
{ validator: validatePass, trigger: 'blur' }
],
checkPass: [
{ validator: validatePass2, trigger: 'blur' }
]
}
}
}
}
</script>
效果如图:
没有 css 样式,丑是丑了点,但是想要的功能已经完成了,而且表单验证也支持自定义的校验规则。想要👍。