>[success] # 利用通信 仿写一个form 通信 ~~~ 1.自定义个组件之间传值就不能再使用'bus'和'vuex' 等系列, 因为'bus' 和 'vuex' 都是项目级别的传递,自定义组件无法使用 变依赖'vuex' 或者'bus',因此需要使用组件通信的其他几种特殊传递 2.第一种常见的父子传递'props' 父传子,'$emit' 控制父组件的某个方法, 让控制父组件的某个方法将从子组件传递过来的值重新赋值形成'子传父'。 3.第二种'vue' api文档中的'provide' / 'inject' ,'提供'/'注入', 这种只能'父传子',但不仅限'父传子'甚至'爷传孙'可以理解成 是一个加强版的'props'。 3.第三种'mixins' 混入继承,这种有点投机取巧适合接口,也就是提取出一个 公共的功能,在对应需要该接口的位置进行调用即可,这种不是传统意义父子 传值,更像是因为组件使用的数据来自一个接口,大家或者这个这接口数值 导致数据一致 4.利用自己的写的 'dispatch' 和 'broadcast' 进行传值,分别是'子孙像父组件' '父组件像子孙传值',利用的原理是'$on' 和 '$emit' ~~~ >[info] ## 分析前辈们form 组件 -- iview ~~~ 1.根据下面代码可以分析实际这个form组件是由两个组件构成的,分别是'Form' 和'FormItem' 组件构成的 2.'Form' 中有两个值分别是'model'和'rules' 依次表示是'对应的字段'和'表单验证规则' 3.'FormItem' 中的'prop' 对应着'model' 中的某个字段,'FormItem'中有个插槽可以填充, 各个输入框 ~~~ ~~~ <template> <div> <Form ref="formInline" :model="formInline" :rules="ruleInline" inline> <FormItem prop="user"> <Input type="text" v-model="formInline.user" placeholder="Username"> <Icon type="ios-person-outline" slot="prepend"></Icon> </Input> </FormItem> </Form> </div> </template> <script> export default { name: 'home', data(){ return{ formInline: { user: '', }, ruleInline: { user: [ { required: true, message: 'Please fill in the user name', trigger: 'blur' } ], } } } } </script> ~~~ >[info] ## 开始定义自己form ~~~ 1.根据上面分析,需要三个组件分别是'Form','Form-item','Input' 2.思路,定义验证规则传入给'Form','Form' 将规则分发给不同的'FormItem' ,每个'FormItem' 去进行验证 3.组件目录 │ ├── 'components' // 存放组件文件夹 │ │ └── 'Form' // 存放对iview 二次封装文件 │ │ └─ 'myFromItem.vue' // 每个单独输入框验证组件 │ │ └─ 'myFrom.vue' // 配置验证规则和字段 4.使用组件通信(三)中自定义封装的方法 ~~~ >[danger] ##### 先自定义个一个input 输入框组件 ~~~ 1.常用v-model 绑定input,'v-model' 是一个语法糖,实际':value' 和'@input' 一个缩写,隐藏在写自己的input组件的时候需要拆开来定义 2.'input' 触发有两种一种是'input' 也就是输出就触发,'blur' 失去焦点触发, 因此组件触发要留出这两个触发事件,三还可以增加'change'事件 3.blur:失去焦点时触发,常见的有输入框失去焦点时触发校验; change:实时输入时触发或选择时触发,常见的有输入框实时输入时触发校验、 下拉选择器选择项目时触发校验等。 ~~~ ~~~ <template> <input type="text" :value="currentValue" @blur="handleBlur" @input="handleInput"> </template> <script> import Emitter from '../../mixins/emitter.js'; export default { mixins:[Emitter], props:{ // 接受父传子 过来的值,因为v-mode 是:value 和 @input的缩写 value:{ type:String, default:'' } }, data(){ return{ currentValue:this.value } }, methods:{ handleInput (event) { // 通过event 获取当前值传递给父组件 const value = event.target.value; this.currentValue = value; this.$emit('input', value); // 通过 自定义子传父 的方式 将自定义的触发名'on-form-change'传递给formItem this.dispatch('iFormItem', 'on-form-change', value); }, handleBlur () { // 通过 自定义子传父 的方式 将自定义的触发名'on-form-change'传递给formItem this.dispatch('iFormItem', 'on-form-blur', this.currentValue); } } } </script> ~~~ >[danger] ##### myFromItem.vue 设计 * 编写组件第一步从样式布局 和 生命周期要做的下手 ~~~ 1.html 的设计需要有label 和一个插槽,其中插槽用来和各种类型的input组 合,'当字段必填的时候' 需要给字段加一个红星 2.根据前辈的分装可以发现'FormItem' 主要填了'prop' 和 'label'也就是依次对 应在form 传入的规则字段,和label 需要展示的名字 3.分析生命周期要做的事,首先要知道父子组件的生命周期调用的顺序: vm.created -》vm.beforedMount-》child.created-》child.beforeMount -》child.mounted-》vm.mounted 根据上面分析简单的首先子组件的生命周期是最先执行完毕的简单的说, 最外层的父组件最开始 会调用自己的created 和beforedMount等生命周期 然后对应最内层的子组件会调用自己的完整的周期,然后再执行父组的'mounted' 生命周期 4.首先我们需要将子组件内容传递给父组件,也就是要将'FormItem' 中的内容传递给 'Form' 组件中,父传子的几种方式中我们选着我们自定义的写的封装方法传递, 让其直接触发的方式也有几种'生命周期','计算属性','watch监听',我们不需要吧所有 的'FormItem' 对象传递过去,也就是有'prop' 需要验证的传递给'Form' 组件 ,需要对 规则'Form' 中有'rules' 规则的字段加※号,关于触发'input' 传递过来的子传父调用 也应当在'mounted '声明周期,因为$on要想绑定监听就需要在生命周期中 4.1 总结上面 在'mounted' 生命周期要做的,将'FormItem' 对象子传父给'Form', 将'Form中的规则获取'并且如果是必填项将加上※,将获取input传递过来的触发事件 用$on 监听上,也就是'input' 和'blur' 事件中的方法 4.2 为了明确容易区分 分别定义了'getRules' 用来获取'Form' 父组件中的规则,和'setRules' 给'FormItem' 中必填项加※ 5.当前生命周期结束后提供 给父组件一个效果 的方法 ~~~ ~~~ <template> <div> <label v-if="label" :class="{ 'i-form-item-label-required': isRequired }">{{label}}</label> <slot></slot> </div> </template> <script> import Emitter from '../../mixins/emitter.js'; export default { name: "mFormItem", mixins:[Emitter], inject:['form'], props:{ label: { type: String, default: '' }, prop:{ type:String, }, }, data(){ return{ isRequired:false } }, methods:{ // 通过inject 接收form 中验证规则,获取对应字段验证条件 getRules(){ let formRules = this.form.rules // 如果有就获取当前字段的规则 formRules = formRules ? formRules[this.prop] :[] // concat 会生成新的数组返回 return [].concat(formRules || []) }, // 获取规则操作给dom 加红色星星标记 和获取input 组件绑定的触发事件 setRules(){ let rules = this.getRules(); // 拿到 数组做是否有内容在做循环 if(rules.length){ // 循环每一项 在规则对象中看是否存在required 也就是必填 // 来看是否加星 rules.every((rule) => { // 如果当前校验规则中有必填项,则标记出来 this.isRequired = rule.required; }); } // 将 父组件对象事件 单独提取出来写成方法 this.$on('on-form-blur', this.onFieldBlur); this.$on('on-form-change', this.onFieldChange); }, }, mounted(){ // 如果有prop 验证字段就将当期formItem 传递给Form if(this.prop){ this.dispatch('mForm', 'on-form-item-add', this) // 如果有调用 setRules 方法 this.setRules(); } }, // 组件销毁前,将实例从 Form 的缓存中移除 beforeDestroy () { this.dispatch('iForm', 'on-form-item-remove', this); } } </script> <style scoped> .i-form-item-label-required:before { content: '*'; color: red; } .i-form-item-message { color: red; } </style> ~~~ * 第二步 从完善功能下手 ~~~ 1.上面已经从'Form' 获取当前'Form' 字段的验证,并对必填项做了处理,这 吧就需要获取当前验证的触发方式,如果有则继续执行 2.需要使用一个第三方的验证库,来专门验证一些表单规则'async-validator' 3.考虑重置功能,重置的数据也是form 的model 里面提供的因此,在item最开始, 生命周期初始的时候 就应该 保存要被重置的内容 ~~~ * 最终完整代码 ~~~ <template> <div> <label v-if="label" :class="{ 'i-form-item-label-required': isRequired }">{{label}}</label> <slot></slot> <div v-if="validateState === 'error'" class="i-form-item-message">{{ validateMessage }}</div> </div> </template> <script> import Emitter from '../../mixins/emitter.js'; import AsyncValidator from 'async-validator'; export default { name: "mFormItem", mixins:[Emitter], inject:['form'], props:{ label: { type: String, default: '' }, prop:{ type:String, }, }, data(){ return{ isRequired: false, // 是否为必填 加星星 validateState: '', // 校验状态 validateMessage: '', // 校验不通过时的提示信息 } }, computed: { // 从 Form 的 model 中动态得到当前表单组件的数据 fieldValue () { return this.form.model[this.prop]; } }, methods:{ // 通过inject 接收form 中验证规则,获取对应字段验证条件 getRules(){ let formRules = this.form.rules // 如果有就获取当前字段的规则 formRules = formRules ? formRules[this.prop] :[] // concat 会生成新的数组返回 return [].concat(formRules || []) }, // 获取规则操作给dom 加红色星星标记 和获取input 组件绑定的触发事件 setRules(){ let rules = this.getRules(); // 拿到 数组做是否有内容在做循环 if(rules.length){ // 循环每一项 在规则对象中看是否存在required 也就是必填 // 来看是否加星 rules.every((rule) => { // 如果当前校验规则中有必填项,则标记出来 this.isRequired = rule.required; }); } // 将 父组件对象事件 单独提取出来写成方法 this.$on('on-form-blur', this.onFieldBlur); this.$on('on-form-change', this.onFieldChange); }, // 获取'Form' 中 'rules' 触发验证规则是否是规定的 blur 和 change getFilterRule(tigger){ const rules = this.getRules(); // 没填写验证规则 或者验证规则等于blur和change 的验证获取到 return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1); }, // 开始验证 validate(trigger, callback = function () {}){ let rules = this.getFilteredRule(trigger); // 不符合 当前验证 则不往下执行 if (!rules || rules.length === 0) { return true; } // 设置状态为校验中 如果是erro 则显示报错信息 this.validateState = 'validating'; // 使用第三方验证库来验证我们传入的规则 // 以下为 async-validator 库的调用方法 let descriptor = {}; descriptor[this.prop] = rules; const validator = new AsyncValidator(descriptor); let model = {}; // 在计算属性中获取 当前输入框的内容this.fieldValue model[this.prop] = this.fieldValue; validator.validate(model, { firstFields: true }, errors => { this.validateState = !errors ? 'success' : 'error'; this.validateMessage = errors ? errors[0].message : ''; callback(this.validateMessage); }); }, onFieldBlur() { this.validate('blur'); }, onFieldChange() { this.validate('change'); }, // 重置数据 resetField () { this.validateState = ''; this.validateMessage = ''; this.form.model[this.prop] = this.initialValue; }, }, mounted(){ // 如果有prop 验证字段就将当期formItem 传递给Form if(this.prop) { this.dispatch('mForm', 'on-form-item-add', this) // 如果有调用 setRules 方法 this.setRules(); // 设置初始值,以便在重置时恢复默认值 this.initialValue = this.fieldValue; } }, // 组件销毁前,将实例从 Form 的缓存中移除 beforeDestroy () { this.dispatch('iForm', 'on-form-item-remove', this); } } </script> <style scoped> .i-form-item-label-required:before { content: '*'; color: red; } .i-form-item-message { color: red; } </style> ~~~ >[danger] ##### Form 组件分析 * 从生命周期开始下手 ~~~ 1.在'FormItem' 的生命周期中提供了传递给form组件的,当前对象的方法, 因此在'Form' 的生命周期开始的时候去接收,因为每个'FormItem' 是相互 独立的因此需要将他们统一保存起来,调用他们的重置和验证功能 2.我们刚才重置和验证都是给每个单独的'FormItme'做的,简单的说他们的 触发条件就是失去焦点和数据改变,因此有时候我们需要点击提交按钮 才触发判断每一个'FormItem' 是否好用,因此我们的'validate' 是验证所有的 3.代码的讲解 ~~~ ~~~ <template> <form> <slot></slot> </form> </template> <script> export default { name: 'mForm', props: { model: { type: Object }, rules: { type: Object }, }, provide() { return { form : this }; }, data () { return { fields: [] }; }, methods: { // 公开方法:全部重置数据 resetFields() { this.fields.forEach(field => { field.resetField(); }); }, // 公开方法:全部校验数据,支持 Promise validate(callback) { return new Promise(resolve => { let valid = true; let count = 0; this.fields.forEach(field => { // 调用'Formitem' 中的验证方法,第二个参数是回调函数 // 验证的方法 如果没有target也就是触发判断的第一个参数为空或者是指定的'change'和'blur'都会触发 // 现在获取了每一个'FormItem' 中的验证是否正确并且调用回调可以触发我们自己规则 // 使用Promise 是为了支持异步 field.validate('', errors => { if (errors) { valid = false; } if (++count === this.fields.length) { // 全部完成 resolve(valid); if (typeof callback === 'function') { callback(valid); } } }); }); }); } }, created () { this.$on('on-form-item-add', (field) => { if (field) this.fields.push(field); }); this.$on('on-form-item-remove', (field) => { if (field.prop) this.fields.splice(this.fields.indexOf(field), 1); }); } } </script> ~~~