仿ElementUI实现一个Form表单的实现代码


Posted in Javascript onApril 23, 2019

使用组件就像流水线上的工人;设计组件就像设计流水线的人,设计好了给工人使用。

完整项目地址:仿 ElementtUI 实现一个 Form 表单

一. 目标

仿 ElementUI 实现一个简单的 Form 表单,主要实现以下四点:

  • 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 层嵌套关系, Form 是最外面一层, FormItem 是中间一层,最内层是 Input 或者 Button

二. 创建项目

我们通过 Vue CLI 3.x 创建项目。

使用 vue create e-form 创建一个目录。

使用 npm run serve 启动项目。

三. Form 组件设计

ElementUI 中的表单叫做 el-form ,我们设计的表单就叫 e-form

为了实现 e-form 表单,我们参考 ElementUI 的表单用法,总结出以下我们需要设计的功能。

  • e-form 负责全局校验,并提供插槽;
  • e-form-item 负责单一项校验及显示错误信息,并提供插槽;
  • e-input 负责数据双向绑定;

1. Input 的设计

我们首先观察一下 ElementUI 中的 Input 组件:

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

在上面的代码中,我们发现 input 标签可以实现一个双向数据绑定,而实现双向数据绑定需要我们在 input 标签上做两件事。

  • 要绑定 value
  • 要响应 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>

仿ElementUI实现一个Form表单的实现代码 

2. FormItem 的设计

<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 预留插槽;

根据上面的需求,我们可以创建出自己的 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>

仿ElementUI实现一个Form表单的实现代码 

3. Form 的设计

到现在,我们已经完成了最内部的 input 以及中间层的 FormItem 的设计,现在我们开始设计最外层的 Form 组件。

当层级过多并且组件间需要进行数据传递时,Vue 为我们提供了 provideinject API,方便我们跨层级传递数据。

我们举个例子来简单实现一下 provideinject 。在 App.vue 中,我们提供数据(provide)。

export default {
 name: "app",
 provide() {
  return {
   msg: '哥是最外层提供的数据'
  }
 }
};
</script>

接着,我们在最内层的 Input.vue 中注入数据,观察结果。

<template>
 <div>
  <!-- 1、绑定 value 
  2、响应 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实现一个Form表单的实现代码

根据上图,我们可以看到无论跨越多少层级, provideinject 可以非常方便的实现数据的传递。

理解了上面的知识点后,我们可以开始设计 Form 组件了。

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

根据 ElementUI 中表单的用法,我们知道 Form 组件需要实现以下功能:

  • 提供数据模型 model;
  • 提供校验规则 rules;
  • 提供槽位,里面放我们的 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 进来,类型为 Objectrules 为可传项。

解释二:为了让各个层级都能使用 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实现一个Form表单的实现代码

到目前为止,我们的基本功能就已经实现了,除了提交与验证规则外,所有的组件几乎与 ElementUI 中的表单一模一样了。下面我们就开始实现校验功能。

4. 设计校验规则

在上面设计的组件中,我们知道校验当前项和展示错误信息的工作是在 FormItem 组件中,但是数据的变化是在 Input 组件中,所以 FormItemInput 组件是有数据传递的。当 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 的 truefalse 。将所有的结果,放在一个数组 eachFiledResultArray 中。

解释四:获取所有的结果,统一进行处理,其中有一个结果为 false ,验证就不能通过。

至此,一个最简化版本的仿 ElementUI 的表单就实现了。

仿ElementUI实现一个Form表单的实现代码 

四. 总结

当然上面的代码还有很多可以优化的地方,比如说 dispatch 函数,我们可以写一遍,使用的时候用 mixin 导入。由于篇幅关系,这里就不做处理了。

通过这次实现,我们首先总结一下其中所涉及的知识点。

  • 父组件传递给子组件用 props
  • 子组件派发事件,用 $emit
  • 跨层级数据交互,用 provide 和 inject
  • 用 slot 可以预留插槽

其次是一些思想:

  • 单项数据流:父组件传递给子组件的值,子组件内部只能用,不能修改。
  • 组件内部的 name 属性,可以通过 this.$parent.$options.name 查找。
  • 想要批量处理很多异步的结果,可以用 promise 对象。

最后,文章会首先发布在我的 Github ,以及公众号上,欢迎关注,欢迎 star。

 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js写一个字符串转成驼峰的实例
Jun 21 Javascript
js 限制input只能输入数字、字母和汉字等等
Dec 18 Javascript
关于jQuery判断元素是否存在的问题示例探讨
Jul 21 Javascript
javascript在IE下trim函数无法使用的解决方法
Sep 12 Javascript
JavaScript获得url所有参数键值表的方法
Mar 21 Javascript
JavaScript实现Iterator模式实例分析
Jun 09 Javascript
JS与jQ读取xml文件的方法
Dec 08 Javascript
jquery html动态添加的元素绑定事件详解
May 24 Javascript
JS模拟超市简易收银台小程序代码解析
Aug 18 Javascript
JS实现前端缓存的方法
Sep 21 Javascript
详解如何实现一个简单的Node.js脚手架
Dec 04 Javascript
vue下拉菜单组件(含搜索)的实现代码
Nov 25 Javascript
Vue在 Nuxt.js 中重定向 404 页面的方法
Apr 23 #Javascript
vue项目首屏加载时间优化实战
Apr 23 #Javascript
灵活使用console让js调试更简单的方法步骤
Apr 23 #Javascript
详解实现一个通用的“划词高亮”在线笔记功能
Apr 23 #Javascript
Vue源码学习之关于对Array的数据侦听实现
Apr 23 #Javascript
vue的keep-alive中使用EventBus的方法
Apr 23 #Javascript
js继承的这6种方式!(上)
Apr 23 #Javascript
You might like
PHP初学者头疼问题总结
2006/10/09 PHP
php基础知识:函数基础知识
2006/12/13 PHP
五款PHP代码重构工具推荐
2014/10/14 PHP
php中使用key,value,current,next和prev函数遍历数组的方法
2015/03/17 PHP
PHP图片裁剪与缩放示例(无损裁剪图片)
2017/02/08 PHP
加载jQuery后$冲突的解决办法
2010/07/09 Javascript
ASP.NET jQuery 实例8 (动态添加内容到DropDownList)
2012/02/03 Javascript
JQuery+CSS提示框实现思路及代码(纯手工打造)
2013/05/07 Javascript
javascript实现详细时间提醒信息效果的方法
2015/03/11 Javascript
简介JavaScript中的push()方法的使用
2015/06/09 Javascript
javascript实现选中复选框后相关输入框变灰不可用的方法
2015/08/11 Javascript
jquery仿QQ登录账号选择下拉框效果
2016/03/22 Javascript
js检测离开或刷新页面时表单数据是否更改的方法
2016/08/02 Javascript
Ionic2调用本地SQlite实例
2017/04/22 Javascript
windows下vue-cli及webpack搭建安装环境
2017/04/25 Javascript
详解React-Native解决键盘遮挡问题(Keyboard遮挡问题)
2017/07/13 Javascript
如何利用ES6进行Promise封装总结
2019/02/11 Javascript
Vue实现一个图片懒加载插件
2019/03/11 Javascript
[02:18]《我与DAC》之工作人员:为了热爱DOTA2的玩家们
2018/03/28 DOTA
举例讲解Linux系统下Python调用系统Shell的方法
2015/11/07 Python
详谈pandas中agg函数和apply函数的区别
2018/04/20 Python
jupyter 中文乱码设置编码格式 避免控制台输出的解决
2020/04/20 Python
Python Tornado核心及相关原理详解
2020/06/24 Python
Python工程师必考的6个经典面试题
2020/06/28 Python
浅析两列自适应布局的3种思路
2016/05/03 HTML / CSS
关于iframe跨域使用postMessage的实现
2019/10/29 HTML / CSS
BCBG官网:BCBGMAXAZRIA
2017/12/29 全球购物
C#实现对任一张表的数据进行增,删,改,查要求,运用Webservice,体现出三层架构
2014/07/11 面试题
《月迹》教学反思
2014/02/19 职场文书
大学军训自我鉴定大全
2014/09/18 职场文书
领导班子“四风问题”“整改方案
2014/10/02 职场文书
党员对十八届四中全会的期盼思想汇报范文
2014/10/17 职场文书
利用python Pandas实现批量拆分Excel与合并Excel
2021/05/23 Python
MySQL Shell import_table数据导入的实现
2021/08/07 MySQL
Mysql事务索引知识汇总
2022/03/17 MySQL
sql注入报错之注入原理实例解析
2022/06/10 MySQL