Vue2 Element 模仿秀-单选框篇(Radio)
前言
这里 Vue Element 模仿秀的第二篇:单选框。经过第一篇的洗礼,对 Element 组件库有了基本的了解,并慢慢地爱上了 Element 模仿秀,在模仿的过程当中,真的可以学到很多东西,有一种豁然开朗,柳暗花明又一村的感觉,下面进入正题。
在开始之前,我们不妨先来看看模仿秀的示例演示:
效果图跟官方的基本保持一致,功能也非常接近。下面我们就来深入代码内部,看看 Element 源码的大致实现思路,代码我也没有完事按照官方的来,有些我觉得暂时没有必要的就去掉了。在代码中的很多地方我都作了注释。不过仅代码个人的可能是偏面的法,请多多指教。
简单的文件结构:
├── index.html
├── main.js
├── App.vue
├── mixins
│ └── emitter.js
└── components
├── Home.vue # 应用组件的地方
├── Radio.vue
├── RadioButton.vue
└── RadioGroup.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 属性就可以了。
<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>
直接看代码就好了,里面有详细的说明。在这里也不用作过多的解释了。
<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>
<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 文件里面代码直接拷贝官方的。
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() 的执行上下文改成自定义的了而已。
-
微信扫一扫,赏我
-
支付宝扫一扫,赏我