elementui源码学习之仿写一个el-switch

1,904 阅读3分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

本篇文章记录仿写一个el-switch组件细节,从而有助于大家更好理解饿了么ui对应组件具体工作细节。本文是elementui源码学习仿写系列的又一篇文章,后续空闲了会不断更新并仿写其他组件。源码在github上,大家可以拉下来,npm start运行跑起来,结合注释有助于更好的理解。github仓库地址如下:github.com/shuirongshu…

switch组件思考

组件功能作用

switch组件一般是表示开关状态或者两种状态之间的切换,如点击开启网站的夜间模式,或关闭夜间模式。如下图vue官网首页就有这样的操作功能:

1.png

vue官网链接地址:cn.vuejs.org/

组件的结构

switch组件的结构还是比较简单的,主要分为两部分:

  1. switch组件切换小圆点按钮
  2. switch组件切换容器

2.png

组件的实现思路

基本的switch切换布局结构

在实现switch组件的时候,switch组件切换容器 可以直接画一个div去表示,那么 switch组件切换小圆点按钮 我们也需要画一个div吗?其实不用的。

  1. 我们可以使用**伪元素**先画出一个切换小圆点按钮(结合定位)
  2. 然后需要定义一个标识布尔值,用于更改切换组件开启关闭状态
  3. 当状态变化的时候,去更改切换小圆点按钮在左侧或在右侧的位置(通过class),即实现了切换功能
  4. 再加上过渡效果,这样的话,switch组件的切换(开启关闭)就会很丝滑了

开启关闭switch组件的说明文字功能注意事项

如下图:

3.gif

  1. 关于开启时候文字在左侧,关闭时候文字在右侧,也开始可以通过弹性盒样式控制 justifyContent:flex-start / flex-end;,当然动态padding也要加上,详情见代码
  2. 若将文字加入切换框内部,那么就需要让切换框背景容器dom的宽度自适应,即根据内容文字的多少来控制,所以要提到width: fit-content;属性(使用fit-content属性,让宽度随着内容文字多少自适应)

关于 fit-content 详情见官方文档:developer.mozilla.org/en-US/docs/…

给伪元素加上hover效果的写法

给伪元素加悬浮效果是先hover再::after(不要搞反了)

正确写法:.target:hover::after { background-color: red; }

错误写法!!!:.target::after:hover { background-color: red; }

这里举一个例子代码效果图,复制粘贴即可使用,如下:

4.gif

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            padding: 120px;
        }

        .target {
            display: inline-block;
            width: 60px;
            height: 18px;
            background-color: #c4c4c4;
            border-radius: 10px;
            cursor: pointer;
            transition: all 0.3s;
            position: relative;
        }

        /* 使用伪元素画一个小圆点 */
        .target::after {
            content: "";
            position: absolute;
            top: -4px;
            left: -2px;
            border-radius: 50%;
            width: 24px;
            height: 24px;
            border: 1px solid #e9e9e9;
            box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3);
            background-color: #fff;
            transition: all 0.3s;
        }

        /* 给自己加悬浮效果直接写即可 */
        .target:hover {
            background-color: green;
        }

        /* 给伪元素加悬浮效果是先hover再::after(不要搞反了) */
        .target:hover::after {
            background-color: red;
        }
    </style>
</head>

<body>
    <div class="target"></div>
</body>

</html>

关于封装的mySwitch组件的其他的东西,结合笔者的注释,就可以清晰的理解了。这个组件主要还是样式的动态控制。

另,笔者封装的组件暂不搭配el-form的校验使用,后续写到表单校验时,会补上并更新github上的代码仓库中

当然部分写法效果,笔者的方案和官方的还是略有不同,毕竟思路略有不同,也建议读者自己尝试仿写封装哦

封装的组件

效果图

笔者的gif录屏软件不太好,道友朋友们有没有不错的gif录制软件推荐一下 ^_^

GIF 2022-9-2 18-38-21.gif

复制粘贴即可使用哦

使用代码

<template>
  <div>
    <my-divider lineType="dotted" content-position="left">普通使用</my-divider>
    <my-switch @change="change" v-model="flag1"></my-switch>
    <my-switch v-model="flag2"></my-switch>
    <my-divider lineType="dotted" content-position="left"
      >开启关闭文字</my-divider
    >
    <my-switch v-model="flag3" openText="开启啦开启啦" closeText="关闭了"></my-switch>
    <my-switch v-model="flag3" openText="ON" closeText="OFF"></my-switch>
    <my-switch v-model="flag3" openText="✔" closeText="✘"></my-switch>
    <my-divider lineType="dotted" content-position="left"
      >自定义开启关闭背景色</my-divider
    >
    <my-switch
      v-model="flag4"
      active-color="#19be6b"
      inactive-color="#ed4014"
    ></my-switch>
    <my-divider lineType="dotted" content-position="left">禁用</my-divider>
    <my-switch v-model="flag5" disabled></my-switch>
    <my-switch v-model="flag6" disabled></my-switch>
    <br />
    <my-divider lineType="dotted" content-position="left"
      >small切换框</my-divider
    >
    <my-switch
      v-model="flag7"
      active-color="#006CFF"
      inactive-color="#DD6DA6"
      openText="small"
      closeText="switch"
      size="small"
    ></my-switch>
    <my-divider lineType="dotted" content-position="left">big切换框</my-divider>
    <my-switch
      v-model="flag8"
      active-color="#2F2F2F"
      inactive-color="#ddd"
      openText="☾"
      closeText="☼"
      size="big"
    ></my-switch>
  </div>
</template>

<script>
export default {
  data() {
    return {
      flag1: true,
      flag2: false,
      flag3: true,
      flag4: true,
      flag5: false,
      flag6: true,
      flag7: true,
      flag8: true,
    };
  },
  methods: {
    change(val) {
      console.log("切换后的状态", val);
    },
  },
};
</script>

封装代码

参考注释,建议自己封装适合自己公司业务的switch组件

<template>
  <div
    class="mySwitchWrap"
    :class="[disabled ? 'disabledSwitch' : '', size]"
    @click="changeStatus"
  >
    <!-- input标签 -->
    <input
      class="switchInput"
      type="checkbox"
      @change="changeStatus"
      ref="input"
      :true-value="activeValue"
      :false-value="inactiveValue"
      :disabled="disabled"
      @keydown.enter="changeStatus"
    />
    <!-- 主要内容 -->
    <span
      :class="[
        'switchCentre',
        'circleDotLeft',
        isOpen ? 'changeCircleDotRight' : '',
      ]"
      :style="{
        background: computedBackground,
        borderColor: computedBackground,
      }"
    >
      <span
        class="text"
        :style="{
          justifyContent: isOpen ? 'flex-start' : 'flex-end',
          padding: isOpen ? '0 28px 0 8px' : '0 8px 0 28px',
        }"
        >{{ isOpen ? openText : closeText }}</span
      >
    </span>
  </div>
</template>

<script>
export default {
  name: "mySwitch",
  props: {
    openText: String,
    closeText: String,
    // v-model搭配value接收数据,this.$emit("input", val)更新数据
    value: {
      type: Boolean,
      default: false, // 默认false
    },
    // 是否禁用,默认不禁用
    disabled: {
      type: Boolean,
      default: false,
    },
    // switch打开时为true
    activeValue: {
      type: Boolean,
      default: true,
    },
    // switch关闭时为false
    inactiveValue: {
      type: Boolean,
      default: false,
    },
    // 自定义switch打开时背景色
    activeColor: {
      type: String,
      default: "",
    },
    // 自定义switch关闭时背景色
    inactiveColor: {
      type: String,
      default: "",
    },
    // switch切换框的大小
    size: {
      type: String,
      default: "",
    },
  },
  computed: {
    // 是否打开切换框取决于外层传递的v-model的值是否为true
    isOpen() {
      return this.value === this.activeValue;
    },
    computedBackground() {
      // 若传递了激活颜色和未激活颜色,就根据是否开启状态使用传递的颜色
      if ((this.activeColor.length > 0) & (this.inactiveColor.length > 0)) {
        return this.isOpen ? this.activeColor : this.inactiveColor;
      }
      // 没传递就根据开启使用默认的背景色
      else {
        return this.isOpen ? "#409EFF" : "#C0CCDA";
      }
    },
  },
  methods: {
    changeStatus() {
      // 禁用情况下,不做状态更改切换
      if (this.disabled) {
        return;
      }
      // 首先看是否开启,若开启,就传递不开启;若不开启,就传递开启(因为状态切换,取反)
      const val = this.isOpen ? this.inactiveValue : this.activeValue;
      this.$emit("input", val); // 更新外层v-model绑定的值
      this.$emit("change", val); // 抛出一个change事件以供用户使用
    },
  },
};
</script>

<style scoped lang="less">
.mySwitchWrap {
  display: inline-block;
  cursor: pointer;
  font-size: 14px;
  margin: 2px;
  /* 将input标签隐藏起来,宽高都为0,透明度也为0,看不到 */
  .switchInput {
    position: absolute;
    width: 0;
    height: 0;
    opacity: 0;
    margin: 0;
  }
  .switchCentre {
    display: inline-block;
    width: auto;
    height: 20px;
    color: #fff;
    background-color: #c4c4c4;
    border: 1px solid;
    outline: 0;
    border-radius: 10px;
    box-sizing: border-box;
    transition: all 0.3s; // 加上过渡效果
    position: relative;
    .text {
      min-width: 54px; // 设置最小宽度
      width: fit-content; // 使用fit-content属性,让宽度随着内容多少自适应
      height: 100%;
      font-size: 12px;
      display: flex;
      // justify-content: justifyContent; // 注意,这里也是通过:style控制文字靠左还是靠右
      align-items: center;
      transition: all 0.3s; // 加上过渡效果
    }
  }
  // 默认小圆点在左侧的(使用伪元素创建一个小圆点)
  .circleDotLeft::after {
    content: "";
    position: absolute;
    top: -4px;
    left: -2px;
    border-radius: 100%;
    width: 24px;
    height: 24px;
    border: 1px solid #e9e9e9;
    box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3); // 原来小圆点有一点阴影
    background-color: #fff;
    transition: all 0.3s; // 加上过渡效果
  }
  // 当switch打开时,加上类名~通过更改定位left值控制圆点在右侧
  .changeCircleDotRight::after {
    left: 100%;
    margin-left: -24px;
  }
  // 悬浮加重小圆点阴影
  .circleDotLeft:hover::after {
    box-shadow: 0 1px 18px 0 rgba(0, 0, 0, 0.5);
  }
}
// 除了cursor样式的not-allowed还要搭配js判断才禁用到位
.disabledSwitch {
  cursor: not-allowed;
  opacity: 0.48;
}
// 禁用情况下,保持小圆点原有阴影
.disabledSwitch .circleDotLeft:hover::after {
  box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.3);
}
// 小型switch组件做一个缩放
.small {
  zoom: 0.8;
}
// 大型switch组件做一个缩放
.big {
  zoom: 1.6;
}
</style>

注意true-value和false-value是官方自带的搭配v-model属性,其实这里不用也行,大家参考一下antd的组件便可明了。详见:cn.vuejs.org/guide/essen…