仿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 相关文章推荐
JavaScript 动态生成方法的例子
Jul 22 Javascript
JavaScript中的运算符种类及其规则介绍
Sep 26 Javascript
javascript不同类型数据之间的运算的转换方法
Feb 13 Javascript
JavaScript中的Web worker多线程API研究
Dec 06 Javascript
基于jQuery实现的无刷新表格分页实例
Feb 17 Javascript
jQuery回到顶部的代码
Jul 09 Javascript
提高JavaScript执行效率的23个实用技巧
Mar 01 Javascript
微信小程序使用canvas的画图操作示例
Jan 18 Javascript
基于vue如何发布一个npm包的方法步骤
May 15 Javascript
JavaScript如何把两个数组对象合并过程解析
Oct 10 Javascript
vue不操作dom实现图片轮播的示例代码
Dec 18 Javascript
解决vue-router路由拦截造成死循环问题
Aug 05 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版(4)
2006/10/09 PHP
php 代码优化之经典示例
2011/03/24 PHP
使用php+apc实现上传进度条且在IE7下不显示的问题解决方法
2013/04/25 PHP
PHP APC配置文件2套和参数详解
2014/06/11 PHP
PHP fclose函数用法总结
2019/02/15 PHP
浅谈PHP之ThinkPHP框架使用详解
2020/07/21 PHP
一个小型js框架myJSFrame附API使用帮助
2008/06/28 Javascript
JavaScript DOM学习第四章 getElementByTagNames
2010/02/19 Javascript
javascript之通用简单的table选项卡实现(二)
2010/05/09 Javascript
js和jquery使按钮失效为不可用状态的方法
2014/01/26 Javascript
使用js Math.random()函数生成n到m间的随机数字
2014/10/09 Javascript
js实现消息滚动效果
2017/01/18 Javascript
浅谈JavaScript异步编程
2017/01/20 Javascript
touch.js 拖动、缩放、旋转 (鼠标手势)功能代码
2017/02/04 Javascript
Angular之指令Directive用法详解
2017/03/01 Javascript
jQuery+Cookie实现切换皮肤功能【附源码下载】
2018/03/25 jQuery
jQuery实现判断上传图片类型和大小的方法示例
2018/04/11 jQuery
JSON的parse()方法介绍
2019/01/31 Javascript
Vue使用localStorage存储数据的方法
2019/05/27 Javascript
jQuery实现简单弹幕效果
2019/11/28 jQuery
小程序跨页面交互的作用与方法详解
2020/01/07 Javascript
JavaScript 绘制饼图的示例
2021/02/19 Javascript
Python数组条件过滤filter函数使用示例
2014/07/22 Python
Python and、or以及and-or语法总结
2015/04/14 Python
python实现跨excel的工作表sheet之间的复制方法
2018/05/03 Python
Python图像处理之简单画板实现方法示例
2018/08/30 Python
python web框架中实现原生分页
2019/09/08 Python
Python利用多线程同步锁实现多窗口订票系统(推荐)
2019/12/22 Python
keras模型可视化,层可视化及kernel可视化实例
2020/01/24 Python
python 实现逻辑回归
2020/12/30 Python
python数据抓取3种方法总结
2021/02/07 Python
Elemis美国官网:英国的第一豪华护肤品牌
2018/03/15 全球购物
世界经理人咨询有限公司面试
2014/09/23 面试题
如何通过 CSS 写出火焰效果
2021/03/24 HTML / CSS
建筑项目策划书
2014/01/13 职场文书
幼儿园心得体会范文
2016/01/21 职场文书