日常业务中,form表单很常见了,在使用Vue开发时,使用ElementUI作为组件开发使用;除了UI组件提供的通用form组件,如:input、select等等 , 由于业务的不同,不仅仅局限于简单的输入、选择 . 会存在组合操作,需要按照业务逻辑处理输入/选择的值 . 探究各UI组件的实现 , 并将自定义form组件融入到form表单中 , 也很重要.
示例中各框架、组件版本
Vue@2.6
Element@2.14.1
阅读本文你可以了解到:
- ElementUI form表单基本使用 , 表单校验流程.
- v-model 自定义组件 , 并结合element from进行校验.
ElementUI
这是一个基础示例:
elementForm.vue
<template>
<div style="width:40%">
<el-form ref="form" :model="userInfo" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="userInfo.name" placeholder="姓名" maxlength="15"></el-input>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input v-model="userInfo.age" placeholder="年龄" ></el-input>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="userInfo.gender">
<el-radio label="0">男</el-radio>
<el-radio label="1">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="爱好" prop="hobby">
<el-checkbox-group v-model="userInfo.hobby">
<el-checkbox v-for="item in hobbies" :key="item" :label="item" name="hobby"></el-checkbox>
</el-checkbox-group>
<div>
<el-input v-model.trim="hobby" placeholder="自定义" @change="handleAddHooby" size="mini" style="width:140px;"></el-input>
</div>
</el-form-item>
<el-form-item label="生日" prop="birthday">
<el-date-picker v-model="userInfo.birthday"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmitInfo">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data(){
function validateAge(rule,value,callback){
try{
if(value===''){
callback('请输入年龄')
}
if(!value.match(/^\d+$/)){
callback('请输入数字')
}
if(value*1 <0 || value*1 >150){
callback('请输入合理的年龄值')
}
callback()
}catch(err){
console.log(err)
callback('请输入年龄')
}
}
return {
hobby:"",
hobbies:['篮球','读书','游戏','唱歌','跳舞'],
userInfo:{
name:'',
age:'',
gender:'1',
hobby:[],
birthday:'',
},
rules:{
name:[{required:true,message:'请输入姓名'}],
age:[{validator:validateAge,trigger:'change'}]
}
}
},
methods:{
handleAddHooby(){
if(this.hobbies.includes(this.hobby)){
return
}
this.hobbies.push(this.hobby)
this.hobby = ''
},
handleSubmitInfo(){
console.log(this.userInfo)
this.$refs.form.validate((error,errrorInfo)=>{
if(error){
return
}
})
}
}
}
</script>
关注点:
<el-form ref="form" \>
绑定 , 可获取到form表单实例,调用方法,比如:validate 手动校验、resetFields 重置表单 等.<el-form-item prop="name" \>
绑定 , 与form绑定的规则集rules 键值对应 , 校验时获取校验规则.<el-input v-model="userInfo.name" \>
组件的输入控制 , v-model 指令的使用
v-model
实现自定义组件
发现在使用组件el-input
, 在没有使用v-model绑定值时,用户无法输入.
<el-input placeholder="姓名"></el-input>
在查看Element源码部分后,发现是在代码里做了控制.
setNativeInputValue() {
const input = this.$refs.input; // this.$refs.input 源码为 this.getInput();
if (!input) return;
if (input.value === this.nativeInputValue) return;
input.value = this.nativeInputValue;
},
示例中定义的model
prop 同 props 的value属性值, 所以当你在父级使用该组件时, 同时使用了v-model和value ,那么你不会在该组件中获取到props的value值(v-model优先级高,value 被忽略); 只有仅在使用value时, 你在自定义组件中才可以获取到.
custom-input.vue
<template>
<div>
<p>这是一个自定义输入组件</p>
<input ref="input"
v-model="inputValue"
@input="handleInput"
/>
</div>
</template>
<script>
export default {
data(){
return {
inputValue:'',
}
},
model:{ // 定义v-model如何去处理该组件 ,值属性定义、事件定义
prop:"value",
event:"custom"
},
props:{
value:[String,Number], // 由于绑定的属性值与v-model定义的prop一致, 两者选其一, v-model优先, value会被忽略
},
mounted(){
this.inputValue = this.value
},
methods:{
/**
* 处理输入, 如果不使用v-model则 父组件需要监听 cusmo 的事件,并更新 value 的值
* 使用了 v-model, 则不用管了, v-model 根据model 定义的event事件类型, 监听事件, 进行值更新.
**/
handleInput(e){
console.log(this.inputValue)
this.$emit('custom', Math.random()*10);
},
},
}
</script>
自定义组件引用 , v-model 和value 同时绑定, value 会被忽略.
// ...
<custom-input v-model="inputValue" value="admin" />
// ...
data(){
return {
inputValue:'',
}
}
当然 , 自定义分发的事件this.$emit('custom', Math.random()*10);
, 我们也可以在父组件对其进行监听
// ...
<custom-input v-model="inputValue" @custom='handleVModel' />
// ...
data(){
return {
inputValue:'',
}
},
methods:{
handleVModel(val){
console.log("listener - vModel :",val)
}
}
自定义输入组件关注点:
-
model
组件属性设置 , 定义v-model 指令如何处理当前组件:数据名称、事件名称;默认 model 的值
prop - value ; event - input
; 示例中定义了事件event - custom
model:{ prop:"value", event:"custom" }
可以自定义
props\event
名称处理具体的业务. -
组件内数据变化,需要分发事件, 当前定义的组件触发的事件类型
this.$emit('custom', event.target.value);
-
v-model
会忽略所有表单元素绑定的默认值 , 比如:value\checked\selected
, -
v-model
绑定的 model-prop 属性名称 会被传入组件的props ,需定义 props - value 拿到v-model的值
测试 - 定义不同model - prop 查看输入如何绑定 ; props 中的value 可用于初始化组件内部的 input 值;
// ...
model:{ // 定义v-model如何去处理该组件 ,值属性定义、事件定义
prop:"customValue", // 定义不同 的prop
event:"custom"
},
props:{
customValue:[String,Number], // 双向绑定的 prop 值, 需要在该组件props定义
value:[String,Number],
},
watch:{
customValue(val){
console.log(val)
}
},
mthods:{
/**
* 内部的input输入框输入
* 格式化内部输入的数据 , 将格式化的数据给v-model ,那父级绑定的就是给定的值
**/
handleInput(e){
this.$emit('custom', Math.random()*10); // 此处定义的 v-model 响应的值给随机数, 区分和内部input的输入
},
}
// ....
通过改造, 定义组件中不同的model-prop . 可以拿到value值来初始化组件内部的input值. v-model 定义的prop 需要定义在组件的props中, 可通过watch监听值更新打印查看.
思考: 自定义form输入组件中使用v-model时, ,v-model好比一个高阶组件 , 自身内部维护一个model-prop 名称的属性值, 监听model-event的事件名称的事件 . 事件触发更新自身的prop的属性的值. 值更新时,同时调用父级、自身的重新渲染.
el-form
校验原理
当用户输入值或更改值时,是如何检测并触发校验的
通过查看源代码部分, 通过监听value的值,分发事件到父级
watch: {
value(val) {
// this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]); // 转发事件到el-form-item ; dispatch 为内部自定义事件转发函数
}
},
}
再看el-form-item
是怎么处理该事件的 , 在mounted 后, 调用addValidateEvents
, 监听了事件,并调用了 addValidateEvents
方法.
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange); // 监听事件 , 触发对应的回调函数
}
},
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
return;
}
this.validate('change'); // 触发校验 , change 为校验规则中 trigger 的值
},
validate(trigger, callback = noop) {
this.validateDisabled = false;
const rules = this.getFilteredRule(trigger); // 获取校验规则, 绑定在el-form上的rules、el-form-item上的rules、required 合并
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules; // 校验规则合集
const validator = new AsyncValidator(descriptor); // 引用的库 async-validator ; 初始化校验规则 , 实例对象 validator
const model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => { // 调用validat 方法, 校验给定的值model
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null); // el-form 任意表单触发校验后的事件,包括当前属性名称、校验结果、校验信息
});
},
自定义组件结合el-form
进行校验
如果我们自定义的组件需要融入到el-form中,并统一校验规则出处 . 需要在自定义组件中在值发生更改后,分发事件出来.
需要改造我们之前自定义的组件custom-input.vue
, 需要在v-model 值更改时,分发校验事件触发校验.
import emitter from 'element-ui/src/mixins/emitter'; // 分发事件的el-form处理方法
// ...
mixins:[emitter], // 混入的方式 , 加载到当前组件
watch:{
customValue(val){
console.log(val)
this.dispatch('ElFormItem', 'el.form.change', [val]); // 值发生变化时, 向el-form-item分发事件 , 调用组件内部的校验流程
}
},
写完了,来引用到父级表单组件中测试一番. 看效果如何 , 使用如下:
<template>
<div style="width:40%">
<el-form ref="form" :model="userInfo" :rules="rules">
<!-- // ... 省略其他 -->
<el-form-item label="自定义" prop="randomValue">
<custom-input v-model="userInfo.randomValue" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmitInfo">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import CustomInput from './custom-input'
export default {
data(){
// ...
function validateAgeRandomValue(rule,value,callback){ // 对自定义组件的值进行自定义校验规则
try{
if(value===''){
callback('请输入随机数')
}
if(value<3){
callback('太小了')
}
if(value>7){
callback('太大了')
}
callback()
}catch(err){
callback('出错了')
}
}
return {
customInputValue:"",
hobby:"",
hobbies:['篮球','读书','游戏','唱歌','跳舞'],
userInfo:{
name:'',
age:'',
gender:'1',
hobby:[],
birthday:'',
randomValue:'',
},
rules:{
name:[{required:true,message:'请输入姓名'}],
age:[{validator:validateAge,trigger:'change'}],
randomValue:[{required:true,message:'请输入一个随机值'},{validator:validateAgeRandomValue}], // 自定义规则
}
}
},
components:{
CustomInput
},
// ...
}
</script>
测试,搞定! ✅
本来还想继续写v-model
源码实现,看的好累, 东西太多了,看的头大;先不加了 😜