本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、引言
今天也是充满希望的一天呢(摸鱼的一天),闲来无事翻了翻开发中常用的
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>
可以看到我们的组件需要接收value、label、slot插槽这些东西,那么我们的思路就出来了,创建一个基础模板如下
<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>
看效果
盲仔:实现了没有完全实现,你这不是在逗我吗,这就是原生radio!
搞错了!再来,接下来我们来隐藏掉原生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来解决遮盖问题,我们来看下效果如何
一个简单的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>
嗯~~~ 味道很对。接下来回头看看我们的change事件。
既然value和input事件都能够直接在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通过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工具的设计理念等,具有短小精悍的特点。
今天的分享就是这些啦~~
听说喜欢点赞的你,今年年终奖拿到手软😍