vue组件—element-ul的Radio组件的实现(仿写)

前言

这里分享一些自己在学习Vue组件的时候仿写过的一些组件,这篇是element-ul的Radio,对于组件的实现话,主要以template、script两部分为主,style基本上都来自源码的样式,文章的内容我会在我实现的代码上进行注释我为什么要这样写,有些组件我分成残次写法和完整写法两种,可能看起来会有些凌乱,如果你有什么好的点子,也可以与我分享,我会学习并整合起来。

template

残次写法:在不实现element-ui文档的css样式的基础上实现单选框的效果

<template>
   // 6.那么按照5的提示,我需要给整个按钮绑定事件来用来传递改变后的值,给label
   //   绑定是运用冒泡的原理,在点击Radio组件的任意一个部分的时候,他都会触发
   //   这个事件,避免用户点击的时候自己明明点到了,但是没有被选中,以此优化用户
   //   体验
   
   // 8.在7的基础上已经完成了label和value的相等,现在只需要给input选中添加条件
   //   就完成了,也就是checked属性的值是true还是false
   
  <label @click="trigger">
    <input type="radio" :checked="value === label"/>
    <span>
      <slot>选项数据</slot>
    </span>
  </label>
</template>

完整写法:实现跟element-ui文档上面类似的效果

<template>
  // 主体结构:
  // a.对于element-ui的单选框而言,从外观上看需要一个能够选中的input,
  //   以及一个放置在后面内容的标签,因为是行内元素,所以以span标签为
  //   主
  
  
  // label标签:
  // a.单选框和多选框一般是行内元素,但是如果你想使用div这种标签然后使用
  //   display:block也可以
  // b.label标签可以和input标签进行联动,之前分析过,当他们两者进行绑定
  //   的时候,点击label标签等价于点击input标签,优化用户的体验感
  
  // 第一个span标签:
  // a.为了实现跟文档上面类似的样式,那么这里需要自己实现一个ckeckbox的
  //   单选框
  // b.input标签自带的checkbox属性仰视你很丑,所以需要将其display:none
  //   隐藏起来
  // c.然后自己使用一个span标签来模拟出一个小圆圈
  // d.然后是否选中的话在文档中这个在很多地方是需要用到的,所以将其封装
  //   为一个方法isChecked,方便调用
  
  // 带有slot的span标签:
  // a.因为这个地方是需要渲染位子内容的,但是文字内容又需要外界传递,所以
  //   在文字渲染的这个位置就需要使用到插槽了,在slot插槽那个地方说到过,
  //   插槽可以将传递的内容给接住,并将接住的内容渲染到插槽所在的这个位置。
  
  
  
  <label class="co-radio" @click="trigger">
    <span :class="['co-radio__input', { 'is-checked': isChecked }]">
      <span class="co-radio__inner"></span>
      <input type="radio" :checked="isChecked" />
    </span>
    <span class="co-radio__label">
      <slot>选项数据</slot>
    </span>
  </label>
</template>

script

残次写法

**element-ul的Radio的使用**

// 1.在el-radio标签上面看见了v-model,那么这个标签上绑定了一个value的属性
//   和一个input的事件,而且这个value属性必须是props才能完成数据的双向绑定

// 2.然后猜测在组件使用的时候是因为标签的label的值与value的值相等的时候,标
//   签才显示为选中,所以label的值也是动态的,所以也需要进行双向绑定,那么
//   label的值也必须是props属性才可以。

// 3.通过上面两点来看的话这两个的属性应该是必传的,对于传递的值string和number
//   都可以

// 4.这样看的话选中状态的切换是因为点击改变了radio组件里面的label的值,然后
//   将改变后的label值传递到组件外部,将组件外部绑定的value值将其修改掉,因此
//   得到另外的value与label的相等关系,因而改变选中状态。


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



<script>
export default {
  name: 'co-radio',
  props: {
    value: {
      type: [String, Number],
    },
    label: {
      required: true,
      type: [String, Number],
    },
  },
  methods: {
    trigger() {
      // 7.当点击6创建的事件的时候,会触发父组件中绑定的input事件,
      //   在触发事件的同时将修改后的label的值传递出去,此时外部
      //   绑定的value的值也会改变成为传递的label值,
      this.$emit('input', this.label)
    },
  },
};
</script>

完整写法

<script>
export default {
  name: 'co-radio',
  props: {
    value: {
      type: [String, Number],
    },
    label: {
      required: true,
      type: [String, Number],
    },
  },
  methods: {
    trigger() {
      this.$emit('input', this.label)
    },
  },
  computed: {
    isChecked() {
      return this.value === this.label;
    },
    
    // 实现一个在文档上面的一个disabled的不可选的属性,如果不传值,
    // 默认为false,如果传值,以传递的布尔值为准
    isDisabled() {
      // 以前的文章提到过,组件上面的属性如果没有声明的话都是attr属性,
      // 那么就可以使用vm.$attrs.属性名来访问
      const disabledValue = this.$attrs.hasOwnProperty('disabled');
      if (disabledValue) {
        if (typeof disabledValue === 'boolean') {
          return disabledValue;
        } else {
          return true;
        }
      } else {
        return false;
      }
    },
  },
};
</script>

style

<style lang="scss">
.co-radio {
  /* pointer-events: none; */
  color: #606266;
  font-weight: 500;
  line-height: 1;
  position: relative;
  cursor: pointer;
  display: inline-block;
  white-space: nowrap;
  outline: none;
  font-size: 14px;
  margin-right: 30px;
  .is-checked + &__label {
    color: #409eff;
  }
  .is-checked > &__inner::after {
    transform: translate(-50%, -50%) scale(1) !important;
  }
  &__input.is-checked > &__inner {
    background-color: #409eff;
    border-color: #409eff;
  }
  &__input {
    white-space: nowrap;
    cursor: pointer;
    outline: none;
    display: inline-block;
    line-height: 1;
    position: relative;
    vertical-align: middle;
    .co-radio__inner {
      border: 1px solid #dcdfe6;
      border-radius: 100%;
      width: 14px;
      height: 14px;
      background-color: #fff;
      position: relative;
      cursor: pointer;
      display: inline-block;
      box-sizing: border-box;
      &::after {
        width: 4px;
        height: 4px;
        border-radius: 100%;
        background-color: #fff;
        content: '';
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%) scale(1);
        transition: transform 0.15s ease-in;
      }
    }
    > input {
      display: none;
    }
  }
  &__label {
    font-size: 14px;
    padding-left: 10px;
  }
}
</style>

扩展:单选框组的实现

实现目标

// 只需要在el-radio-group上面使用v-model绑定,就能将el-radio-group里面
// 所有的el-radio都使用v-model绑定,就不用一个一个的去添加了

<el-radio-group v-model="radio">
    <el-radio :label="3">备选项</el-radio>
    <el-radio :label="6">备选项</el-radio>
    <el-radio :label="9">备选项</el-radio>
</el-radio-group>

简单实现el-radio-group组件

<template>
  <div class="el-radio-group"> 
    <slot />
  </div>
</template>

<script>
export default {
  // 给其一个name的属性
  name: 'co-radio-group',
  // 因为使用v-model进行了绑定,所以存在一个props的value属性
  props: {
    value: {
      required: true,
      type: [String, Number],
    },
  },
};
</script>

注意:在定义组件的时候,按照规定需要给其一个name的属性,如果是去找父组件的name属性,那么使用vm.$parent.$options.name可以获取,如果寻找子组件的name属性,使用vm.$children.$options.name可以获取,

el-radio作为el-radio-group的时候

// el-radio作为el-radio-group的时候,结构没有什么变化,不过在属性和功能上
// 存在一些差异,下面解释一下差异在什么地方

<script>
export default {
  name: 'co-radio',
  props: {
    value: {
      // 1.因为如果在el-radio-group上面使用v-model绑定,就能将
      //   el-radio-group里面所有的el-radio都使用v-model绑定,
      //   而绑定就存在value的props属性,所以这里的value属性变得
      //   不是必传属性了。
      type: [String, Number],
    },
    label: {
      required: true,
      type: [String, Number],
    },
  },
  methods: {
    trigger() {
      // 5.因为既可以在el-radio-group上使用v-model,也可以在自己身上使用,
      // 那么就需要判断一下input事件在谁的身上,也就是需要往谁的身上传值了
      this.$parent.$options.name === 'co-radio-group'
        ? this.$parent.$emit('input', this.label)
        : this.$emit('input', this.label);
    },
  },
  computed: {
    // 2.现在因为value的来源有两个,所以需要进行判断,并且以_开头的属性是
    //   不希望外界直接访问的。
    _value() {
      // 3.判断一下el-radio标签的父组件是不是el-radio-group,如果是,那就
      //   说明此时是嵌套关系,value的值来自el-radio-group,如果不是,就
      //   表示不是嵌套关系,那么v-model肯定绑定在自己的身上,所以value的
      //   值自然就来自自己了。
      return this.$parent.$options.name === 'co-radio-group'
        ? this.$parent.value
        : this.value;
    },
    isChecked() {
      // 4.同样还是判断value与label的值是否相等,只是有区别的在于value值的
      // 来源不一样而已。
      return this._value === this.label;
    },
    isDisabled() {
      const disabledValue = this.$attrs.hasOwnProperty('disabled');
      if (disabledValue) {
        if (typeof disabledValue === 'boolean') {
          return disabledValue;
        } else {
          return true;
        }
      } else {
        return false;
      }
    },
  },
};
</script>

选中的流程(我的理解)因为radio标签的label属性是必传的,所以我的每一个radio标签上面有一个不变的label属性,那么这个属性值也不会发生变化,如果默认选中标签的label的值是1,然后我点击一个label的值为2的标签,因为点击就会触发trigger函数,函数里面的$emit函数会向radio组件外部传递值,在$emit函数触发的过程中,我得标签的label的值由原来的1变成了现在的2,此时会触发radio组件外部的使用v-model在组件上面绑定的那个回调函数,因为使用v-model默认会存在一个input的事件,那么此时就会触发绑定的这个input事件,而事件对应的回调函数的参数就是我所传递过来的改变后的label的值,在input事件触发之后,它会使用改变后的label的值将绑定的value的值修改掉,而radio组件内部的value的属性与v-model绑定的value是相同的,因为radio组件内部的label的值是没有变化的,而绑定的value的属性发生改变,这样他就会寻找哪一个组件的label和我的value值是相等的,这样就完成了从label为1的组件切换到了label为2的组件了,后面的切换以此内推,

组件的全局注册—Vue.component的替代Vue.use()

概念:直接使用Vue.use(组件名)就可以全局注册一个组件了

作用:使其可以辅助我们间接调用其他的Vue的静态方法

原理

// 举例:为Vue添加一个静态方法$use

// 1.组件就是一个配置对象,所以typeof组件的时候,它会是一个object
// 2.然后它会给执行组件里面的一个install的方法,这个方法执行的时候
//   会自动传递一个Vue实例作为install函数的参数
// 3.在install的方法里面,使用Vue.component全局注册组件,组件的名字
//   是自己给组件的name的属性

Vue.$use() = function( model ) {
    if( typeof model === 'object' && module.install === 'function') {
        module.install( this )
    }
}

function install( v ) {
    v.component( this.name, this )
}


使用对于自己定义的组件需要手动添加一个install的方法,这样才能让自己定义的组件使用use全局注册

// 在组件的文件夹里面新建一个index.js的文件书写以下文件:

import 组件名 from 组件存放的位置

组件名.install = function ( v ) {
    v.component( this.name, this )
}

export default 组件名

原文链接:https://juejin.cn/post/7225133152490586171 作者:梦满枝头月初盈

(0)
上一篇 2023年4月24日 上午10:30
下一篇 2023年4月24日 上午10:41

相关推荐

发表回复

登录后才能评论