简介
开关选择器 Switch
用于表示开关状态/两种状态之间的切换时。本文将分析源码实现,耐心读完,相信会对您有所帮助。组件源码实现详见packages/switch/src/component.vue
。 🔗 组件文档 Switch 🔗 gitee源码 component.vue
组件checkbox
单独使用可以表示两种状态之间的切换,和 switch
类似。两者在于切换 switch
会直接触发状态改变,而 checkbox
一般用于状态标记,需要和提交操作配合。
更多组件剖析详见 👉 📚 Element 2 源码剖析组件总览 。
源码实现
组件的 prop
声明,各属性功能说明详见 官方文档switch#attributes 。
组件的根节点是类名el-switch
的div元素,内部有四个子元素:
type="checkbox"
的input 元素。
关闭时的图标/文字描述(左侧),类名el-switch__label--left
的span 元素。
组件核心,类名el-switch__core
的span 元素。
打开时的图标/文字描述(右侧),类名el-switch__label--right
的span 元素。
<template>
<div
class="el-switch"
:class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
role="switch"
:aria-checked="checked"
:aria-disabled="switchDisabled"
@click.prevent="switchValue"
>
<input class="el-switch__input" type="checkbox">
<span :class="['el-switch__label', 'el-switch__label--left']"></span>
<span class="el-switch__core" ref="core" ></span>
<span :class="['el-switch__label', 'el-switch__label--right']"></span>
</div>
</template>
页面无法看到 input 元素,由其样式.el-switch__input
可知宽/高/透明度都为0。页面无法聚焦到该元素。
.el-switch__input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
根节点
根据组件的禁用状态以及打开状态绑定样式 is-disabled
、is-checked
。
:class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"
计算属性checked
判定组件是否打开状态。组件扩展的 value 类型,不仅支持boolean
类型,也支持string/number
。当使用string/number
类型参数时, 需要手动设定activeValue
和 inactiveValue
属性值。
checked() {
return this.value === this.activeValue;
},
计算属性switchDisabled
返回组件是否禁用。
computed: {
switchDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
// 注入 在form中使用
inject: {
elForm: {
default: ''
}
},
添加点击事件@click="switchValue"
,用于实现组件点击后切换状态(当设置文字描述时,点击文字也能实现状态切换)。事件使用了修饰符 prevent
,当触发的事件调用 event.preventDefault()
,阻止自身默认事件的执行。
方法 switchValue
在非禁用状态下调用handleChange
方法,实现开关状态的切换。
方法 handleChange
中使用this.$emit('input', val);
更新组件v-model值也就是value值。没有看到更新属性value
值的操作。使用this.$emit('change', val);
触发自定义状态变化事件。
switchValue() {
!this.switchDisabled && this.handleChange();
},
handleChange(event) {
const val = this.checked ? this.inactiveValue : this.activeValue;
this.$emit('input', val);
this.$emit('change', val);
// ...
},
组件使用v-model,需要使用$emit('input')
来改变组件v-model值,否则无法更新用户传入的v-model值。 因为
属性 role
、aria-checked
、aria-disabled
用于实现 ARIA无障碍网页应用。
状态描述
组件支持使用文字或者icon图标对状态进行描述。
组件左侧为关闭状态描述元素,只有设置了属性inactiveIconClass
或inactiveText
才会显示。若设置属性inactiveIconClass
会忽略 inactiveText
。
组件右侧为打开状态描述元素,实现逻辑请参考左侧
分析。
<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>
// core
<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>
组件核心
span元素是开关的外层容器,圆形滑块是由:after
伪元素实现。
<span class="el-switch__core" ref="core" :style="{ 'width': coreWidth + 'px' }">
通过CSS设置高度、外边框圆角实现左右半圆效果。
.el-switch__core {
display: inline-block;
position: relative;
height: 20px;
border-radius: 10px;
background: #dcdfe6;
}
定义打开状态下开关背景色。
.el-switch.is-checked .el-switch__core {
border-color: #409eff;
background-color: #409eff;
}
圆形滑块使用绝对定位,使用 left
和margin-left
定义打开状态下滑块位置(最右侧) 。 使用transition: all .3s
定义了滑块动画时间以及背景颜色变化的时间。
.el-switch__core:after {
content: "";
position: absolute;
top: 1px;
left: 1px;
border-radius: 100%;
transition: all 0.3s;
width: 16px;
height: 16px;
background-color: #fff;
}
.el-switch.is-checked .el-switch__core::after {
left: 100%;
margin-left: -17px;
}
属性 coreWidth
默认值为40。若设置了不同状态的开关背景色,在mounted
中会调用方法setBackgroundColor
。将元素样式border-color
、 background-color
更新为当前状态设置的颜色。
props: {
width: {
type: Number,
default: 40
},
},
data() {
return {
coreWidth: this.width
};
},
mounted() {
this.coreWidth = this.width || 40;
if (this.activeColor || this.inactiveColor) {
this.setBackgroundColor();
}
},
methods: {
setBackgroundColor() {
let newColor = this.checked ? this.activeColor : this.inactiveColor;
this.$refs.core.style.borderColor = newColor;
this.$refs.core.style.backgroundColor = newColor;
},
}
input 元素
关于 input 元素的功能和使用场景无法准确分析推测。
<template>
<div class="el-switch" >
<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"
>
// ...
</div>
</template>
<script>
import Focus from 'element-ui/src/mixins/focus';
export default {
mixins: [Focus('input'), Migrating, emitter],
props: {
// ...
name: {
type: String,
default: ''
},
id: String
},
watch: {
checked() {
this.$refs.input.checked = this.checked;
// ...
}
},
methods: {
handleChange(event) {
// ...
this.$nextTick(() => {
// set input's checked property
// in case parent refuses to change component's value
this.$refs.input.checked = this.checked;
});
},
},
mounted() {
// ...
this.$refs.input.checked = this.checked;
}
};
</script>
文档中组件暴露出的 fous
方法就是用于获取input元素的焦点 this.$refs["input"].focus()
。
export default function(ref) {
return {
methods: {
focus() {
this.$refs[ref].focus(); // 获取焦点;
}
}
};
}
将相关代码移除,组件也可正常运行。此处逻辑不做过多赘述!
拾遗
在created
中,进行了数值处理,若是v-model的值不是 activeValue
inactiveValue
任意属性值,使用$emit('input',val)
更新v-model值为inactiveValue
,让组件置位关闭状态。!
和~
运算符让number转变成boolen类型。
created() {
if (!~[this.activeValue, this.inactiveValue].indexOf(this.value)) {
this.$emit('input', this.inactiveValue);
}
},
当设置属性 validateEvent
值为true 时,改变 switch 状态时是否触发表单的校验,触发组件 formItem
的方法 onFieldChange
。具体校验逻辑等到组件Form
分析文章详细介绍。
watch: {
checked() {
// ...
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [this.value]);
}
}
},
// method packages\form\src\form-item.vue
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
// ...
this.$on('el.form.change', this.onFieldChange);
}
},
组件样式
组件样式源码 packages\theme-chalk\src\switch.scss
使用混合指令 b
、e
、when
、m
嵌套生成组件样式。
// 生成 .el-rate
@include b(switch) {
// ...
@include when(disabled) {
// 生成 .el-switch.is-disabled .el-switch__core,.el-switch.is-disabled .el-switch__label
& .el-switch__core,
& .el-switch__label {
// ...
}
}
// 生成 .el-switch__label
@include e(label) {
// ...
// 生成.el-switch__label.is-active
@include when(active) {
// ...
}
// 生成 .el-switch__label--left
@include m(left) {
// ...
}
// 生成 .el-switch__label--right
@include m(right) {
// ...
}
// 生成 .el-switch__label *
& * {
// ...
}
}
// 生成 .el-switch__input
@include e(input) {
// ...
}
// 生成 .el-switch__core
@include e(core) {
// ...
// 生成 .el-switch__core:after
&:after {
// ...
}
}
@include when(checked) {
// 生成 .el-switch.is-checked .el-switch__core
.el-switch__core {
// ...
// 生成 .el-switch.is-checked .el-switch__core::after
&::after {
// ...
}
}
}
// 生成 .el-switch.is-disabled
@include when(disabled) {
// ...
}
@include m(wide) {
.el-switch__label {
&.el-switch__label--left {
// 生成 .el-switch--wide .el-switch__label.el-switch__label--left span
span {
// ...
}
}
&.el-switch__label--right {
// 生成 .el-switch--wide .el-switch__label.el-switch__label--right span
span {
// ...
}
}
}
}
// 过渡样式使用场景未找到
// 生成 .el-switch .label-fade-enter,.el-switch .label-fade-leave-active
& .label-fade-enter,
& .label-fade-leave-active {
// ...
}
}