我在element-ui的radio中学到了什么?

2,548 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、引言


今天也是充满希望的一天呢(摸鱼的一天),闲来无事翻了翻开发中常用的element-ui,发现了radio组件中的一些巧妙设计,我们一起来看看怎么样来实现一个radio组件吧。

二、准备工作


准备好瓜子可乐小板凳

三、正文


1. radio

在开始之前我们先看看element-ui的radio是怎么使用的

<template> 
    <el-radio v-model="radio" label="1">备选项</el-radio>
    <el-radio v-model="radio" label="2">备选项</el-radio> 
</template>

可以看到我们的组件需要接收valuelabelslot插槽这些东西,那么我们的思路就出来了,创建一个基础模板如下

<template>
 <div>
     <input type="radio">
     <div class="radio-radio-label">
        <slot></slot>
     </div>
 </div>
</template>
<script>
export default {
   componentName: 'radio-demo',
   props:{
       label: {
           type: String,
           default: ""
       },
       value: {
           type: String,
           default: ""
       }
   }
}
</script>

接下来我们思考一下怎么样才能然input和传过来的数据进行联动呢,这里需要注意一些细节

props中的value可以接收v-model传进来的值,但是这个数据是单向的,只能读不能更改的,所以我们要通过计算属性将value二次封装

this.$emit('input',val) 可以修改父组件v-model绑定值(v-model原理)

v-model:作用于radio类型的标签时会将value值赋予v-model绑定值

接下来我们的组件就可以写成这样

<template>
 <div class="radio-container">
     <div class="radio-input">
          <input 
           type="radio"
           :value="label"
           v-model="model"
           :checked='model === label'
           >
     </div>
     <div class="radio-radio-label">
        <slot></slot>
     </div>
 </label>
</template>
<script>
export default {
   componentName: 'radio-demo',
   props:{
       label: {
           type: String,
           default: ""
       },
       value: {
           type: String,
           default: ""
       }
   },
   computed:{
       model:{
           get(){
               return this.value
           },
           set(val){
               this.$emit('input', val)
           }
       }
   }
}
</script>

看效果

soogif(1).gif

盲仔:实现了没有完全实现,你这不是在逗我吗,这就是原生radio!

QQ截图20211229111858.png

搞错了!再来,接下来我们来隐藏掉原生input,再把自己的制作的div版radio放上去

我们来给input穿上皇帝的新衣

.radio-real{
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            opacity: 0;
            z-index: -1;
        }

加上我们自己制作的DIV版的 "radio"

 <div class="radio-show" :class="{'is-checked':model === label}">
    <div class="radio-inner"></div>
 </div>
 
       .radio-show{
            border: 1px solid #eee;
            border-radius: 50%;
            width: 14px;
            height: 14px;
            display:flex;
            align-items: center;
            justify-content: center;
            .radio-inner{
            width: 4px;
            height:4px;
            border-radius: 50%;
            background-color: #fff
        }
            
        }
        .is-checked{
            background-color:rgb(152, 237, 243)
        }

这时候我们打开页面就会发现,MD为啥点不动!! 莫慌,看样式我们就知道input被上层div遮挡住了

label标签来解决这个问题,该标签可以为input标签定义标记,当用户点击label标签的时候会触发被标记input,用法如下

 <label role="radio"></label>

所以我们可以将最外层div换成label来解决遮盖问题,我们来看下效果如何

QQ录屏1.gif

一个简单的radio组件就完成了

2.radio-group

radio完成了,那我们来看看单选组是怎么实现的吧,先来看看element-ui的radio-group是怎么用的吧

<el-radio-group v-model="radio" @change='handleChange'> 
    <el-radio :label="6">备选项</el-radio> 
    <el-radio :label="9">备选项</el-radio> 
</el-radio-group>

我们只需要在radio-group进行v-model参数绑定,并且通过$emit触发实例中的change事件即可,话不多说~甘蔗

<template>
<div class="container">
  <slot></slot>
</div>
</template>

<script>
export default {
componentName: "demo-radio-group",
props:{
  value: {
    type:String,
    default:''
  }
},
created(){
  this.$on('handleChange',val=>{
    this.$emit('change',val)
  })
}

}

一个简单的group就搞定啦,为什么要把$emit放到当前实例事件上面呢,我们后面再说,现在呢我们来思考下把value和change事件都挪到了radio-group组件中,那我们上一节中做的radio组件要怎么获取value,怎么通过$emit('input')来修改v-model的值呢?

既然我们没办法在radio组件中拿到这些东西,那我们就直接在radio中获取到radio-group组件实例,通过radio-group的实例来进行操作即可。

在computed中获取radio-group组件

 groupVm(){
          let parent = this.$parent
          while (parent){
              if(parent.$options.componentName !== 'demo-radio-group'){
                  parent = parent.$parent;
              }else{
                  return  parent;
              }
          }
          return null
      }

重写radio组件中的计算属性model

model:{
          get(){
              return this.groupVm?this.groupVm.value : this.value
          },
          set(val){
              if(this.groupVm){
                  this.groupVm.$emit('input',val)
              }else{
                  this.$emit('input', val)
              }
              
          }
      }

经过这两部分的操作,我们的radio-group已经可以正常工作了,我们来看看效果如何

 <radio-group v-model="val">
       <radio  label="radio1">1</radio>
       <radio  label="radio2">2</radio>
  </radio-group>

  <radio-group v-model="val1" >
       <radio  label="radio3">3</radio>
       <radio  label="radio4">4</radio>
  </radio-group>

QQ2.gif

嗯~~~ 味道很对。接下来回头看看我们的change事件。

既然valueinput事件都能够直接在radio调用group的实例进行调用,那么change也是可以的。

//radio.vue
handleChange(){
            this.groupVm.$emit('change',this.model)
          }
          
          
//使用页面 home.vue
  <radio-group v-model="val" @change="handleChange">
         <radio  label="radio1">1</radio>
         <radio  label="radio2">2</radio>
    </radio-group>

这样是非常不好追溯事件传递的,所以我们可以在radio-group通过on植入一个handleChange事件,在这个事件中执行this.on植入一个handleChange事件,在这个事件中执行this.emit(change)事件

//radio.vue
 handleChange(){
            this.groupVm.$emit('handleChange',this.model)
        }
        
//radio-group.vue
 created(){
    this.$on('handleChange',val=>{
      this.$emit('change',val)
    })
  }

这样我们的change事件就完成了

3. emitter

在组件的制作过程中,经常会有触发父级或者子级实例中的事件操作,emitter就是用来处理这种操作的工具,在vue2中通过混入emitter.js来进行事件调用

function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
  var name = child.$options.componentName;

  if (name === componentName) {
    child.$emit.apply(child, [eventName].concat(params));
  } else {
    broadcast.apply(child, [componentName, eventName].concat([params]));
  }
});
}
export default {
methods: {
  dispatch(componentName, eventName, params) {
    var parent = this.$parent || this.$root;
    var name = parent.$options.componentName;

    while (parent && (!name || name !== componentName)) {
      parent = parent.$parent;

      if (parent) {
        name = parent.$options.componentName;
      }
    }
    if (parent) {
      parent.$emit.apply(parent, [eventName].concat(params));
    }
  },
  broadcast(componentName, eventName, params) {
    broadcast.call(this, componentName, eventName, params);
  }
}
};

使用:

//radio.vue
import Emitter from './emitter.js'
export default {
    mixins:[Emitter]
    ...一系列生命周期
    methods:{
        handleChange(){
            this.$nextTick(() => {
            this.$emit('change', this.model);
            this.groupVm && this.dispatch('demo-radio-group', 'handleChange', this.model);
        });
        }
    }
}

代码

1.radio.vue

<template>
  <label class="radio-container" role="radio">
      <div class="radio-input">
          <div class="radio-show" :class="{'is-checked':model === label}">
              <div class="radio-inner"></div>
          </div>
           <input 
            class="radio-real"
            type="radio"
            :value="label"
            v-model="model"
            :checked='model === label'
            @change='handleChange'
            >
      </div>
      <div class="radio-radio-label">
         <slot></slot>
      </div>
     
  </label>
</template>
<script>
import Emitter from "./emitter"
export default {
    componentName: 'demo-radio',
    mixins:[Emitter],
    props:{
        label: {
            type: String,
            default: ""
        },
        value: {
            type: String,
            default: ""
        }
    },
    computed:{
        groupVm(){
            let parent = this.$parent
            while (parent){
                if(parent.$options.componentName !== 'demo-radio-group'){
                    parent = parent.$parent;
                }else{
                    return  parent;
                }
            }
            return null
        },
        model:{
            get(){
                return this.groupVm?this.groupVm.value : this.value
            },
            set(val){
                if(this.groupVm){
                    this.groupVm.$emit('input',val)
                }else{
                    this.$emit('input', val)
                }
                
            }
        }
    },
    methods:{
        handleChange(){
            this.$nextTick(() => {
            this.$emit('change', this.model);
            this.groupVm && this.dispatch('demo-radio-group', 'handleChange', this.model);
        });
        }
    }
}
</script>

<style scoped lang='scss'>
.radio-container{
    display: flex;
    align-items: center;
    .radio-input{
        position: relative;
        margin-right: 10px;
        .radio-show{
            border: 1px solid #eee;
            border-radius: 50%;
            width: 14px;
            height: 14px;
            display:flex;
            align-items: center;
            justify-content: center;
            .radio-inner{
            width: 4px;
            height:4px;
            border-radius: 50%;
            background-color: #fff
        }
            
        }
        .is-checked{
            background-color:rgb(152, 237, 243)
        }
        .radio-real{
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            opacity: 0;
            z-index: -1;
        }

    }
}

</style>

2.radio-group

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  componentName: "demo-radio-group",
  props:{
    value: {
      type:String,
      default:''
    }
  },
  created(){
    this.$on('handleChange',val=>{
      this.$emit('change',val)
    })
  }

}
</script>

3.emitter.js

function broadcast(componentName, eventName, params) {
    this.$children.forEach(child => {
      var name = child.$options.componentName;
  
      if (name === componentName) {
        child.$emit.apply(child, [eventName].concat(params));
      } else {
        broadcast.apply(child, [componentName, eventName].concat([params]));
      }
    });
  }
  export default {
    methods: {
      dispatch(componentName, eventName, params) {
        var parent = this.$parent || this.$root;
        var name = parent.$options.componentName;
  
        while (parent && (!name || name !== componentName)) {
          parent = parent.$parent;
  
          if (parent) {
            name = parent.$options.componentName;
          }
        }
        if (parent) {
          parent.$emit.apply(parent, [eventName].concat(params));
        }
      },
      broadcast(componentName, eventName, params) {
        broadcast.call(this, componentName, eventName, params);
      }
    }
  }

4. home.vue

<template>
  <div class="container">
    <radio-group v-model="val" @change="handleChange">
         <radio  label="radio1">1</radio>
         <radio  label="radio2">2</radio>
    </radio-group>
    
    <radio-group v-model="val1" @change="handleChange1">
         <radio  label="radio3">3</radio>
         <radio  label="radio4">4</radio>
    </radio-group>
  </div>
</template>

<script>
import radio from '../../components/radio/radio.vue'
import radioGroup from '../../components/radio/radio-group.vue'
export default {
    components:{
        radio,radioGroup
    },
    data(){
        return {
            val:'',
            val1:''
        }
    },
    methods:{
        handleChange(val){
            console.log(val);
        },
        handleChange1(val){
            console.log(val);
        }
    }

}
</script>

四、总结


      element-ui中的radio组件没有什么难度,属于是新手级别的入门组件,不过里面也是又很多可以学到的东西,像组件中的设计以及实例中属性的获取和事件的调用,emiter工具的设计理念等,具有短小精悍的特点

今天的分享就是这些啦~~

听说喜欢点赞的你,今年年终奖拿到手软😍