一、目標
ElementUI 中 Form 組件主要有以下 功能 / 模塊:
- Form
- FormItem
- Input
- 表單驗證
在這套組件中,有 3 層嵌套,這里面使用的是 slot 插槽,我們在接下來的代碼中也需要運用到它。
二、組件設計
- e-form 全局校驗,並提供插槽;
- e-form 單一項校驗和顯示錯誤信息,並提供插槽;
- e-input 負責數據的雙向綁定
三、開始
e-input
<template>
<div>
<!-- 1.綁定 value 2.響應 input 事件 -->
<input type="text" :value="valueInInput" @input="handleInput">
</div>
</template>
<script>
export default {
name: 'EInput',
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
valueInInput: this.value // 確保數據流的單向傳遞
}
},
methods: {
handleInput(e) {
this.valueInInput = e.target.value;
this.$emit('input', this.valueInInput); // 此處提交的事件名必須是 ‘input’
// 數據變了,定向通知 formItem 進行校驗
this.dispatch('EFormItem', 'validate', this.valueInInput);
},
dispatch(componentName, eventName, params) { // 查找指定 name 的組件,
let parent = this.$parent || this.$root;
let name = parent.$options.name
while(parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
// 這里,我們不能用 this.$emit 直接派發事件,因為在 FormItem 組件中,Input 組件的位置只是一個插槽,無法做事件監聽,
// 所以此時我們讓 FormItem 自己派發事件,並自己監聽。修改 FormItem 組件,在 created 中監聽該事件。
parent.$emit.apply(parent, [eventName].concat(params));
}
}
}
}
</script>
這里需要注意的是 v-model 綁定的值與 props 傳遞的值的關系,從而能將修改后的值暴露至頂層自定義組件。使用如下:
<template>
<div id="app">
<e-input v-model="initValue"></e-input>
<div>{{ initValue }}</div>
</div>
</template>
<script>
import EInput from './components/Input.vue';
export default {
name: "app",
components: {
EInput
},
data() {
return {
initValue: '223',
};
},
};
</script>
FormItem 的設計
<template>
<div>
<label v-if="label">{{ label }}</label>
<div>
<!-- 拓展槽 -->
<slot></slot>
<!-- 驗證提示信息 -->
<p v-if="validateState === 'error'" class="error">{{ validateMessage }}</p>
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator';
export default {
name: 'EFormItem',
props: {
label: { type: String, default: '' }, // 表單項的名稱
prop: { type: String, default: '' } // 表單項的自定義 prop
},
inject: ['eForm'], // provide/inject,vue 跨層級通信
data() {
return {
validateState: '',
validateMessage: ''
}
},
methods: {
validate() {
return new Promise(resolve => {
const descriptor = {}; // async-validator 建議用法;
descriptor[this.prop] = this.eForm.rules[this.prop];
// 校驗器
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.eForm.model[this.prop];
// 異步校驗
validator.validate(model, errors => {
if (errors) {
this.validateState = 'error';
this.validateMessage = errors[0].message;
resolve(false);
} else {
this.validateState = '';
this.validateMessage = '';
resolve(true);
}
})
})
},
dispatch(componentName, eventName, params) { // 查找上級指定 name 的組件
var parent = this.$parent || this.$root;
var name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
}
},
created() {
this.$on('validate', this.validate); // 'validate' 事件由 e-input 組件通知,在 e-form-item 組件接收到后自行觸發對應方法
},
// 因為我們需要在 Form 組件中校驗所有的 FormItem ,
// 所以當 FormItem 掛載完成后,需要派發一個事件告訴 Form :你可以校驗我了。
mounted() {
// 當 FormItem 中有 prop 屬性的時候才校驗,
// 沒有的時候不校驗。比如提交按鈕就不需要校驗。
if (this.prop) {
this.dispatch('EForm', 'addFiled', this);
}
}
}
</script>
<style scoped>
.error {
color: red;
}
</style>
其中, methods 中的方法均是輔助方法,validate() 是異步校驗的方法。
Form 的設計
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
name: 'EForm',
props: {
model: {
type: Object,
required: true
},
rules: {
type: Object
}
},
provide() { // provide/inject,vue 跨層級通信
return {
eForm: this // form 組件實例, 在其他組件中可以通過 this.xxx 來獲取屬性/方法
}
},
data() {
return {
fileds: [] // 需要校驗的 e-form-item 組件數組
}
},
methods: {
async validate(cb) {
const eachFiledResultArray = this.fileds.map(filed => filed.validate());
const results = await Promise.all(eachFiledResultArray);
let ret = true;
results.forEach(valid => {
if (!valid) {
ret = false;
}
});
cb(ret)
}
},
created() {
// 緩存需要檢驗的組件
this.fileds = [];
this.$on('addFiled', filed => this.fileds.push(filed))
}
}
</script>
