仿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 相关文章推荐
IE8 下的Js错误HTML Parsing Error...
Aug 14 Javascript
javascript 获取页面的高度及滚动条的位置的代码
May 06 Javascript
jQuery使用drag效果实现自由拖拽div
Jun 11 Javascript
星期几的不同脚本写法(推荐)
Jun 01 Javascript
AngularJS入门教程之 XMLHttpRequest实例讲解
Jul 27 Javascript
javascript删除html标签函数cIsHTML
Jan 09 Javascript
想用好React的你必须要知道的一些事情
Jul 24 Javascript
jQuery实现table中两列CheckBox只能选中一个的示例
Sep 22 jQuery
node 利用进程通信实现Cluster共享内存
Oct 27 Javascript
jQuery判断网页是否已经滚动到浏览器底部的实现方法
Oct 27 jQuery
vue.js删除列表中的一行
Jun 30 Javascript
angular8.5集成TinyMce5的使用和详细配置(推荐)
Nov 16 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
收音机的保养
2021/03/01 无线电
用函数读出数据表内容放入二维数组
2006/10/09 PHP
php daodb插入、更新与删除数据
2009/03/19 PHP
php实现文件下载(支持中文文名)
2013/12/04 PHP
PHP获取文件夹大小函数用法实例
2015/07/01 PHP
Laravel中七个非常有用但很少人知道的Carbon方法
2017/09/21 PHP
PHP设计模式之装饰器模式定义与用法简单示例
2018/08/13 PHP
php简单检测404页面的方法示例
2019/08/23 PHP
jquery 插件 web2.0分格的分页脚本,可用于ajax无刷新分页
2008/12/25 Javascript
jquery ajax 登录验证实现代码
2009/09/23 Javascript
JS获取select-option-text_value的方法
2013/12/26 Javascript
js实现文本框中焦点在最后位置
2014/03/04 Javascript
js实现弹出窗口、页面变成灰色并不可操作的例子分享
2014/05/10 Javascript
Extjs grid panel自带滚动条失效的解决方法
2014/09/11 Javascript
js贪吃蛇网页版游戏特效代码分享(挑战十关)
2015/08/24 Javascript
JavaScript中实现Map的示例代码
2015/09/09 Javascript
浅析jQuery Ajax请求参数和返回数据的处理
2016/02/24 Javascript
基于RequireJS和JQuery的模块化编程日常问题解析
2016/04/14 Javascript
AngularJs篇:使用AngularJs打造一个简易权限系统的实现代码
2016/12/26 Javascript
jQuery插件FusionCharts绘制2D双折线图效果示例【附demo源码】
2017/04/14 jQuery
nodeJS(express4.x)+vue(vue-cli)构建前后端分离实例(带跨域)
2017/07/05 NodeJs
微信小程序实现漂亮的弹窗效果
2020/05/26 Javascript
Vue.js组件高级特性实例详解
2018/12/24 Javascript
Vue函数式组件-你值得拥有
2019/05/09 Javascript
轻松解决JavaScript定时器越走越快的问题
2019/05/13 Javascript
node.js爬虫框架node-crawler初体验
2020/10/29 Javascript
python3.8 微信发送服务器监控报警消息代码实现
2019/11/05 Python
Python Django搭建网站流程图解
2020/06/13 Python
使用纯 CSS 创作一个脉动 loader效果的源码
2018/09/28 HTML / CSS
英国第一的市场和亚马逊替代品:OnBuy
2019/03/16 全球购物
倩碧澳大利亚官网:Clinique澳大利亚
2019/07/22 全球购物
商务日语专业毕业生求职信
2013/10/26 职场文书
公证委托书格式
2014/09/13 职场文书
学校群众路线专项整治方案
2014/10/31 职场文书
二十年同学聚会致辞
2015/07/28 职场文书
Redis数据同步之redis shake的实现方法
2022/04/21 Redis