3.23 Switch 开关

简介

开关选择器 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-disabledis-checked

:class="{ 'is-disabled': switchDisabled, 'is-checked': checked }"

计算属性checked判定组件是否打开状态。组件扩展的 value 类型,不仅支持boolean类型,也支持string/number。当使用string/number类型参数时, 需要手动设定activeValueinactiveValue属性值。

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值。 因为

属性 rolearia-checkedaria-disabled用于实现 ARIA无障碍网页应用。

状态描述

组件支持使用文字或者icon图标对状态进行描述。

组件左侧为关闭状态描述元素,只有设置了属性inactiveIconClassinactiveText才会显示。若设置属性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;
} 

圆形滑块使用绝对定位,使用 leftmargin-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-colorbackground-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 使用混合指令 bewhenm 嵌套生成组件样式。

// 生成 .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 {
    // ...
  }
}  

最后更新于