仿ElementUI實(shí)現(xiàn)一個Form表單的實(shí)現(xiàn)代碼
使用組件就像流水線上的工人;設(shè)計組件就像設(shè)計流水線的人,設(shè)計好了給工人使用。
完整項目地址:仿 ElementtUI 實(shí)現(xiàn)一個 Form 表單
一. 目標(biāo)
仿 ElementUI 實(shí)現(xià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)建項目
我們通過 Vue CLI 3.x
創(chuàng)建項目。
使用 vue create e-form
創(chuàng)建一個目錄。
使用 npm run serve
啟動項目。
三. Form 組件設(shè)計
ElementUI 中的表單叫做 el-form
,我們設(shè)計的表單就叫 e-form
。
為了實(shí)現(xiàn) e-form
表單,我們參考 ElementUI 的表單用法,總結(jié)出以下我們需要設(shè)計的功能。
- e-form 負(fù)責(zé)全局校驗(yàn),并提供插槽;
- e-form-item 負(fù)責(zé)單一項校驗(yàn)及顯示錯誤信息,并提供插槽;
- e-input 負(fù)責(zé)數(shù)據(jù)雙向綁定;
1. Input 的設(shè)計
我們首先觀察一下 ElementUI 中的 Input
組件:
<el-input v-model="ruleForm.name"></el-input>
在上面的代碼中,我們發(fā)現(xiàn) input
標(biāo)簽可以實(shí)現(xiàn)一個雙向數(shù)據(jù)綁定,而實(shí)現(xiàn)雙向數(shù)據(jù)綁定需要我們在 input
標(biāo)簽上做兩件事。
- 要綁定 value
- 要響應(yīng) input 事件
當(dāng)我們完成這兩件事以后,我們就可以完成一個 v-model
的語法糖了。
我們創(chuàng)建一個 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>
我們對上面的代碼做一點(diǎn)解釋:
**解釋一:**既然我們想做一個 Input
組件,那么接收的值必然是父組件傳進(jìn)來的,并且當(dāng)父組件沒有傳進(jìn)來值的時候,我們可以它一個默認(rèn)值 ""
。
**解釋二:**我們在設(shè)計組件的時候,要遵循單向數(shù)據(jù)流的原則:父組件傳進(jìn)來的值,我們只能用,不能改。那么將父組件傳進(jìn)來的值進(jìn)行一個賦值操作,賦值給 Input
組件內(nèi)部的 valueInInput
,如果這個值發(fā)生變動,我們就修改內(nèi)部的值 valueInInput
。這樣我們既可以處理數(shù)據(jù)的變動,又不會直接修改父組件傳進(jìn)來的值。
**解釋三:**當(dāng) Input
中的值發(fā)生變動時,觸發(fā) @input
事件,此時我們通過 event.target.value
獲取到變化后的值,將它重新賦值給內(nèi)部的 valueInInput
。
**解釋四:**完成了內(nèi)部賦值之后,我們需要做的就是將變化后的值通知父組件,這里我們用 this.$emit
向上派發(fā)事件。其中第一個參數(shù)為事件名,第二個參數(shù)為變化的值。
完成了以上四步,一個實(shí)現(xiàn)了雙向數(shù)據(jù)綁定的簡單的 Input
組件就設(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è)計
<el-form-item label="用戶名" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item>
在 ElementUI 的 formItem
中,我們可以看到:
- 需要
label
來顯示名稱; - 需要
prop
來校驗(yàn)當(dāng)前項; - 需要給
input
或button
預(yù)留插槽;
根據(jù)上面的需求,我們可以創(chuàng)建出自己的 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>
和上面一樣,我們接著對上面的代碼進(jìn)行一些解釋:
**解釋一:**根據(jù) ElementUI 中的用法,我們知道 label
是父組件傳來,且當(dāng)傳入時我們展示,不傳入時不展示。
解釋二: slot
是一個預(yù)留的槽位,我們可以在其中放入 input
或其他組件、元素。
解釋三: p
標(biāo)簽是用來展示錯誤信息的,如果驗(yàn)證狀態(tài)為 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>
3. Form 的設(shè)計
到現(xiàn)在,我們已經(jīng)完成了最內(nèi)部的 input
以及中間層的 FormItem
的設(shè)計,現(xiàn)在我們開始設(shè)計最外層的 Form
組件。
當(dāng)層級過多并且組件間需要進(jìn)行數(shù)據(jù)傳遞時,Vue 為我們提供了 provide
和 inject
API,方便我們跨層級傳遞數(shù)據(jù)。
我們舉個例子來簡單實(shí)現(xiàn)一下 provide
和 inject
。在 App.vue 中,我們提供數(shù)據(jù)(provide)。
export default { name: "app", provide() { return { msg: '哥是最外層提供的數(shù)據(jù)' } } }; </script>
接著,我們在最內(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ù)上圖,我們可以看到無論跨越多少層級, provide
和 inject
可以非常方便的實(shí)現(xiàn)數(shù)據(jù)的傳遞。
理解了上面的知識點(diǎn)后,我們可以開始設(shè)計 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)建一個 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)來一個數(shù)據(jù)模型 model
進(jìn)來,類型為 Object
。 rules
為可傳項。
解釋二:為了讓各個層級都能使用 Form
中的數(shù)據(jù),需要依靠 provide
函數(shù)提供數(shù)據(jù)。
解釋三:直接將組件的實(shí)例傳遞下去。
完成了 Form
組件的設(shè)計,我們在 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 中的表單一模一樣了。下面我們就開始實(shí)現(xiàn)校驗(yàn)功能。
4. 設(shè)計校驗(yàn)規(guī)則
在上面設(shè)計的組件中,我們知道校驗(yàn)當(dāng)前項和展示錯誤信息的工作是在 FormItem
組件中,但是數(shù)據(jù)的變化是在 Input
組件中,所以 FormItem
和 Input
組件是有數(shù)據(jù)傳遞的。當(dāng) Input
中的數(shù)據(jù)變化時,要告訴 FormItem
,讓 FormItem
進(jìn)行校驗(yàn),并展示錯誤。
首先,我們修改一下 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
組件的位置只是一個插槽,無法做事件監(jiān)聽,所以此時我們讓 FormItem
自己派發(fā)事件,并自己監(jiān)聽。修改 FormItem
組件,在 created
中監(jiān)聽該事件。
created() { this.$on('validate', this.validate); }
當(dāng) Input
組件中的數(shù)據(jù)變化時, FormItem
組件監(jiān)聽到 validate
事件后,執(zhí)行 validate
函數(shù)。
下面,我們就要處理我們的 validate
函數(shù)了。而在 ElementUI 中,驗(yàn)證用到了一個底層庫async-validator,我們可以通過 npm
安裝這個包。
npm i async-validator
async-validator
是一個可以對數(shù)據(jù)進(jìn)行異步校驗(yàn)的庫,具體的用法可以參考上面的鏈接。我們通過這個庫來完成我們的 validate
函數(shù)。繼續(xù)看 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]; // 校驗(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); } }); }); }, // 查找上級指定名稱的組件 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
組件提供的數(shù)據(jù) - Form
組件的實(shí)例,下面就可以使用 this.eForm.xxx
來使用 Form
中的數(shù)據(jù)了。
解釋二:因?yàn)槲覀冃枰?Form
組件中校驗(yàn)所有的 FormItem
,所以當(dāng) FormItem
掛載完成后,需要派發(fā)一個事件告訴 Form
:你可以校驗(yàn)我了。
解釋三:當(dāng) FormItem
中有 prop
屬性的時候才校驗(yàn),沒有的時候不校驗(yàn)。比如提交按鈕就不需要校驗(yàn)。
<e-form-item> <input type="submit" @click="submitForm()" value="提交"> </e-form-item>
**解釋四:**返回一個 promise 對象,批量處理所有異步校驗(yàn)的結(jié)果。
解釋五: descriptor
對象是 async-validator
的用法,采用鍵值對的形式,用來檢查當(dāng)前項。比如:
// 檢查當(dāng)前項 // async-validator 給出的例子 name: { type: "string", required: true, validator: (rule, value) => value === 'muji', }
FormItem
中檢查當(dāng)前項完成了,現(xiàn)在我們需要處理一下 Form
組件中的全局校驗(yàn)。表單提交時,需要對 form
進(jìn)行一個全局校驗(yàn)。大致的思路是:循環(huán)遍歷表單中的所有派發(fā)上來的 FormItem
,讓每一個 FormItem
執(zhí)行自己的校驗(yàn)函數(shù),如果有一個為 false
,則校驗(yàn)不通過;否則,校驗(yàn)通過。我們通過代碼實(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)的表單項,因?yàn)槲覀冊?FormItem
中派發(fā)了事件。只有需要校驗(yàn)的 FormItem
會被派發(fā)到這里,而且都會保存在數(shù)組中。
if (this.prop) { this.dispatch('EForm', 'addFiled', this); }
解釋二:當(dāng)點(diǎn)擊提交按鈕時,會觸發(fā)這個事件。
解釋三:遍歷所有被添加到 fileds
中的 FormItem
項,讓每一項單獨(dú)去驗(yàn)證,會返回 Promise 的 true
或 false
。將所有的結(jié)果,放在一個數(shù)組 eachFiledResultArray
中。
解釋四:獲取所有的結(jié)果,統(tǒng)一進(jìn)行處理,其中有一個結(jié)果為 false
,驗(yàn)證就不能通過。
至此,一個最簡化版本的仿 ElementUI 的表單就實(shí)現(xiàn)了。
四. 總結(jié)
當(dāng)然上面的代碼還有很多可以優(yōu)化的地方,比如說 dispatch
函數(shù),我們可以寫一遍,使用的時候用 mixin
導(dǎo)入。由于篇幅關(guān)系,這里就不做處理了。
通過這次實(shí)現(xiàn),我們首先總結(jié)一下其中所涉及的知識點(diǎn)。
- 父組件傳遞給子組件用 props
- 子組件派發(fā)事件,用 $emit
- 跨層級數(shù)據(jù)交互,用 provide 和 inject
- 用 slot 可以預(yù)留插槽
其次是一些思想:
- 單項數(shù)據(jù)流:父組件傳遞給子組件的值,子組件內(nèi)部只能用,不能修改。
- 組件內(nèi)部的 name 屬性,可以通過 this.$parent.$options.name 查找。
- 想要批量處理很多異步的結(jié)果,可以用 promise 對象。
最后,文章會首先發(fā)布在我的 Github ,以及公眾號上,歡迎關(guān)注,歡迎 star。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
用element的upload組件實(shí)現(xiàn)多圖片上傳和壓縮的示例代碼
這篇文章主要介紹了用element的upload組件實(shí)現(xiàn)多圖片上傳和壓縮的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02Vue觸發(fā)input選取文件點(diǎn)擊事件操作
這篇文章主要介紹了Vue觸發(fā)input選取文件點(diǎn)擊事件操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08JavaScript的MVVM庫Vue.js入門學(xué)習(xí)筆記
這篇文章主要介紹了JavaScript的MVVM庫Vue.js入門學(xué)習(xí)筆記,Vue.js是一個新興的js庫,主要用于實(shí)現(xiàn)響應(yīng)的數(shù)據(jù)綁定和組合的視圖組件,需要的朋友可以參考下2016-05-05element-plus日歷(Calendar)動態(tài)渲染以及避坑指南
這篇文章主要給大家介紹了關(guān)于element-plus日歷(Calendar)動態(tài)渲染以及避坑指南的相關(guān)資料,這是最近幫一個后端朋友處理一個前端問題,elementUI中calendar日歷組件內(nèi)容進(jìn)行自定義顯示,實(shí)現(xiàn)類似通知事項的日歷效果,需要的朋友可以參考下2023-08-08詳解vue 在移動端體驗(yàn)上的優(yōu)化解決方案
這篇文章主要介紹了vue 在移動端體驗(yàn)上的優(yōu)化解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05