實(shí)現(xiàn)要求
模仿 ElementUI 的表單,分為四層結(jié)構(gòu):index 組件、Form 表單組件、FormItem 表單項(xiàng)組件、Input 和 CheckBox 組件,具體分工如下:
index 組件:
- 實(shí)現(xiàn):分別引入 Form 組件、FormItem 組件、Input 組件,實(shí)現(xiàn)組裝;
Form 表單組件:
- 實(shí)現(xiàn):預(yù)留插槽、管理數(shù)據(jù)模型 model、自定義校驗(yàn)規(guī)則 rules、全局校驗(yàn)方法 validate;
FormItem 表單項(xiàng)組件:
- 實(shí)現(xiàn):預(yù)留插槽、顯示 label 標(biāo)簽、執(zhí)行數(shù)據(jù)校驗(yàn)、顯示校驗(yàn)結(jié)果;
Input 和 CheckBox 組件:
- 實(shí)現(xiàn):綁定數(shù)據(jù)模型 v-model、通知 FormItem 組件執(zhí)行校驗(yàn);
Input 組件
具體實(shí)現(xiàn)如下:
1、自定義組件要實(shí)現(xiàn) v-model 必須實(shí)現(xiàn) :value 和 @input。
2、當(dāng)輸入框中的數(shù)據(jù)發(fā)生變化時(shí),通知父組件執(zhí)行校驗(yàn)。
3、當(dāng) Input 組件綁定的 type 類型為 password 時(shí),在組件內(nèi)部使用 v-bind="$attrs" 獲取 props 之外的內(nèi)容。
4、設(shè)置 inheritAttrs 為 false, 從而避免頂層容器繼承屬性。
Input 組件實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<template> <div> <input :value= "value" @input= "onInput" v-bind= "$attrs" /> </div> </template> <script> export default { inheritAttrs: false , // 避免頂層容器繼承屬性 props: { value: { type: String, default : "" } }, data() { return {}; }, methods: { onInput(e) { // 通知父組件數(shù)值發(fā)生變化 this .$emit( "input" , e.target.value); // 通知 FormItem 執(zhí)行校驗(yàn) // 這種寫法不健壯,因?yàn)?Input 組件和 FormItem 組件之間可能會(huì)隔代 this .$parent.$emit( "validate" ); } } }; </script> <style scoped></style> |
注意:代碼中使用 this.$parent 派發(fā)事件,這種寫法不健壯,當(dāng) Input 組件和 FormItem 組件之間隔代時(shí)會(huì)出現(xiàn)問(wèn)題。具體解決方式見文章尾部代碼優(yōu)化部分。
CheckBox 組件
1、自定義實(shí)現(xiàn) checkBox 的雙向數(shù)據(jù)綁定,和 input 大同小異,必須實(shí)現(xiàn) :checked 和 @change。
CheckBox 組件實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<template> <section> <input type= "checkbox" :checked= "checked" @change= "onChange" /> </section> </template> <script> export default { props: { checked: { type: Boolean, default : false } }, model: { prop: "checked" , event: "change" }, methods: { onChange(e) { this .$emit( "change" , e.target.checked); this .$parent.$emit( "validate" ); } } }; </script> <style scoped lang= "less" ></style> |
FormItem 組件
具體實(shí)現(xiàn)如下:
1、給 Input 組件或者 CheckBox 組件預(yù)留插槽。
2、如果用戶在組件上設(shè)置 label 屬性,要展示 label 標(biāo)簽。
3、監(jiān)聽校驗(yàn)事件,并執(zhí)行校驗(yàn)(使用 async-validator 插件進(jìn)行校驗(yàn))。
4、如果不符合校驗(yàn)規(guī)則,需要顯示校驗(yàn)結(jié)果。
在開發(fā)的過(guò)程中,我們需要思考幾個(gè)問(wèn)題:
1、在組件內(nèi)部,如何得到需要校驗(yàn)的數(shù)據(jù)和校驗(yàn)規(guī)則?
2、在 Form 表單中會(huì)有多個(gè)菜單項(xiàng),如:用戶名、密碼、郵箱...等等,那么 FormItem 組件是如何得知現(xiàn)在校驗(yàn)的是哪個(gè)菜單呢?
FormItem 組件實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
<template> <div class= "formItem-wrapper" > <div class= "content" > <label v- if = "label" :style= "{ width: labelWidth }" >{{ label }}:</label> <slot></slot> </div> <p v- if = "errorMessage" class= "errorStyle" >{{ errorMessage }}</p> </div> </template> <script> import Schema from "async-validator" ; export default { inject: [ "formModel" ], props: { label: { type: String, default : "" }, prop: String }, data() { return { errorMessage: "" , labelWidth: this .formModel.labelWidth }; }, mounted() { // 監(jiān)聽校驗(yàn)事件,并執(zhí)行校驗(yàn) this .$on( "validate" , () => { this .validate(); }); }, methods: { validate() { // 執(zhí)行組件的校驗(yàn) // 1、獲取數(shù)據(jù) const values = this .formModel.model[ this .prop]; // 2、獲取校驗(yàn)規(guī)則 const rules = this .formModel.rules[ this .prop]; // 3、執(zhí)行校驗(yàn) const schema = new Schema({ [ this .prop]: rules }); // 參數(shù)1是值,餐數(shù)2是校驗(yàn)錯(cuò)誤對(duì)象數(shù)組 // validate 返回的是 Promise<Boolean> return schema.validate({ [ this .prop]: values }, errors => { if (errors) { this .errorMessage = errors[0].message; } else { this .errorMessage = "" ; } }); } } }; </script> <style scoped lang= "less" > @labelWidth: 90px; .formItem-wrapper { padding-bottom: 10px; } .content { display: flex; } .errorStyle { font-size: 12px; color: red; margin: 0; padding-left: @labelWidth; } </style> |
我們先回答一下上面提出的兩個(gè)問(wèn)題,此處會(huì)涉及到組件之間傳值,可以參考之前的文章《組件傳值、通訊》:
首先表單的數(shù)據(jù)和校驗(yàn)規(guī)則是定義在 index 組件內(nèi)部,并且掛載在 Form 組件上,表單的校驗(yàn)項(xiàng)發(fā)生在 FormItem 組件中,先在 Form 組件內(nèi)部通過(guò) props 接受到傳遞的數(shù)據(jù),然后通過(guò) provide/inject 的方式在 FormItem 組件中傳遞給后代組件。
我們?nèi)粘T谟?ElementUI 的表單校驗(yàn)是會(huì)發(fā)現(xiàn),在每一個(gè)需要校驗(yàn)的表單上會(huì)設(shè)置一個(gè) prop 屬性,并且屬性值和綁定的數(shù)據(jù)一致。此處的用途是為了能夠在 FormItem 組件中執(zhí)行校驗(yàn)時(shí)獲取相對(duì)的校驗(yàn)規(guī)則和數(shù)據(jù)對(duì)象。
在 FormItem 組件中通過(guò)使用 inject 獲取注入的 Form 實(shí)例,和 prop 屬性組合使用,可以獲取到表單數(shù)據(jù)和校驗(yàn)規(guī)則。
1
2
3
4
5
|
// 1、獲取數(shù)據(jù) const values = this .formModel.model[ this .prop]; // 2、獲取校驗(yàn)規(guī)則 const rules = this .formModel.rules[ this .prop]; |
使用 async-validator 插件實(shí)例化一個(gè) schema 對(duì)象,用來(lái)執(zhí)行校驗(yàn),schema.validate 需要傳遞兩個(gè)參數(shù),參數(shù)1是當(dāng)前需要校驗(yàn)的字段和相對(duì)應(yīng)的 rules 組成的鍵值對(duì)對(duì)象,參數(shù)2是一個(gè) callback 函數(shù),用來(lái)獲取錯(cuò)誤信息(是一個(gè)數(shù)組)。validate 方法返回的是一個(gè) Promise<Boolean>。
注意:此組件的 validate 方法中,最后使用 return 的目的是為了在 Form 組件中執(zhí)行全局校驗(yàn)使用。
Form 組件
具體實(shí)現(xiàn)如下:
1、給 FormItem 組件預(yù)留插槽。
2、傳遞 Form 實(shí)例給后代,比如 FormItem 用來(lái)獲取校驗(yàn)的數(shù)據(jù)和規(guī)則。
3、執(zhí)行全局校驗(yàn)
Form 組件實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<template> <div> <slot></slot> </div> </template> <script> export default { provide() { return { formModel: this // 傳遞 Form 實(shí)例給后代,比如 FormItem 用來(lái)獲取校驗(yàn)的數(shù)據(jù)和規(guī)則 }; }, props: { model: { type: Object, required: true }, rules: { type: Object }, labelWidth: String }, data() { return {}; }, methods: { validate(cb) { // 執(zhí)行全局校驗(yàn) // map 結(jié)果是若干 Promise 數(shù)組 const tasks = this .$children.filter(item => item.prop).map(item => item.validate()); // 所有任務(wù)必須全部校驗(yàn)成功才算校驗(yàn)通過(guò) Promise.all(tasks) .then(() => { cb( true ); }) . catch (() => { cb( false ); }); } } }; </script> <style scoped></style> |
我們?cè)?Form 組件中使用 provide 注入當(dāng)前組件對(duì)象,方便后續(xù)子孫代獲取數(shù)據(jù)/方法使用。
執(zhí)行全局校驗(yàn)的時(shí)候,先使用 filter 過(guò)濾掉不需要校驗(yàn)的組件(我們?cè)?FormItem 組件上設(shè)置的 prop 屬性,只要有此屬性,就是需要校驗(yàn)的),然后分別執(zhí)行組件中的 validate 方法(如果在 FormItem 組件中不使用 return 數(shù)據(jù),最后獲取到的全都是 undefined),返回的是一個(gè)若干 Promise 數(shù)組。
簡(jiǎn)單介紹一個(gè) Promise.all() 方法:
Promise.all() 方法接收一個(gè)promise的iterable類型(注:Array,Map,Set都屬于ES6的iterable類型)的輸入,并且只返回一個(gè)Promise實(shí)例, 那個(gè)輸入的所有promise的resolve回調(diào)的結(jié)果是一個(gè)數(shù)組。這個(gè)Promise的resolve回調(diào)執(zhí)行是在所有輸入的promise的resolve回調(diào)都結(jié)束,或者輸入的iterable里沒有promise了的時(shí)候。它的reject回調(diào)執(zhí)行是,只要任何一個(gè)輸入的promise的reject回調(diào)執(zhí)行或者輸入不合法的promise就會(huì)立即拋出錯(cuò)誤,并且reject的是第一個(gè)拋出的錯(cuò)誤信息。
index 組件
定義模型數(shù)據(jù)、校驗(yàn)規(guī)則等等,分別引入 Form 組件、FormItem 組件、Input 組件,實(shí)現(xiàn)組裝。
index 組件實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
<template> <div> <Form :model= "formModel" :rules= "rules" ref= "loginForm" label-width= "90px" > <FormItem label= "用戶名" prop= "username" > <Input v-model= "formModel.username" ></Input> </FormItem> <FormItem label= "密碼" prop= "password" > <Input type= "password" v-model= "formModel.password" ></Input> </FormItem> <FormItem label= "記住密碼" prop= "remember" > <CheckBox v-model= "formModel.remember" ></CheckBox> </FormItem> <FormItem> <button @click= "onLogin" >登錄</button> </FormItem> </Form> </div> </template> <script> import Input from "@/components/form/Input" ; import CheckBox from '@/components/form/CheckBox' import FormItem from "@/components/form/FormItem" ; import Form from "@/components/form/Form" ; export default { data() { const validateName = (rule, value, callback) => { if (!value) { callback( new Error( "用戶名不能為空" )); } else if (value !== "admin" ) { callback( new Error( "用戶名錯(cuò)誤 - admin" )); } else { callback(); } }; const validatePass = (rule, value, callback) => { if (!value) { callback( false ); } else { callback(); } }; return { formModel: { username: "" , password: "" , remember: false }, rules: { username: [{ required: true , validator: validateName }], password: [{ required: true , message: "密碼必填" }], remember: [{ required: true , message: "記住密碼必選" , validator: validatePass }] } }; }, methods: { onLogin() { this .$refs.loginForm.validate(isValid => { if (isValid) { alert( "登錄成功" ); } else { alert( "登錄失敗" ); } }); } }, components: { Input, CheckBox, FormItem, Form } }; </script> <style scoped></style> |
當(dāng)我們點(diǎn)擊登錄按鈕時(shí),會(huì)執(zhí)行全局校驗(yàn)方法,我們可以使用 this.$refs.xxx 獲取 DOM 元素和組件實(shí)例。
在上面我們還留了一個(gè)小尾巴~,就是在 Input 組件中通知父組件執(zhí)行校驗(yàn),目前使用的是 this.$parent.$emit(),這樣寫有一個(gè)弊端,就是當(dāng) Input 組件和 FormItem 組件之后隔代的時(shí)候,再使用 this.$parent 獲取不到 FormItem 組件。
我們可以封裝一個(gè) dispatch 方法,主要實(shí)現(xiàn)向上循環(huán)查找父元素并且派發(fā)事件,代碼實(shí)現(xiàn)如下:
1
2
3
4
5
6
7
8
9
10
|
dispatch(eventName, data) { let parent = this .$parent; // 查找父元素 while (parent) { // 父元素用$emit觸發(fā) parent.$emit(eventName, data); // 遞歸查找父元素 parent = parent.$parent; } } |
該方法可以借用 mixins 引入使用:mixins/emmiters.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
export default { methods: { dispatch(eventName, data) { let parent = this .$parent; // 查找父元素 while (parent) { // 父元素用$emit觸發(fā) parent.$emit(eventName, data); // 遞歸查找父元素 parent = parent.$parent; } } } }; |
修改 Input 組件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
<template> <div> <input :value= "value" @input= "onInput" v-bind= "$attrs" /> </div> </template> <script> import emmiter from "@/mixins/emmiter" ; export default { inheritAttrs: false , // 避免頂層容器繼承屬性 mixins: [emmiter], props: { value: { type: String, default : "" } }, data() { return {}; }, methods: { onInput(e) { // 通知父組件數(shù)值發(fā)生變化 this .$emit( "input" , e.target.value); // 通知 FormItem 執(zhí)行校驗(yàn) // 這種寫法不健壯,因?yàn)?Input 組件和 FormItem 組件之間可能會(huì)隔代 // this.$parent.$emit("validate"); this .dispatch( "validate" ); // 使用 mixin 中 emmiter 的 dispatch,解決跨級(jí)問(wèn)題 } } }; </script> <style scoped></style> |
總結(jié)
到此這篇關(guān)于Vue模仿ElementUI的form表單的文章就介紹到這了,更多相關(guān)Vue模仿ElementUI form表單內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://juejin.cn/post/6937691963709718558