使用組件就像流水線上的工人;設計組件就像設計流水線的人,設計好了給工人使用。css
完整項目地址:仿 ElmentUI 實現一個 Form 表單vue
仿 ElementUI 實現一個簡單的 Form 表單,主要實現如下四點:git
咱們先看一下 ElementUI 中 Form 表單的基本用法github
<el-form :model="ruleForm" :rules="rules" ref="loginForm">
<el-form-item label="用戶名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="密碼" prop="pwd">
<el-input v-model="ruleForm.pwd"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')">登陸</el-button>
</el-form-item>
</el-form>
複製代碼
在 ElementUI 的表單中,主要進行了 3 層嵌套關係,Form
是最外面一層,FormItem
是中間一層,最內層是 Input
或者 Button
。shell
咱們經過 Vue CLI 3.x
建立項目。npm
使用 vue create e-form
建立一個目錄。數組
使用 npm run serve
啓動項目。promise
ElementUI 中的表單叫作 el-form
,咱們設計的表單就叫 e-form
。緩存
爲了實現 e-form
表單,咱們參考 ElementUI 的表單用法,總結出如下咱們須要設計的功能。app
e-form
負責全局校驗,並提供插槽;e-form-item
負責單一項校驗及顯示錯誤信息,並提供插槽;e-input
負責數據雙向綁定;咱們首先觀察一下 ElementUI 中的 Input
組件:
<el-input v-model="ruleForm.name"></el-input>
複製代碼
在上面的代碼中,咱們發現 input
標籤能夠實現一個雙向數據綁定,而實現雙向數據綁定須要咱們在 input
標籤上作兩件事。
當咱們完成這兩件事之後,咱們就能夠完成一個 v-model
的語法糖了。
咱們建立一個 Input.vue 文件:
<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(event) {
this.valueInInput = event.target.value; // 解釋三
this.$emit('input', this.valueInInput); // 解釋四
}
},
};
</script>
複製代碼
咱們對上面的代碼作一點解釋:
**解釋一:**既然咱們想作一個 Input
組件,那麼接收的值必然是父組件傳進來的,而且當父組件沒有傳進來值的時候,咱們能夠它一個默認值 ""
。
**解釋二:**咱們在設計組件的時候,要遵循單向數據流的原則:父組件傳進來的值,咱們只能用,不能改。那麼將父組件傳進來的值進行一個賦值操做,賦值給 Input
組件內部的 valueInInput
,若是這個值發生變更,咱們就修改內部的值 valueInInput
。這樣咱們既能夠處理數據的變更,又不會直接修改父組件傳進來的值。
**解釋三:**當 Input
中的值發生變更時,觸發 @input
事件,此時咱們經過 event.target.value
獲取到變化後的值,將它從新賦值給內部的 valueInInput
。
**解釋四:**完成了內部賦值以後,咱們須要作的就是將變化後的值通知父組件,這裏咱們用 this.$emit
向上派發事件。其中第一個參數爲事件名,第二個參數爲變化的值。
完成了以上四步,一個實現了雙向數據綁定的簡單的 Input
組件就設計完成了。此時咱們能夠在 App.vue 中引入 Input
組件觀察一下結果。
<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>
複製代碼
<el-form-item label="用戶名" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
複製代碼
在 ElementUI 的 formItem
中,咱們能夠看到:
label
來顯示名稱;prop
來校驗當前項;input
或 button
預留插槽;根據上面的需求,咱們能夠建立出本身的 formItem
,新建一個 FormItem.vue 文件 。
<template>
<div>
<!-- 解釋一 -->
<label v-if="label">{{ label }}</label>
<div>
<!-- 解釋二 -->
<slot></slot>
<!-- 解釋三 -->
<p v-if="validateState === 'error'" class="error">{{ validateMessage }}</p>
</div>
</div>
</template>
<script>
export default {
name: "EFormItem",
props: {
label: { type: String, default: '' },
prop: { type: String, default: '' }
},
data() {
return {
validateState: '',
validateMessage: ''
}
},
}
</script>
<style scoped>
.error {
color: red;
}
</style>
複製代碼
和上面同樣,咱們接着對上面的代碼進行一些解釋:
**解釋一:**根據 ElementUI 中的用法,咱們知道 label
是父組件傳來,且當傳入時咱們展現,不傳入時不展現。
解釋二: slot
是一個預留的槽位,咱們能夠在其中放入 input
或其餘組件、元素。
解釋三: p
標籤是用來展現錯誤信息的,若是驗證狀態爲 error
時,就顯示。
此時,咱們的 FormItem
組件也可使用了。一樣,咱們在 App.vue 中引入該組件。
<template>
<div id="app">
<e-form-item label="用戶名" prop="name">
<e-input v-model="ruleForm.name"></e-input>
</e-form-item>
<e-form-item label="密碼" prop="pwd">
<e-input v-model="ruleForm.pwd"></e-input>
</e-form-item>
<div>
{{ ruleForm }}
</div>
</div>
</template>
<script>
import EInput from './components/Input.vue';
import EFormItem from './components/FormItem.vue';
export default {
name: "app",
components: {
EInput,
EFormItem
},
data() {
return {
ruleForm: {
name: '',
pwd: '',
},
};
},
};
</script>
複製代碼
到如今,咱們已經完成了最內部的 input
以及中間層的 FormItem
的設計,如今咱們開始設計最外層的 Form
組件。
當層級過多而且組件間須要進行數據傳遞時,Vue 爲咱們提供了 provide
和 inject
API,方便咱們跨層級傳遞數據。
咱們舉個例子來簡單實現一下 provide
和 inject
。在 App.vue 中,咱們提供數據(provide)。
export default {
name: "app",
provide() {
return {
msg: '哥是最外層提供的數據'
}
}
};
</script>
複製代碼
接着,咱們在最內層的 Input.vue 中注入數據,觀察結果。
<template>
<div>
<!-- 一、綁定 value
二、響應 input 事件-->
<input type="text" :value="valueInInput" @input="handleInput">
<div>{{ msg }}</div>
</div>
</template>
<script>
export default {
name: "EInput",
inject: [ 'msg' ],
props: {
value: {
type: String,
default: '',
}
},
data() {
return {
valueInInput: this.value
};
},
methods: {
handleInput(event) {
this.valueInInput = event.target.value;
this.$emit('input', this.valueInInput);
}
},
};
</script>
複製代碼
根據上圖,咱們能夠看到不管跨越多少層級,provide
和 inject
能夠很是方便的實現數據的傳遞。
理解了上面的知識點後,咱們能夠開始設計 Form
組件了。
<el-form :model="ruleForm" :rules="rules" ref="loginForm">
</el-form>
複製代碼
根據 ElementUI 中表單的用法,咱們知道 Form
組件須要實現如下功能:
FormItem
等組件;根據上面的需求,咱們建立一個 Form.vue 組件:
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
name: 'EForm',
props: { // 解釋一
model: {
type: Object,
required: true
},
rules: {
type: Object
}
},
provide() { // 解釋二
return {
eForm: this // 解釋三
}
}
}
</script>
複製代碼
解釋一: 該組件須要用戶傳遞進來一個數據模型 model
進來,類型爲 Object
。rules
爲可傳項。
解釋二: 爲了讓各個層級都能使用 Form
中的數據,須要依靠 provide
函數提供數據。
解釋三:直接將組件的實例傳遞下去。
完成了 Form
組件的設計,咱們在 App.vue 中使用一下:
<template>
<div id="app">
<e-form :model="ruleForm" :rules="rules">
<e-form-item label="用戶名" prop="name">
<e-input v-model="ruleForm.name"></e-input>
</e-form-item>
<e-form-item label="密碼" prop="pwd">
<e-input v-model="ruleForm.pwd"></e-input>
</e-form-item>
<e-form-item>
<button>提交</button>
</e-form-item>
</e-form>
</div>
</template>
<script>
import EInput from './components/Input.vue';
import EFormItem from './components/FormItem.vue';
import EForm from "./components/Form";
export default {
name: "app",
components: {
EInput,
EFormItem,
EForm
},
data() {
return {
ruleForm: {
name: '',
pwd: '',
},
rules: {
name: [{ required: true }],
pwd: [{ required: true }]
},
};
},
};
</script>
複製代碼
到目前爲止,咱們的基本功能就已經實現了,除了提交與驗證規則外,全部的組件幾乎與 ElementUI 中的表單如出一轍了。下面咱們就開始實現校驗功能。
在上面設計的組件中,咱們知道校驗當前項和展現錯誤信息的工做是在 FormItem
組件中,可是數據的變化是在 Input
組件中,因此 FormItem
和 Input
組件是有數據傳遞的。當 Input
中的數據變化時,要告訴 FormItem
,讓 FormItem
進行校驗,並展現錯誤。
首先,咱們修改一下 Input
組件:
methods: {
handlerInput(event) {
this.valueInInput = event.target.value;
this.$emit("input", this.valueInInput);
// 數據變了,定向通知 FormItem 校驗
this.dispatch('EFormItem', 'validate', this.valueInput);
},
// 查找指定 name 的組件,
dispatch(componentName, eventName, params) {
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));
}
}
}
複製代碼
這裏,咱們不能用 this.$emit
直接派發事件,由於在 FormItem
組件中,Input
組件的位置只是一個插槽,沒法作事件監聽,因此此時咱們讓 FormItem
本身派發事件,並本身監聽。修改 FormItem
組件,在 created
中監聽該事件。
created() {
this.$on('validate', this.validate);
}
複製代碼
當 Input
組件中的數據變化時,FormItem
組件監聽到 validate
事件後,執行 validate
函數。
下面,咱們就要處理咱們的 validate
函數了。而在 ElementUI 中,驗證用到了一個底層庫 async-validator,咱們能夠經過 npm
安裝這個包。
npm i async-validator
複製代碼
async-validator
是一個能夠對數據進行異步校驗的庫,具體的用法能夠參考上面的連接。咱們經過這個庫來完成咱們的 validate
函數。繼續看 FormItem.vue 這個文件:
<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: '' }
},
inject: ["eForm"], // 解釋一
created() {
this.$on("validate", this.validate);
},
mounted() { // 解釋二
if (this.prop) { // 解釋三
this.dispatch('EForm', 'addFiled', this);
}
},
data() {
return {
validateMessage: "",
validateState: ""
};
},
methods: {
validate() {
// 解釋四
return new Promise(resolve => {
// 解釋五
const descriptor = {
// name: this.form.rules.name =>
// name: [ { require: true }, { ... } ]
};
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) {
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));
}
}
}
};
</script>
<style scoped>
.error {
color: red;
}
</style>
複製代碼
咱們對上面的代碼作一個解釋。
解釋一: 注入 Form
組件提供的數據 - Form
組件的實例,下面就可使用 this.eForm.xxx
來使用 Form
中的數據了。
解釋二: 由於咱們須要在 Form
組件中校驗全部的 FormItem
,因此當 FormItem
掛載完成後,須要派發一個事件告訴 Form
:你能夠校驗我了。
解釋三: 當 FormItem
中有 prop
屬性的時候才校驗,沒有的時候不校驗。好比提交按鈕就不須要校驗。
<e-form-item>
<input type="submit" @click="submitForm()" value="提交">
</e-form-item>
複製代碼
**解釋四:**返回一個 promise 對象,批量處理全部異步校驗的結果。
解釋五: descriptor
對象是 async-validator
的用法,採用鍵值對的形式,用來檢查當前項。好比:
// 檢查當前項
// async-validator 給出的例子
name: {
type: "string",
required: true,
validator: (rule, value) => value === 'muji',
}
複製代碼
FormItem
中檢查當前項完成了,如今咱們須要處理一下 Form
組件中的全局校驗。表單提交時,須要對 form
進行一個全局校驗。大體的思路是:循環遍歷表單中的全部派發上來的 FormItem
,讓每個 FormItem
執行本身的校驗函數,若是有一個爲 false
,則校驗不經過;不然,校驗經過。咱們經過代碼實現一下:
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
props: {
model: { type: Object, required: true },
rules: { type: Object }
},
provide() {
return {
eForm: this, // provide this component's instance
}
},
data() {
return {
fileds: [],
}
},
created() {
// 解釋一
this.fileds = [];
this.$on('addFiled', filed => this.fileds.push(filed));
},
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);
}
},
}
</script>
<style lang="scss" scoped>
</style>
複製代碼
解釋一: 用 fileds
緩存須要校驗的表單項,由於咱們在 FormItem
中派發了事件。只有須要校驗的 FormItem
會被派發到這裏,並且都會保存在數組中。
if (this.prop) {
this.dispatch('EForm', 'addFiled', this);
}
複製代碼
解釋二: 當點擊提交按鈕時,會觸發這個事件。
解釋三: 遍歷全部被添加到 fileds
中的 FormItem
項,讓每一項單獨去驗證,會返回 Promise 的 true
或 false
。將全部的結果,放在一個數組 eachFiledResultArray
中。
解釋四: 獲取全部的結果,統一進行處理,其中有一個結果爲 false
,驗證就不能經過。
至此,一個最簡化版本的仿 ElementUI 的表單就實現了。
固然上面的代碼還有不少能夠優化的地方,好比說 dispatch
函數,咱們能夠寫一遍,使用的時候用 mixin
導入。因爲篇幅關係,這裏就不作處理了。
經過此次實現,咱們首先總結一下其中所涉及的知識點。
props
$emit
provide
和 inject
slot
能夠預留插槽其次是一些思想:
name
屬性,能夠經過 this.$parent.$options.name
查找。promise
對象。若是文章中錯誤或表述不嚴謹的地方,歡迎指正。
最後,文章會首先發布在個人 Github ,以及公衆號上,歡迎關注,歡迎 star。