溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

仿ElementUI實現(xiàn)一個Form表單的實現(xiàn)代碼

發(fā)布時間:2020-08-31 14:40:26 來源:腳本之家 閱讀:179 作者:李等等扣丁 欄目:web開發(fā)

使用組件就像流水線上的工人;設(shè)計組件就像設(shè)計流水線的人,設(shè)計好了給工人使用。

完整項目地址:仿 ElementtUI 實現(xiàn)一個 Form 表單

一. 目標

仿 ElementUI 實現(xiàn)一個簡單的 Form 表單,主要實現(xiàn)以下四點:

  • Form
  • FormItem
  • Input
  • 表單驗證

我們先看一下 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 的表單中,主要進行了 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 。

為了實現(xiàn) e-form 表單,我們參考 ElementUI 的表單用法,總結(jié)出以下我們需要設(shè)計的功能。

  • e-form 負責全局校驗,并提供插槽;
  • e-form-item 負責單一項校驗及顯示錯誤信息,并提供插槽;
  • e-input 負責數(shù)據(jù)雙向綁定;

1. Input 的設(shè)計

我們首先觀察一下 ElementUI 中的 Input 組件:

<el-input v-model="ruleForm.name"></el-input>

在上面的代碼中,我們發(fā)現(xiàn) input 標簽可以實現(xiàn)一個雙向數(shù)據(jù)綁定,而實現(xiàn)雙向數(shù)據(jù)綁定需要我們在 input 標簽上做兩件事。

  • 要綁定 value
  • 要響應(yīng) input 事件

當我們完成這兩件事以后,我們就可以完成一個 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>

我們對上面的代碼做一點解釋:

**解釋一:**既然我們想做一個 Input 組件,那么接收的值必然是父組件傳進來的,并且當父組件沒有傳進來值的時候,我們可以它一個默認值 ""

**解釋二:**我們在設(shè)計組件的時候,要遵循單向數(shù)據(jù)流的原則:父組件傳進來的值,我們只能用,不能改。那么將父組件傳進來的值進行一個賦值操作,賦值給 Input 組件內(nèi)部的 valueInInput ,如果這個值發(fā)生變動,我們就修改內(nèi)部的值 valueInInput 。這樣我們既可以處理數(shù)據(jù)的變動,又不會直接修改父組件傳進來的值。

**解釋三:**當 Input 中的值發(fā)生變動時,觸發(fā) @input 事件,此時我們通過 event.target.value 獲取到變化后的值,將它重新賦值給內(nèi)部的 valueInInput 。

**解釋四:**完成了內(nèi)部賦值之后,我們需要做的就是將變化后的值通知父組件,這里我們用 this.$emit 向上派發(fā)事件。其中第一個參數(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>

仿ElementUI實現(xiàn)一個Form表單的實現(xiàn)代碼 

2. FormItem 的設(shè)計

<el-form-item label="用戶名" prop="name">
		<el-input v-model="ruleForm.name"></el-input>
</el-form-item>

在 ElementUI 的 formItem 中,我們可以看到:

  1. 需要 label 來顯示名稱;
  2. 需要 prop 來校驗當前項;
  3. 需要給 inputbutton 預(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ù) ElementUI 中的用法,我們知道 label 是父組件傳來,且當傳入時我們展示,不傳入時不展示。

解釋二: slot 是一個預(yù)留的槽位,我們可以在其中放入 input 或其他組件、元素。

解釋三: p 標簽是用來展示錯誤信息的,如果驗證狀態(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>

仿ElementUI實現(xiàn)一個Form表單的實現(xiàn)代碼 

3. Form 的設(shè)計

到現(xiàn)在,我們已經(jīng)完成了最內(nèi)部的 input 以及中間層的 FormItem 的設(shè)計,現(xiàn)在我們開始設(shè)計最外層的 Form 組件。

當層級過多并且組件間需要進行數(shù)據(jù)傳遞時,Vue 為我們提供了 provideinject API,方便我們跨層級傳遞數(shù)據(jù)。

我們舉個例子來簡單實現(xiàn)一下 provideinject 。在 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>

仿ElementUI實現(xiàn)一個Form表單的實現(xiàn)代碼

根據(jù)上圖,我們可以看到無論跨越多少層級, provideinject 可以非常方便的實現(xiàn)數(shù)據(jù)的傳遞。

理解了上面的知識點后,我們可以開始設(shè)計 Form 組件了。

<el-form :model="ruleForm" :rules="rules" ref="loginForm">
	
</el-form>

根據(jù) ElementUI 中表單的用法,我們知道 Form 組件需要實現(xiàn)以下功能:

  • 提供數(shù)據(jù)模型 model;
  • 提供校驗規(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>

解釋一:該組件需要用戶傳遞進來一個數(shù)據(jù)模型 model 進來,類型為 Object 。 rules 為可傳項。

解釋二:為了讓各個層級都能使用 Form 中的數(shù)據(jù),需要依靠 provide 函數(shù)提供數(shù)據(jù)。

解釋三:直接將組件的實例傳遞下去。

完成了 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>

仿ElementUI實現(xiàn)一個Form表單的實現(xiàn)代碼

到目前為止,我們的基本功能就已經(jīng)實現(xiàn)了,除了提交與驗證規(guī)則外,所有的組件幾乎與 ElementUI 中的表單一模一樣了。下面我們就開始實現(xiàn)校驗功能。

4. 設(shè)計校驗規(guī)則

在上面設(shè)計的組件中,我們知道校驗當前項和展示錯誤信息的工作是在 FormItem 組件中,但是數(shù)據(jù)的變化是在 Input 組件中,所以 FormItemInput 組件是有數(shù)據(jù)傳遞的。當 Input 中的數(shù)據(jù)變化時,要告訴 FormItem ,讓 FormItem 進行校驗,并展示錯誤。

首先,我們修改一下 Input 組件:

methods: {
  handlerInput(event) {
   this.valueInInput = event.target.value;
   this.$emit("input", this.valueInInput);
   
   // 數(shù)據(jù)變了,定向通知 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 直接派發(fā)事件,因為在 FormItem 組件中, Input 組件的位置只是一個插槽,無法做事件監(jiān)聽,所以此時我們讓 FormItem 自己派發(fā)事件,并自己監(jiān)聽。修改 FormItem 組件,在 created 中監(jiān)聽該事件。

created() {
	this.$on('validate', this.validate);
}

Input 組件中的數(shù)據(jù)變化時, FormItem 組件監(jiān)聽到 validate 事件后,執(zhí)行 validate 函數(shù)。

下面,我們就要處理我們的 validate 函數(shù)了。而在 ElementUI 中,驗證用到了一個底層庫async-validator,我們可以通過 npm 安裝這個包。

npm i async-validator

async-validator 是一個可以對數(shù)據(jù)進行異步校驗的庫,具體的用法可以參考上面的鏈接。我們通過這個庫來完成我們的 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];
    // 校驗器
    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 組件提供的數(shù)據(jù) - Form 組件的實例,下面就可以使用 this.eForm.xxx 來使用 Form 中的數(shù)據(jù)了。

解釋二:因為我們需要在 Form 組件中校驗所有的 FormItem ,所以當 FormItem 掛載完成后,需要派發(fā)一個事件告訴 Form :你可以校驗我了。

解釋三:當 FormItem 中有 prop 屬性的時候才校驗,沒有的時候不校驗。比如提交按鈕就不需要校驗。

<e-form-item>
		<input type="submit" @click="submitForm()" value="提交">
</e-form-item>

**解釋四:**返回一個 promise 對象,批量處理所有異步校驗的結(jié)果。

解釋五: descriptor 對象是 async-validator 的用法,采用鍵值對的形式,用來檢查當前項。比如:

// 檢查當前項
// async-validator 給出的例子
name: {
		type: "string",
		required: true,
		validator: (rule, value) => value === 'muji',
}

FormItem 中檢查當前項完成了,現(xiàn)在我們需要處理一下 Form 組件中的全局校驗。表單提交時,需要對 form 進行一個全局校驗。大致的思路是:循環(huán)遍歷表單中的所有派發(fā)上來的 FormItem ,讓每一個 FormItem 執(zhí)行自己的校驗函數(shù),如果有一個為 false ,則校驗不通過;否則,校驗通過。我們通過代碼實現(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 緩存需要校驗的表單項,因為我們在 FormItem 中派發(fā)了事件。只有需要校驗的 FormItem 會被派發(fā)到這里,而且都會保存在數(shù)組中。

if (this.prop) {
   this.dispatch('EForm', 'addFiled', this);
}

解釋二:當點擊提交按鈕時,會觸發(fā)這個事件。

解釋三:遍歷所有被添加到 fileds 中的 FormItem 項,讓每一項單獨去驗證,會返回 Promise 的 truefalse 。將所有的結(jié)果,放在一個數(shù)組 eachFiledResultArray 中。

解釋四:獲取所有的結(jié)果,統(tǒng)一進行處理,其中有一個結(jié)果為 false ,驗證就不能通過。

至此,一個最簡化版本的仿 ElementUI 的表單就實現(xiàn)了。

仿ElementUI實現(xiàn)一個Form表單的實現(xiàn)代碼 

四. 總結(jié)

當然上面的代碼還有很多可以優(yōu)化的地方,比如說 dispatch 函數(shù),我們可以寫一遍,使用的時候用 mixin 導入。由于篇幅關(guān)系,這里就不做處理了。

通過這次實現(xiàn),我們首先總結(jié)一下其中所涉及的知識點。

  • 父組件傳遞給子組件用 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)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI