封装组件的基本方法就是通过props和emit进行父子组件的传值和通信。利用插槽、组件等增加组件的可扩展性和复用性。
通用表单组件功能:收集数据、校验数据并提交
需求分析
- 实现KForm
-
指定数据、校验规则
-
- KFormItem
- 执行校验
-
显示错误信息
- KInput
-
维护数据
-
基本表单
封装思路:
- 整个Form表单数据绑定在el-form上::model=“form”,form就是表单的数据对象,使用provide将form数据,rules校验规则传递给后代(provide() 仅限于UI库的编写,平常不用 )。
- 表单里面的每一项是放在el-form-item标签里面,放入我们想渲染出来的组件,如输入框,单选等;
- 每个el-form-item中可以绑定了prop、label、rules等属性,我们可以在配置文件中配置对应属性的值进行绑定;
- 利用 inject 将 form 表单信息注入该组件,对每一项 el-form-item 进行规则校验。
<template>
<div class="content">
<KForm ref="loginForm" :model="userInfo" :rules="rules">
<KFormItem label="用户名" prop="username">
<KInput v-model="userInfo.username" placeholder="请输入用户名"></KInput>
</KFormItem>
<KFormItem label="密码" prop="password">
<KInput v-model="userInfo.password" type="password" placeholder="请输入用户名"></KInput>
</KFormItem>
<!-- 提交按钮 -->
<KFormItem>
<button @click="login">登录</button>
</KFormItem>
</KForm>
</div>
</template>
<script>
import KInput from './test/KInput.vue'
import KFormItem from './test/KFormItem.vue'
import KForm from './test/KForm.vue'
export default {
name: '',
components: {
KInput,
KFormItem,
KForm,
},
data() {
return {
userInfo: {
username: '',
password: '',
},
rules: {
username: [
{
required: true,
message: '请输入用户名名称',
},
],
password: [
{
required: true,
message: '请输入密码',
},
],
},
}
},
created() {},
mounted() {},
methods: {
login() {
// 调用form组件中的表单校验方法
this.$refs['loginForm'].validate(valid => {
if (valid) {
alert('sumbit')
} else {
console.log('error submit!')
return false
}
})
},
},
}
</script>
封装最内层KInput组件:KInput.vue
<template>
<div>
<!-- 自定义组件 双向绑定::value @input -->
<!-- v-bind="$attrs" 展开 $attrs -->
<input :type="type" :value="value" v-bind="$attrs" @input="onInput" />
</div>
</template>
<script>
export default {
name: '',
inheritAttrs: false,
props: {
value: {
type: String,
default: '',
},
type: {
type: String,
default: 'text',
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {
onInput(e) {
// 派发一个 input 事件,实现数据的双向绑定
this.$emit('input', e.target.value)
// 通知父级执行校验
this.$parent.$emit('valid')
},
},
}
</script>
注:像是在 KInput 上绑定了 placeholder="请输入用户名" 属性,但是不想再props中定义,可以使用 v-bind="$attrs" 直接展开 KInput 足尖上绑定的属性来获取 placeholder 该属性。
kformitem.vue
<template>
<div>
<!-- label -->
<label v-if="label">{
{ label }}</label>
<slot></slot>
<!-- 校验信息的显示 -->
<p v-if="error">{
{ error }}</p>
<!-- <p>{
{ form.rules }}</p> -->
</div>
</template>
<script>
import Schema from 'async-validator'
export default {
name: '',
components: {},
inject: ['form'],
props: {
label: {
type: String,
default: '',
},
prop: {
type: String,
default: '',
},
},
data() {
return {
error: '', // error是空说明校验通过
}
},
created() {},
mounted() {
this.$on('valid', () => {
this.validate()
})
},
methods: {
validate() {
// 规则
const rules = this.form.rules[this.prop]
// 当前值
const value = this.form.model[this.prop]
// 校验描述对象
const desc = {
[this.prop]: rules,
}
// 创建 Schema 实例
const schema = new Schema(desc)
return schema.validate({ [this.prop]: value }, errors => {
if (errors) {
this.error = errors[0].message
} else {
this.error = ''
}
})
},
},
}
</script>
预留表单项插槽,借用 async-validator 插件完成表单校验功能,在 kformitem 组件中完成每一项表单校验的方法,但是要完成在提交时对每一个表单进行校验,就需要在 kformitem 上次层 kform 组件中循环遍历每一项的表单校验方法。 因为 schema.validate 的方法返回的实例是一个 promise ,所以可以借助 promise.all 完成表单的校验。
kform.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name: '',
components: {},
provide() {
return {
form: this,
}
},
props: {
model: {
type: Object,
require: true,
default: () => {
return {}
},
},
rules: {
type: Object,
default: () => {
return {}
},
},
},
data() {
return {}
},
created() {},
mounted() {},
methods: {
validate(handle) {
// 获取所有孩子 kformitem
// [resultPromise]
const tasks = this.$children
// 过滤掉没有prop属性的孩子
.filter(items => items.prop)
.map(item => item.validate())
// 统一处理 promise 结果
Promise.all(tasks)
.then(() => handle(true))
.catch(() => handle(false))
},
},
}
</script>
因为 formitem 的子代不一定都是表单项,还有可能是按钮,按钮是没有被传递prop属性的,为了校验的是每一个有校验方法的表单项,需要过滤掉没有 prop 属性的孩子。
问题修正:
对于 input 插件中 $parent 写法问题,如果组件嵌套多层使用,使用 $parent 就不恰当了,看element-ui 源码中封装时使用了 emitter 方法的混入。
mixins: [emitter, Migrating],
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
// this.dispatch 是emitter混入得来的
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
},
}