简介
终于遇到一个简单的组件了,不过这个组件的实现还是和我之前的实现有所不同,下图Element的Switch组件
看着就很简单,是不是呀,官网代码 点此之前自己的实现方式
关于开关组件,之前自己写了一个,其实这个组件是不需要绑定任何click事件的,也就是说js部分几乎可以不写,核心思想就是利用<input type=checkbox>
的checked属性,当鼠标点击input时,会切换其checked属性,这是原生checkbox的特性。下面是自己实现的switch的html
<template>
<!--点击外层label,内层checkbox会发生改变-->
<label class="switch">
<input type="checkbox" v-model="value" />
<!--内层滑动条,圆形按钮是after元素-->
<div class="switch-checkbox"></div>
</label>
</template>
复制代码
里面的input用display:none隐藏掉,<div class="switch-checkbox"></div>
才是滑块的背景,用:after伪元素来模拟里面的圆形滑块,关键是在不写js的情况下如何控制滑块的左右移动,通过checked属性就能办到,如下
input[type="checkbox"] {
//隐藏input
display: none;
//利用input的checked触发滑动动画
&:checked {
//这里的+(相邻兄弟选择器)很重要,否则无法选择到,因为不加+就变成子元素
+.switch-checkbox {
background-color:@activeBgColor;
&:after {
transform: translateX(26px);
background: @activeButtonColor;
opacity: 1!important;
}
}
}
}
复制代码
&:checked情况下用相邻兄弟选择器选择.switch-checkbox类里面的after伪元素,让其的transform发生改变,从而更改滑块的位置,这样就不用写一行js实现滑块的移动
Element的实现方式
先来看switch的html结构
<template>
<div
class="el-switch"
:class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="switchDisabled"
@click="switchValue"
>
<input
class="el-switch__input"
type="checkbox"
@change="handleChange"
ref="input"
:id="id"
:name="name"
:true-value="activeValue"
:false-value="inactiveValue"
:disabled="switchDisabled"
@keydown.enter="switchValue"
>
<!--前面的文字描述-->
<span
:class="['el-switch__label', 'el-switch__label--left', !checked ? 'is-active' : '']"
v-if="inactiveIconClass || inactiveText">
<i :class="[inactiveIconClass]" v-if="inactiveIconClass"></i>
<span v-if="!inactiveIconClass && inactiveText" :aria-hidden="checked">{{ inactiveText }}</span>
</span>
<!--开关核心部分-->
<span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
</span>
<!--后面的文字描述-->
<span
:class="['el-switch__label', 'el-switch__label--right', checked ? 'is-active' : '']"
v-if="activeIconClass || activeText">
<i :class="[activeIconClass]" v-if="activeIconClass"></i>
<span v-if="!activeIconClass && activeText" :aria-hidden="!checked">{{ activeText }}</span>
</span>
</div>
</template>
复制代码
最外层一个div包裹,这是为了当有文字描述时,可以点击文字也触发开关状态改变,注意这个div上绑定了点击事件@click="switchValue"
,这就和自己实现的方式不同了,Element的开关组件写了很多js,目的是能更好的控制一些特性实现,功能更丰富,可以猜到,switchValue这个方法就是切换开关的状态
里面先是一个input,这个input被隐藏掉,css代码如下
@include e(input) {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
&:focus ~ .el-switch__core {
outline: 1px solid $--color-primary;
}
}
复制代码
绝对定位且宽高都为0,也就是说无法点击到该input,然后是3个span并排下来,第一个和最后一个span都是文字描述,如果用户传入文字才显示,否则不显示,中间的span才是核心,很明显这个span是开关的外层椭圆背景,里面的滑块是由after伪元素实现
<span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
</span>
复制代码
看一下el-switch__core类的内容
@include e(core) {
margin: 0;
display: inline-block;
position: relative;
width: $--switch-width;
height: $--switch-height;
border: 1px solid $--switch-off-color;
outline: none;
border-radius: $--switch-core-border-radius;
box-sizing: border-box;
background: $--switch-off-color;
cursor: pointer;
transition: border-color .3s, background-color .3s;
vertical-align: middle;
&:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
border-radius: $--border-radius-circle;
transition: all .3s;
width: $--switch-button-size;
height: $--switch-button-size;
background-color: $--color-white;
}
}
复制代码
开关外层的椭圆背景是display:inline-block且相对定位,因为里面的滑块要绝对定位,:after部分就是一个绝对定位的圆形,transition: all .3s
规定了滑块动画时间以及背景颜色变化的时间,但是换这个&:after只是滑块未激活状态,激活状态的css如下
@include when(checked) {
.el-switch__core {
border-color: $--switch-on-color;
background-color: $--switch-on-color;
&::after {
left: 100%;
margin-left: -$--switch-button-size - 1px;
}
}
}
复制代码
可以看到激活状态下滑块的left值变为100%,相当于移动到了右侧,而外层椭圆形背景的颜色也变化为滑块激活时的颜色
Switch的js逻辑
现在介绍下整个组件的数据传递逻辑,首先先来看用法
<el-switch
v-model="value2"
active-color="#13ce66"
inactive-color="#ff4949">
</el-switch>
复制代码
只需要给该组件的v-model设置一个data中的值即可,当开关开启关闭后整个value2会相应的变化,首先要知道组件的v-model用法,v-model就是@input和:value的简写,因此在组件内部要有一个value作为prop,具体看Vue官网。然后回到Switch,最外层的div绑定了click事件,代码如下
switchValue() {
!this.switchDisabled && this.handleChange();
},
复制代码
当组件不在禁用状态下时触发handleChange方法,handleChange如下
handleChange(event) {
this.$emit('input', !this.checked ? this.activeValue : this.inactiveValue);
this.$emit('change', !this.checked ? this.activeValue : this.inactiveValue);
this.$nextTick(() => {
// set input's checked property
// in case parent refuses to change component's value
this.$refs.input.checked = this.checked;
});
},
复制代码
这里主要看前2句,第一句是emit了一个input,同时将开关最新的值传递出去,这就是组件v-model的用法,必须指定一个$emit('input')
来改变组件上v-model的值,否则无法更新用户传入的v-model,然后第二个$emit
是组件添加的change事件,用于switch 状态发生变化时的回调函数,用户可以在这里面监测开关值改变了这一事件
!this.checked ? this.activeValue : this.inactiveValue
说明了如果不是激活状态,则传递出去activeValue,激活状态的值,这就是在切换状态了。那么this.checked是啥呢?来看一下
computed: {
checked() {
return this.value === this.activeValue;
},
}
复制代码
原来是一个计算属性,当v-model的值和激活状态的值相同时就是checked状态,反之不是,这样当this.$emit('input', !this.checked ? this.activeValue : this.inactiveValue)
后checked这个计算属性也就跟着变化了,那么问题来了,handleClick后是如何控制滑块的动画效果呢?因为这里没有写任何js,
这里通过$refs.input.checked拿到了内置input的checked的值(这里通过setAttribute也可以改,),注意到最外层的div
<div
...
:class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
@click="switchValue"
>
复制代码
这里的class内有个is-checked类,它就是由checked这个计算属性控制,当checked为true时,div就添加这个is-checked类,这个类实际上啥都没有,作用是用来控制div里面滑块span的类以及after伪元素,如下
当有is-checked类时,上述css就被激活,因此改变了滑块背景的背景色和边框色,同时也改变了:after伪元素。handleClick里面的nextTick那里不明白,有这么2句注释,这里将input的checked属性也改变了,是为了防止父组件拒绝修改value,为什么会拒绝修改呢,不太清楚
// set input's checked property
// in case parent refuses to change component's value
复制代码
我试着将switch组件里面的所有input相关的内容都去掉,该组件仍然工作正常,说明input不是必须的,仔细想一下也对,上面的分析和input没有任何关系,组件内维护了activeValue和inactiveValue,通过v-model传入value,然后将value和activeValue相比来确定switch是否checked,确实不需要input
最后看一下created里面的内容
created() {
if (!~[this.activeValue, this.inactiveValue].indexOf(this.value)) {
this.$emit('input', this.inactiveValue);
}
},
复制代码
这里说明当用户传入的v-model的值既不是activeValue也不是inactiveValue时,将inactiveValue传递出去,也就是让组件置位非开启状态,这个~代表按位非运算符,如果[this.activeValue, this.inactiveValue].indexOf(this.value)为-1,则按位非后变为0,再!后变为1,为true,则进if
再说下~~和!!,前者是用来将小数向下取整的操作(~对浮点数进行了截断),后者是用来将值变为bool值