网站首页 » 前端开发 » Vue » Vue2 Element 模仿秀-单选框篇(Radio)
上一篇:
下一篇:

Vue2 Element 模仿秀-单选框篇(Radio)

前言

这里 Vue Element 模仿秀的第二篇:单选框。经过第一篇的洗礼,对 Element 组件库有了基本的了解,并慢慢地爱上了 Element 模仿秀,在模仿的过程当中,真的可以学到很多东西,有一种豁然开朗,柳暗花明又一村的感觉,下面进入正题。

在开始之前,我们不妨先来看看模仿秀的示例演示:

Vue Element 模仿秀-单选框篇(Radio)

效果图跟官方的基本保持一致,功能也非常接近。下面我们就来深入代码内部,看看 Element 源码的大致实现思路,代码我也没有完事按照官方的来,有些我觉得暂时没有必要的就去掉了。在代码中的很多地方我都作了注释。不过仅代码个人的可能是偏面的法,请多多指教。

简单的文件结构:

├── index.html
├── main.js
├── App.vue
├── mixins
│  └── emitter.js
└── components
   ├── Home.vue          # 应用组件的地方
   ├── Radio.vue
   ├── RadioButton.vue
   └── RadioGroup.vue

上码时间:

Home.vue
<template>
 <div class="container">
<div class="panel panel-default">
  <div class="panel-heading">基础用法(选中了第{{radio+1}}个)</div>
  <div class="panel-body">
    <yk-radio v-model="radio" :label="0">按钮</yk-radio>
    <yk-radio v-model="radio" :label="1">按钮</yk-radio>
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">单选框组(选中了第{{radio1+1}}个)</div>
  <div class="panel-body">
     <yk-radio-group v-model="radio1">
      <yk-radio :label="0">按钮</yk-radio>
      <yk-radio :label="1">按钮</yk-radio>
    </yk-radio-group>
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">带有边框(选中了第{{radio2+1}}个)</div>
  <div class="panel-body">
  <yk-radio-group v-model="radio2" border size="medium">
    <yk-radio :label="0" disabled>按钮</yk-radio>
    <yk-radio :label="1">按钮</yk-radio>
    <yk-radio :label="2">按钮</yk-radio>
    <yk-radio :label="3">按钮</yk-radio>
    <yk-radio :label="4">按钮</yk-radio>
    <yk-radio :label="5">按钮</yk-radio>
  </yk-radio-group>
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">按钮样式(选中了第{{radio3+1}}个)</div>
  <div class="panel-body">
  <yk-radio-group v-model="radio3" size="medium">
    <yk-radio-button :label="0" disabled>按钮</yk-radio-button>
    <yk-radio-button :label="1">按钮</yk-radio-button>
    <yk-radio-button :label="2">按钮</yk-radio-button>
    <yk-radio-button :label="3">按钮</yk-radio-button>
    <yk-radio-button :label="4">按钮</yk-radio-button>
    <yk-radio-button :label="5">按钮</yk-radio-button>
  </yk-radio-group>
  </div>
</div>

<!-- 在父组件这里,很巧妙地在子组件上使用了 v-model 来实现父子组件之间的数据双向绑定-->
 </div>
</template>

<script>
import YkRadio from "@/components/Radio";
import YkRadioButton from "@/components/RadioButton";
import YkRadioGroup from "@/components/RadioGroup";
import "@/assets/icon.css";
export default {
  name: "Home",
  components: { YkRadio, YkRadioButton, YkRadioGroup },
  data() {
    return {
      radio: 0,
      radio1: 0,
      radio2: 0,
      radio3: 0
    };
  },
  watch: {
    radio() {
      console.log(this.radio);
    },
    radio1() {
      console.log(this.radio1);
    },
    radio2() {
      console.log(this.radio2);
    },
    radio3() {
      console.log(this.radio3);
    }
  }
};
</script>

上面通过 watch 来监听单选按钮的选中状态,并打印出来。这里就相当于模拟取值了,在实际的应用中,一般情况下我们也不需要用 watch 来监听这些值的变量,因为这些值都是双向绑定的。

如果单选按钮有用 yk-radio-group 标签包裹起来的,你只需要在它上面用 v-model 来绑定一个值就 ok 了,并且你还可以在它上面添加按钮大小的配置项和禁用的配置项。当然如果你只想禁用某个按钮,你直接在对应按钮上添加 disabled 属性就可以了。

Radio.vue
<template>
  <!--
   在 label 标签中官方还绑定了一个 keydown 事件:
   @keydown.space.stop.prevent="model = label"
   stop : 阻止事件继续传播
   prevent : 阻止事件默认行为
   space : 空格按键
   在这里我把它去掉了,在按下空格键的时候,给 model 赋下值而已,我还没想到会在什么场景下用到,所以去掉了。
  -->
  <label class="yk-radio"
  :class="[
    border && size ? 'yk-radio-' + size : '',
    { 'is-checked': model === label,
      'is-disabled': isDisable,
      'is-bordered': border
    }
    ]"><span class="yk-radio-box" :class="[
        {
          'is-checked': model === label,
          'is-disabled': isDisable
          }
        ]">
        <i class="yk-radio-icon"></i>
        <input 
          v-model="model" 
          type="radio" 
          :value="label" 
          :disabled="isDisable" 
          :name="name" 
          @change="handleChange"
          class="yk-radio-input">
        </span>
        <span class="yk-radio-text">
          <slot></slot>
          <template v-if="!$slots.default">{{label}}</template> 
        </span>
    </label>
</template>
<!-- 如果组件标签里没有传东西,则默认使用 label 值作为名称 这里不清楚 $slots 的可以网上搜一下-->
<script>
import Emitter from "@/mixins/emitter";
export default {
  name: "YkRadio",
  componentName: "YkRadio",
  mixins: [Emitter],
  // 接收父组件传过来的值
  props: {
    label: {}, // 什么都不填,表示不对变量作限制
    value: {},
    disabled: Boolean,
    name: String
  },
  // 强烈建议使用 props 的对象形式来接收参数,而不是用 props 的数组形式来接收父组件传过来的参数
  // 因为使用对象形式接收参数时,你在父组件中给子组件可以直接传一个 disabled 属性,而不需要写成 disabled=true 就可以自动转为 false 或者 true,前提是你设置了接收的数据类型
  // props: ["label", "name", "value", "disabled"],
  computed: {
    model: {
      /* 
       * 由于计算属性默认只有 get 方法,所以如果我们想在其它地方设置 model,
       * 我们就需要给 model 属性添加一个 set 方法,此时 get 方法也需要重写下。
      */
      get() {
        return this.isGroup ? this._radioGroup.value : this.value;
      },
      set(val) {
        // val 变量为设置 model 时的值,即 model 的值
        // 如果被 YkRadioGroup 组件包裹
        if (this.isGroup) {
          // 通知并传参给父组件(YkRadioGroup),并且触发父组件上的 input 事件执行父组件中对应的函数
          this.dispatch("YkRadioGroup", "input", [val]);
        } else {
          // 如果单独的 radio (没被 YkRadioGroup 组件包裹),这里广播一个 input 事件来通过父组件,让父组件去执行它自己的方法,也就是修改在本示例中父组件 v-model 绑定的变量的值
          this.$emit("input", val); // val 参数传给父组件我(子组件)选了哪一个单选按钮(即后面代码中的 this.model = this.label)
        }
      }
    },
    // 判断是否是组单选按钮, 即被 YkRadioGroup 组件包裹着
    isGroup() {
      let parent = this.$parent;
      while (parent) {
        // parent.$options.componentName 中的 componentName 这个是自定义属性,不是 Vue 自带的属性,需要你自己在 YkRadioGroup 组件中添加,不然这里会找不到,切记!
        if (parent.$options.componentName !== "YkRadioGroup") {
          parent = parent.$parent;
        } else {
          this._radioGroup = parent; // 找到 YkRadioGroup 组件时(父组件),保存到子组件的 this._radioGroup 属性上,这里可以把 this._radioGroup 理解成在子组件中父组件的引用
          return true;
        }
      }
      return false;
    },

    isDisable() {
      // 如果被 YkRadioGroup 组件包裹,并且 YkRadioGroup 组件标签上添加有 disabled 属性,则取父组件上的 disabled 状态 ,否则返回子组件的 disabled 状态
      return this.isGroup && this._radioGroup.disabled
        ? this._radioGroup.disabled
        : this.disabled;
    },
    border() {
      return this.isGroup ? this._radioGroup.border : "";
    },
    size() {
      return this.isGroup ? this._radioGroup.size : "";
    }
  },
  methods: {
    // 这里为什么要在 input 框上触发这个方法?网上查了下,也没真正理解它的作用。估计得在以后遇到特定的场景,把它使用上了,才真正理解它。
    // 现在我对 $nextTick() 的理解仅知道它类似一个钩子函数,调用 handleChange() 方法后会在特定的时刻触发。在这个示例中,input 不调用 handleChange() 方法,整个组件也是能正常使用的。
    // 因为这个方法中主要是执行了下 handleChange() 方法中的 dispatch() ,触发 YkRadioGroup 组件上的 handleChange 方法,并把 this.model 传过去。
    handleChange() {
      this.$nextTick(() => {
        console.log("这里Radio");
        this.isGroup &&
          this.dispatch("YkRadioGroup", "handleChange", this.model);
      });
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.yk-radio {
  color: #606266;
  font-weight: 500;
  line-height: 1;
  position: relative;
  cursor: pointer;
  display: inline-block;
  white-space: nowrap;
  outline: none;
  font-size: 14px;
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}
.yk-radio-box {
  white-space: nowrap;
  cursor: pointer;
  outline: none;
  display: inline-block;
  line-height: 1;
  position: relative;
  vertical-align: middle;
}

.yk-radio-icon {
  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;
}

.yk-radio-box.is-checked .yk-radio-icon {
  border-color: #409eff;
  background: #409eff;
}

.yk-radio-box .yk-radio-icon:after {
  width: 4px;
  height: 4px;
  border-radius: 100%;
  background-color: #fff;
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(0);
  transition: transform 0.15s cubic-bezier(0.71, -0.46, 0.88, 0.6);
}

.yk-radio-box.is-checked .yk-radio-icon:after {
  transform: translate(-50%, -50%) scale(1);
}

.yk-radio-input {
  opacity: 0;
  outline: none;
  position: absolute;
  z-index: -1;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
}
.yk-radio-text {
  font-size: 14px;
  padding-left: 10px;
}
.yk-radio-box.is-checked + .yk-radio-text {
  color: #409eff;
}
.yk-radio + .yk-radio {
  margin-left: 30px;
}

.yk-radio-box.is-disabled + span.yk-radio-text {
  color: #c0c4cc;
  cursor: not-allowed;
}

.yk-radio-box.is-disabled .yk-radio-icon {
  background-color: #f5f7fa;
  border-color: #e4e7ed;
  cursor: not-allowed;
}

.yk-radio-box.is-disabled.is-checked .yk-radio-icon {
  background-color: #f5f7fa;
  border-color: #e4e7ed;
}
.yk-radio-box.is-disabled.is-checked .yk-radio-icon:after {
  background-color: #c0c4cc;
}
.yk-radio-box.is-disabled .yk-radio-icon:after {
  cursor: not-allowed;
  background-color: #f5f7fa;
}

.yk-radio-button,
.yk-radio-button-text {
  position: relative;
  display: inline-block;
  outline: none;
}

.yk-radio-button-input {
  opacity: 0;
  outline: none;
  position: absolute;
  z-index: -1;
  left: -999px;
}

.yk-radio-button-input:disabled + .yk-radio-button-text {
  color: #c0c4cc;
  cursor: not-allowed;
  background-image: none;
  background-color: #fff;
  border-color: #ebeef5;
  box-shadow: none;
}

.yk-radio-button-input:disabled:checked + .yk-radio-button-text {
  background-color: #f2f6fc;
}

.yk-radio.is-bordered {
  padding: 12px 20px 0 10px;
  border-radius: 4px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  height: 40px;
}
.yk-radio.is-bordered.is-checked {
  border-color: #409eff;
}

.yk-radio-small.is-bordered {
  padding: 8px 15px 0 10px;
  border-radius: 3px;
  height: 32px;
}
.yk-radio-medium.is-bordered {
  padding: 10px 20px 0 10px;
  border-radius: 4px;
  height: 36px;
}
.yk-radio-mini.is-bordered {
  padding: 6px 15px 0 10px;
  border-radius: 3px;
  height: 28px;
}
.yk-radio.is-bordered.is-disabled {
  cursor: not-allowed;
  border-color: #ebeef5;
}
.yk-radio.is-bordered + .yk-radio.is-bordered {
  margin-left: 10px;
}
</style>

直接看代码就好了,里面有详细的说明。在这里也不用作过多的解释了。

RadioButton.vue
<template>
  <label class="yk-radio-button"
  :class="[
    size ? 'yk-radio-button-'+ size :'',
    { 'is-checked': model === label,
      'is-disabled': isDisable
    }
    ]">
      <input 
          v-model="model" 
          type="radio" 
          :value="label" 
          :disabled="isDisable" 
          :name="name" 
          @change="handleChange"
          class="yk-radio-button-input">
        <span class="yk-radio-button-text">
          <slot></slot>
          <template v-if="!$slots.default">{{label}}</template> 
        </span>
    </label>
</template>
<!-- 如果组件标签里没有传东西,则默认使用 label 值作为名称 这里不清楚 $slots 的可以网上搜一下-->
<script>
import Emitter from "@/mixins/emitter";
export default {
  name: "YkRadio",
  componentName: "YkRadio",
  mixins: [Emitter],
  props: {
    label: {}, // 什么都不填,表示不对变量作限制
    value: {},
    disabled: Boolean,
    name: String
  },
  // 强列建议使用 props 的对象形式来处理父组件传进来的值。(像上面的 disabled 声明了Boolean)
  // props: ["label", "name", "value", "disabled"],
  computed: {
    model: {
      /* 
       * 由于计算属性默认只有 get 方法,所以如果我们想在其它地方设置 model,
       * 我们就需要给 model 属性添加一个 set 方法,此时 get 方法也需要重写下。
      */
      get() {
        return this._radioGroup.value;
      },
      set(val) {
        // this.dispatch("YkRadioGroup", "input", [val]);
        this._radioGroup.$emit("input", val);
        // 这里广播一个 input 事件来通过父组件,让父组件去执行它自己的方法,也就是修改在本示例中父组件 v-model 绑定的变量的值
      }
    },
    // 判断是否是组单选按钮, 即被 YkRadioGroup 组件包裹着
    _radioGroup() {
      let parent = this.$parent;
      while (parent) {
        // parent.$options.componentName 中的 componentName 这个是自定义属性,不是 Vue 自带的属性,需要你自己在 YkRadioGroup 组件中添加,不然这里会找不到,切记!
        if (parent.$options.componentName !== "YkRadioGroup") {
          parent = parent.$parent;
        } else {
          return parent;
        }
      }
      return false;
    },
    size() {
      return this._radioGroup.size || this.disabled;
    },
    isDisable() {
      // YkRadioGroup 组件标签上添加有 disabled 属性,则取父组件上的 disabled 状态 ,否则返回子组件的 disabled 状态
      return this._radioGroup.disabled || this.disabled;
    }
  },
  methods: {
    handleChange() {
      this.$nextTick(() => {
        console.log("这是什么");
        this.isGroup &&
          this.dispatch("YkRadioGroup", "handleChange", this.model);
      });
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.yk-radio-button,
.yk-radio-button-text {
  position: relative;
  display: inline-block;
  outline: none;
}
.yk-radio-button-input {
  opacity: 0;
  outline: none;
  position: absolute;
  z-index: -1;
  left: -999px;
}
.yk-radio-button:first-child .yk-radio-button-text {
  border-left: 1px solid #dcdfe6;
  border-radius: 4px 0 0 4px;
  box-shadow: none !important;
}

.yk-radio-button:last-child .yk-radio-button-text {
  border-right: 1px solid #dcdfe6;
  border-radius: 0 4px 4px 0;
  box-shadow: none !important;
}

.yk-radio-button-text {
  line-height: 1;
  white-space: nowrap;
  vertical-align: middle;
  background: #fff;
  border: 1px solid #dcdfe6;
  font-weight: 500;
  border-left: 0;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  margin: 0;
  cursor: pointer;
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 0;
}

.yk-radio-button-input:checked + .yk-radio-button-text {
  color: #fff;
  background-color: #409eff;
  border-color: #409eff;
  box-shadow: -1px 0 0 0 #409eff;
}

.yk-radio-button-input:disabled + .yk-radio-button-text {
  color: #c0c4cc;
  cursor: not-allowed;
  background-image: none;
  background-color: #fff;
  border-color: #ebeef5;
  box-shadow: none;
}

.yk-radio-button-input:disabled:checked + .yk-radio-button-text {
  background-color: #f2f6fc;
}

.yk-radio-button-medium .yk-radio-button-text {
  padding: 10px 20px;
  font-size: 14px;
  border-radius: 0;
}

.yk-radio-button-small .yk-radio-button-text {
  padding: 9px 15px;
  font-size: 12px;
  border-radius: 0;
}

.yk-radio-button-mini .yk-radio-button-text {
  padding: 7px 15px;
  font-size: 12px;
  border-radius: 0;
}
</style>
RadioGroup.vue
<template>
  <div class="yk-radio-group">
    <slot></slot>
  </div>
</template>

<script>
import Emitter from "@/mixins/emitter";
export default {
  name: "YkRadioGroup",
  componentName: "YkRadioGroup",
  mixins: [Emitter],
  props: {
    value: {},
    border: Boolean,
    size: String,
    disabled: Boolean
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.yk-radio-group {
  display: inline-block;
  line-height: 1;
  vertical-align: middle;
  font-size: 0;
}
</style>

上面的组件中使用了 Vue 的混合模式 mixins 。正如它的英文所代表的意思一样,把东西混合在一起。说得贴切一点就是把一些方法(emitter.js 中写的一些方法)放到了组件实例上。这里的 emitter.js 就相当于一个 jQuery 库,不同点在于 mixins 把这些方法直接挂在了实例对象上,可以通过 this.xx() 来调用而已。emitter.js 文件里面代码直接拷贝官方的。

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) {
        // console.log(parent, componentName, [eventName].concat(params));
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

emitter.js 里给我们提供了两个方法,一个事件广播方法:broadcast() ,一个是事件分发方法:dispatch()。这两个方法更灵活一些,你可以指定接收的组件。比如:dispatch() 中的实现其实就是通过 Vue 官方的 $emit() 方法来实现分发的。只不过把这个 $emit() 的执行上下文改成自定义的了而已。

 

  • 微信扫一扫,赏我

  • 支付宝扫一扫,赏我

声明

原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

本文永久链接:http://yunkus.com/vue-element-parody-radio/

发表评论

电子邮件地址不会被公开。 必填项已用*标注

评论 END