仿ElementUI實(shí)現(xiàn)一個(gè)Form表單的實(shí)現(xiàn)代碼
使用組件就像流水線上的工人;設(shè)計(jì)組件就像設(shè)計(jì)流水線的人,設(shè)計(jì)好了給工人使用。
完整項(xiàng)目地址:仿 ElementtUI 實(shí)現(xiàn)一個(gè) Form 表單
一. 目標(biāo)
仿 ElementUI 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 Form 表單,主要實(shí)現(xiàn)以下四點(diǎn):
- Form
- FormItem
- Input
- 表單驗(yàn)證
我們先看一下 ElementUI 中 Form 表單的基本用法
<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 的表單中,主要進(jìn)行了 3 層嵌套關(guān)系, Form 是最外面一層, FormItem 是中間一層,最內(nèi)層是 Input 或者 Button 。
二. 創(chuàng)建項(xiàng)目
我們通過(guò) Vue CLI 3.x 創(chuàng)建項(xiàng)目。
使用 vue create e-form 創(chuàng)建一個(gè)目錄。
使用 npm run serve 啟動(dòng)項(xiàng)目。
三. Form 組件設(shè)計(jì)
ElementUI 中的表單叫做 el-form ,我們?cè)O(shè)計(jì)的表單就叫 e-form 。
為了實(shí)現(xiàn) e-form 表單,我們參考 ElementUI 的表單用法,總結(jié)出以下我們需要設(shè)計(jì)的功能。
- e-form 負(fù)責(zé)全局校驗(yàn),并提供插槽;
- e-form-item 負(fù)責(zé)單一項(xiàng)校驗(yàn)及顯示錯(cuò)誤信息,并提供插槽;
- e-input 負(fù)責(zé)數(shù)據(jù)雙向綁定;
1. Input 的設(shè)計(jì)
我們首先觀察一下 ElementUI 中的 Input 組件:
<el-input v-model="ruleForm.name"></el-input>
在上面的代碼中,我們發(fā)現(xiàn) input 標(biāo)簽可以實(shí)現(xiàn)一個(gè)雙向數(shù)據(jù)綁定,而實(shí)現(xiàn)雙向數(shù)據(jù)綁定需要我們?cè)?input 標(biāo)簽上做兩件事。
- 要綁定 value
- 要響應(yīng) input 事件
當(dāng)我們完成這兩件事以后,我們就可以完成一個(gè) v-model 的語(yǔ)法糖了。
我們創(chuàng)建一個(gè) Input.vue 文件:
<template>
<div>
<!-- 1. 綁定 value
2. 響應(yīng) 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>
我們對(duì)上面的代碼做一點(diǎn)解釋:
**解釋一:**既然我們想做一個(gè) Input 組件,那么接收的值必然是父組件傳進(jìn)來(lái)的,并且當(dāng)父組件沒(méi)有傳進(jìn)來(lái)值的時(shí)候,我們可以它一個(gè)默認(rèn)值 "" 。
**解釋二:**我們?cè)谠O(shè)計(jì)組件的時(shí)候,要遵循單向數(shù)據(jù)流的原則:父組件傳進(jìn)來(lái)的值,我們只能用,不能改。那么將父組件傳進(jìn)來(lái)的值進(jìn)行一個(gè)賦值操作,賦值給 Input 組件內(nèi)部的 valueInInput ,如果這個(gè)值發(fā)生變動(dòng),我們就修改內(nèi)部的值 valueInInput 。這樣我們既可以處理數(shù)據(jù)的變動(dòng),又不會(huì)直接修改父組件傳進(jìn)來(lái)的值。
**解釋三:**當(dāng) Input 中的值發(fā)生變動(dòng)時(shí),觸發(fā) @input 事件,此時(shí)我們通過(guò) event.target.value 獲取到變化后的值,將它重新賦值給內(nèi)部的 valueInInput 。
**解釋四:**完成了內(nèi)部賦值之后,我們需要做的就是將變化后的值通知父組件,這里我們用 this.$emit 向上派發(fā)事件。其中第一個(gè)參數(shù)為事件名,第二個(gè)參數(shù)為變化的值。
完成了以上四步,一個(gè)實(shí)現(xiàn)了雙向數(shù)據(jù)綁定的簡(jiǎn)單的 Input 組件就設(shè)計(jì)完成了。此時(shí)我們可以在 App.vue 中引入 Input 組件觀察一下結(jié)果。
<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>
2. FormItem 的設(shè)計(jì)
<el-form-item label="用戶名" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item>
在 ElementUI 的 formItem 中,我們可以看到:
- 需要
label來(lái)顯示名稱; - 需要
prop來(lái)校驗(yàn)當(dāng)前項(xiàng); - 需要給
input或button預(yù)留插槽;
根據(jù)上面的需求,我們可以創(chuàng)建出自己的 formItem ,新建一個(gè) 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>
和上面一樣,我們接著對(duì)上面的代碼進(jìn)行一些解釋:
**解釋一:**根據(jù) ElementUI 中的用法,我們知道 label 是父組件傳來(lái),且當(dāng)傳入時(shí)我們展示,不傳入時(shí)不展示。
解釋二: slot 是一個(gè)預(yù)留的槽位,我們可以在其中放入 input 或其他組件、元素。
解釋三: p 標(biāo)簽是用來(lái)展示錯(cuò)誤信息的,如果驗(yàn)證狀態(tài)為 error 時(shí),就顯示。
此時(shí),我們的 FormItem 組件也可以使用了。同樣,我們?cè)?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>
3. Form 的設(shè)計(jì)
到現(xiàn)在,我們已經(jīng)完成了最內(nèi)部的 input 以及中間層的 FormItem 的設(shè)計(jì),現(xiàn)在我們開(kāi)始設(shè)計(jì)最外層的 Form 組件。
當(dāng)層級(jí)過(guò)多并且組件間需要進(jìn)行數(shù)據(jù)傳遞時(shí),Vue 為我們提供了 provide 和 inject API,方便我們跨層級(jí)傳遞數(shù)據(jù)。
我們舉個(gè)例子來(lái)簡(jiǎn)單實(shí)現(xiàn)一下 provide 和 inject 。在 App.vue 中,我們提供數(shù)據(jù)(provide)。
export default {
name: "app",
provide() {
return {
msg: '哥是最外層提供的數(shù)據(jù)'
}
}
};
</script>
接著,我們?cè)谧顑?nèi)層的 Input.vue 中注入數(shù)據(jù),觀察結(jié)果。
<template>
<div>
<!-- 1、綁定 value
2、響應(yīng) 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>

根據(jù)上圖,我們可以看到無(wú)論跨越多少層級(jí), provide 和 inject 可以非常方便的實(shí)現(xiàn)數(shù)據(jù)的傳遞。
理解了上面的知識(shí)點(diǎn)后,我們可以開(kāi)始設(shè)計(jì) Form 組件了。
<el-form :model="ruleForm" :rules="rules" ref="loginForm"> </el-form>
根據(jù) ElementUI 中表單的用法,我們知道 Form 組件需要實(shí)現(xiàn)以下功能:
- 提供數(shù)據(jù)模型 model;
- 提供校驗(yàn)規(guī)則 rules;
- 提供槽位,里面放我們的 FormItem 等組件;
根據(jù)上面的需求,我們創(chuàng)建一個(gè) 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>
解釋一:該組件需要用戶傳遞進(jìn)來(lái)一個(gè)數(shù)據(jù)模型 model 進(jìn)來(lái),類型為 Object 。 rules 為可傳項(xiàng)。
解釋二:為了讓各個(gè)層級(jí)都能使用 Form 中的數(shù)據(jù),需要依靠 provide 函數(shù)提供數(shù)據(jù)。
解釋三:直接將組件的實(shí)例傳遞下去。
完成了 Form 組件的設(shè)計(jì),我們?cè)?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>

到目前為止,我們的基本功能就已經(jīng)實(shí)現(xiàn)了,除了提交與驗(yàn)證規(guī)則外,所有的組件幾乎與 ElementUI 中的表單一模一樣了。下面我們就開(kāi)始實(shí)現(xiàn)校驗(yàn)功能。
4. 設(shè)計(jì)校驗(yàn)規(guī)則
在上面設(shè)計(jì)的組件中,我們知道校驗(yàn)當(dāng)前項(xiàng)和展示錯(cuò)誤信息的工作是在 FormItem 組件中,但是數(shù)據(jù)的變化是在 Input 組件中,所以 FormItem 和 Input 組件是有數(shù)據(jù)傳遞的。當(dāng) Input 中的數(shù)據(jù)變化時(shí),要告訴 FormItem ,讓 FormItem 進(jìn)行校驗(yàn),并展示錯(cuò)誤。
首先,我們修改一下 Input 組件:
methods: {
handlerInput(event) {
this.valueInInput = event.target.value;
this.$emit("input", this.valueInInput);
// 數(shù)據(jù)變了,定向通知 FormItem 校驗(yàn)
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 直接派發(fā)事件,因?yàn)樵?FormItem 組件中, Input 組件的位置只是一個(gè)插槽,無(wú)法做事件監(jiān)聽(tīng),所以此時(shí)我們讓 FormItem 自己派發(fā)事件,并自己監(jiān)聽(tīng)。修改 FormItem 組件,在 created 中監(jiān)聽(tīng)該事件。
created() {
this.$on('validate', this.validate);
}
當(dāng) Input 組件中的數(shù)據(jù)變化時(shí), FormItem 組件監(jiān)聽(tīng)到 validate 事件后,執(zhí)行 validate 函數(shù)。
下面,我們就要處理我們的 validate 函數(shù)了。而在 ElementUI 中,驗(yàn)證用到了一個(gè)底層庫(kù)async-validator,我們可以通過(guò) npm 安裝這個(gè)包。
npm i async-validator
async-validator 是一個(gè)可以對(duì)數(shù)據(jù)進(jìn)行異步校驗(yàn)的庫(kù),具體的用法可以參考上面的鏈接。我們通過(guò)這個(gè)庫(kù)來(lái)完成我們的 validate 函數(shù)。繼續(xù)看 FormItem.vue 這個(gè)文件:
<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];
// 校驗(yàn)器
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.eForm.model[this.prop];
// 異步校驗(yàn)
validator.validate(model, errors => {
if (errors) {
this.validateState = "error";
this.validateMessage = errors[0].message;
resolve(false);
} else {
this.validateState = "";
this.validateMessage = "";
resolve(true);
}
});
});
},
// 查找上級(jí)指定名稱的組件
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>
我們對(duì)上面的代碼做一個(gè)解釋。
解釋一:注入 Form 組件提供的數(shù)據(jù) - Form 組件的實(shí)例,下面就可以使用 this.eForm.xxx 來(lái)使用 Form 中的數(shù)據(jù)了。
解釋二:因?yàn)槲覀冃枰?Form 組件中校驗(yàn)所有的 FormItem ,所以當(dāng) FormItem 掛載完成后,需要派發(fā)一個(gè)事件告訴 Form :你可以校驗(yàn)我了。
解釋三:當(dāng) FormItem 中有 prop 屬性的時(shí)候才校驗(yàn),沒(méi)有的時(shí)候不校驗(yàn)。比如提交按鈕就不需要校驗(yàn)。
<e-form-item> <input type="submit" @click="submitForm()" value="提交"> </e-form-item>
**解釋四:**返回一個(gè) promise 對(duì)象,批量處理所有異步校驗(yàn)的結(jié)果。
解釋五: descriptor 對(duì)象是 async-validator 的用法,采用鍵值對(duì)的形式,用來(lái)檢查當(dāng)前項(xiàng)。比如:
// 檢查當(dāng)前項(xiàng)
// async-validator 給出的例子
name: {
type: "string",
required: true,
validator: (rule, value) => value === 'muji',
}
FormItem 中檢查當(dāng)前項(xiàng)完成了,現(xiàn)在我們需要處理一下 Form 組件中的全局校驗(yàn)。表單提交時(shí),需要對(duì) form 進(jìn)行一個(gè)全局校驗(yàn)。大致的思路是:循環(huán)遍歷表單中的所有派發(fā)上來(lái)的 FormItem ,讓每一個(gè) FormItem 執(zhí)行自己的校驗(yàn)函數(shù),如果有一個(gè)為 false ,則校驗(yàn)不通過(guò);否則,校驗(yàn)通過(guò)。我們通過(guò)代碼實(shí)現(xiàn)一下:
<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 緩存需要校驗(yàn)的表單項(xiàng),因?yàn)槲覀冊(cè)?FormItem 中派發(fā)了事件。只有需要校驗(yàn)的 FormItem 會(huì)被派發(fā)到這里,而且都會(huì)保存在數(shù)組中。
if (this.prop) {
this.dispatch('EForm', 'addFiled', this);
}
解釋二:當(dāng)點(diǎn)擊提交按鈕時(shí),會(huì)觸發(fā)這個(gè)事件。
解釋三:遍歷所有被添加到 fileds 中的 FormItem 項(xiàng),讓每一項(xiàng)單獨(dú)去驗(yàn)證,會(huì)返回 Promise 的 true 或 false 。將所有的結(jié)果,放在一個(gè)數(shù)組 eachFiledResultArray 中。
解釋四:獲取所有的結(jié)果,統(tǒng)一進(jìn)行處理,其中有一個(gè)結(jié)果為 false ,驗(yàn)證就不能通過(guò)。
至此,一個(gè)最簡(jiǎn)化版本的仿 ElementUI 的表單就實(shí)現(xiàn)了。
四. 總結(jié)
當(dāng)然上面的代碼還有很多可以優(yōu)化的地方,比如說(shuō) dispatch 函數(shù),我們可以寫一遍,使用的時(shí)候用 mixin 導(dǎo)入。由于篇幅關(guān)系,這里就不做處理了。
通過(guò)這次實(shí)現(xiàn),我們首先總結(jié)一下其中所涉及的知識(shí)點(diǎn)。
- 父組件傳遞給子組件用 props
- 子組件派發(fā)事件,用 $emit
- 跨層級(jí)數(shù)據(jù)交互,用 provide 和 inject
- 用 slot 可以預(yù)留插槽
其次是一些思想:
- 單項(xiàng)數(shù)據(jù)流:父組件傳遞給子組件的值,子組件內(nèi)部只能用,不能修改。
- 組件內(nèi)部的 name 屬性,可以通過(guò) this.$parent.$options.name 查找。
- 想要批量處理很多異步的結(jié)果,可以用 promise 對(duì)象。
最后,文章會(huì)首先發(fā)布在我的 Github ,以及公眾號(hào)上,歡迎關(guān)注,歡迎 star。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
ElementPlus el-message-box樣式錯(cuò)位問(wèn)題及解決
這篇文章主要介紹了ElementPlus el-message-box樣式錯(cuò)位問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
vue3中使用highlight.js實(shí)現(xiàn)代碼高亮顯示的代碼示例
代碼高亮是在網(wǎng)頁(yè)開(kāi)發(fā)中常見(jiàn)的需求之一,它可以使代碼在頁(yè)面上以不同的顏色或樣式進(jìn)行突出顯示提高可讀性,這篇文章主要介紹了vue3中使用highlight.js實(shí)現(xiàn)代碼高亮顯示的相關(guān)資料,需要的朋友可以參考下2025-04-04
el-input寬度跟隨輸入內(nèi)容自適應(yīng)的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于el-input寬度跟隨輸入內(nèi)容自適應(yīng)的實(shí)現(xiàn)方法,我們?cè)賹?shí)際應(yīng)用中可能需要input文本框能夠根據(jù)輸入字符的所占據(jù)的寬度自動(dòng)調(diào)節(jié)尺寸,需要的朋友可以參考下2023-08-08
elementui中的el-cascader級(jí)聯(lián)選擇器的實(shí)踐
本文主要介紹了elementui中的el-cascader級(jí)聯(lián)選擇器的實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
vue如何實(shí)現(xiàn)清空this.$route.query的值
這篇文章主要介紹了vue如何實(shí)現(xiàn)清空this.$route.query的值,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
Vue $attrs & inheritAttr實(shí)現(xiàn)button禁用效果案例
這篇文章主要介紹了Vue $attrs & inheritAttr實(shí)現(xiàn)button禁用效果案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
vue點(diǎn)擊按鈕實(shí)現(xiàn)讓頁(yè)面的某一個(gè)元素全屏展示
這篇文章主要介紹了vue點(diǎn)擊按鈕實(shí)現(xiàn)讓頁(yè)面的某一個(gè)元素全屏展示,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10

