自定义el-form实现

322 阅读1分钟

1代码结构

image.png

2.代码实现

2.1 index.vue

实现测试代码

<template>
    <div>
        <JForm ref="submitForm" :model="model" :rules="rules">
            <JFormItem label="用户名" prop="username">
                 <JInput v-model="model.username" type="text" placeholder="请输入用户名2"></JInput>
            </JFormItem>
            <JFormItem label="用户密码" prop="password">
                 <JInput  v-model="model.password"  type="password" placeholder="请输入用户密码" ></JInput>
            </JFormItem> 
            <JFormItem>
                 <button @click="onClick">提交</button>
            </JFormItem>
        </JForm>
    </div>
</template>

<script>
 import JForm from "./JForm";
 import JFormItem from "./JFormItem";
 import JInput from "./JInput";
    export default {
        components: {
            JForm,
            JFormItem,
            JInput
        },
        data() {
            return {
                model:{
                    username:'',
                    password:'',
                },
                rules: {
                    "username" : [{required:true,message:"请输入用户信息"}],
                    "password" : [{required:false,message:"请输入用户密码"}],
                }
            }
        },
        methods: {
            onClick() {
                this.$refs.submitForm.validate(isVaild => {
                    if(isVaild){
                        alert("验证成功")
                    }else {
                        alert("验证失败")
                    }
                });
            }
        },
    }
</script>

2.2 JForm.vue

技术点

  • 使用provide做跨部件对象访问
  • prop记录model表单信息和rules表单校验规则
  • this.$children遍历所有子组件formItem的validate方法,并且返回promise对象,通过promise.all做批量处理。都成功才返回true 校验成功
<template>
    <div>
        <slot></slot>
    </div>
</template>

<script>
    export default {
        name : "JForm",
        provide () {
            return {
                form :this
            }
        },
        props: {
            model: {
                type: Object,
                required : true
            },
            rules: {
                type: Object,
                required : false
            }
        },
        methods: {
            //callbackFn是主页面调用成功的回调
            validate(callbackFn) {//校验所有formItem的验证
               let list = this.$children.filter(item=> item.prop)
               let taskList = []
               list.forEach(item => {
                   taskList.push(item.validate())
               })
               //判断所有promise对象是否成功,
               Promise.all(taskList).then(res => {
                   callbackFn(true)
               }).catch(e => {
                   callbackFn(false)
               }) 
            }
        },
    }
</script>

2.3 JFormItem.vue

技术点

  • 通过async-validator实现校验
  • inject : ['form'],注入provide的form对象
  • 对象属性名{[this.prop] :rules } 使用计算属性方式[this.prop]简写
<template>
    <div>
        <label v-if="label">{{label}}</label>
        <slot></slot>
        <p class="error">{{error}}</p>
    </div>
</template>

<script>
import Validator from "async-validator";
    export default {
        inject : ['form'],
        name : "JFormItem",
        props: {
            label: {
                type: String,
                default: ''
            },
            prop: {
                type: String,
                default: ''
            },
        },
        data() {
            return {
                error: ''
            }
        },
        mounted () {
            //添加监听方法用于子组件通知自己要开始校验
            this.$on("validate", () => {
                this.validate()
            })
        },
        methods: {
            //返回一个promise对象
            validate() {
                let rules = this.form.rules[this.prop]
                let value = this.form.model[this.prop]
                let validator = new Validator({[this.prop] :rules })
                return validator.validate({[this.prop]:value}, errors => {
                    if(errors) {
                        this.error =  errors[0].message
                    }else {
                        this.error = ""
                    }
                } )
            }
        },
    }
</script>
<style scoped>
.error {color: red;}
</style>

2.4 JInput.vue

技术点

  • v-bind="$attrs" 获取传入的所有prop
  • inheritAttrs:false 在调用的地方不显示prop信息
  • @input="onInput" :value="value" 自己实现v-model效果
  • this.parent.parent.emit与父组件JFormItem交互
<template>
    <div>
        <input :type="type" @input="onInput" :value="value" v-bind="$attrs" >
    </div>
</template>

<script>
    export default {
        inheritAttrs:false,
        name : "JInput" ,
        props: {
            value: {
                type: String,
                default: ""
            },
            type: {
                type: String,
                default: ""
            },
        },
        methods: {
            onInput(e) { 
                this.$emit('input',e.target.value)
                // 每次编辑都触发校验
                this.$parent.$emit('validate')
            }
        },
    }
</script>

2.3代码优化

问题:

  • 由于使用 $chlidren 和 $parent 与组件产生强关联,一旦父子关系变化,不便于后期重构与代码调整。

1.解决父组件Form遍历所有FormItem问题

实现步骤

  • 父组件itemList,添加监听方法“jFrom.addItem”,等待子组件实例化后加入进来itemList
  • 子组件实例化后,调用$dispatch派发把自己 注册到父组件的itemList
  • 父组件就可以直接遍历所有itemList子组件

2.解决jInput调用FormItem的validate的校验方法

实现步骤

子组件直接调用$dispatch派发,父组件validate方法校验

代码实现

  • 定义$boardcast 支持无限往下查找$chlid中对应eventName广播方法
  • 定义$dispatch 往上查找$parent对应eventName的派发方法
  • 通过匹配componentName 自定义组件名称来确定是否找到
//注册全局方法 
//1. 从子往上传递
Vue.prototype.$dispatch = function(componentName,eventName,data) {
  let parent = this.$parent || this.$root
  let name = parent.$options.componentName
  while(parent && (!name ||name !== componentName)){//找不到相同的父级组件名字或者未定义一直往上找
      parent = parent.$parent
  } 
  if(parent) {
       parent.$emit.apply(parent,[eventName].concat(data));//父组件调用$emit 触发自己定义的$on事件
  }
}
//2. 从父往子传递 暂时没用到
Vue.prototype.$boardcast = function(componentName,eventName,data) {
  boardcast.call(this,componentName,eventName,data);
}
function boardcast(componentName,eventName,data) {  
  this.$children.forEach(child => {
       let name = child.$options.componentName
       if(name === componentName) {
            child.$emit.apply(child,[eventName].concat(data));
       } else {
            boardcast.apply(child,[componentName,eventName].concat(data));
       }
  });
}

//对应页面新增componentName属性用于 寻找时匹配
//JForm.vue
export default {
        name : "jFrom" ,
        componentName : "j-from",
         data() {
            return {
                itemList: [] // 新增所有子模块的集合对象,用于校验时候一起触发
            }
        },
        created () {
            //等待jfromItem注册进来
            this.$on("jFrom.addItem", (item) => {
                this.itemList.push(item)
            })
        },
          methods: {
            //callbackFn是主页面调用成功的回调
            validate(callbackFn) {//校验所有formItem的验证
               let taskList = []
               this.itemList.forEach(item => {
                   taskList.push(item.validate())
               })
            }
        },
}

//JFormItem.vue
export default {
        name : "JFormItem" ,
        componentName : "j-from-item",
        mounted () {
            //加载完的formItem都自动注册进入父组件的itemList
            if(this.prop) {
                this.$dispatch("j-from","jFrom.addItem",[this]) //这里由于$dispatch使用的是 apply 所以参数需要是数组,使用[this]
            }
        },
}


//JInput.vue
export default { 
         methods: {
            onInput(e) {
                this.$emit('input',e.target.value)
                // this.$parent.$emit("validate") 
                this.$dispatch('j-form-item', 'validate')//往上找到对应的el-form-item下的validate方法
            }
        },
}