本篇文章记录仿写一个
el-switch
组件细节,从而有助于大家更好理解饿了么ui对应组件具体工作细节。本文是elementui源码学习仿写系列的又一篇文章,后续空闲了会不断更新并仿写其他组件。源码在github上,大家可以拉下来,npm start运行跑起来,结合注释有助于更好的理解。github仓库地址如下:
https://github.com/shuirongshuifu/elementSrcCodeStudy
switch组件思考
组件功能作用
switch组件
一般是表示开关状态或者两种状态之间的切换,如点击开启网站的夜间模式,或关闭夜间模式。如下图vue官网首页就有这样的操作功能:
vue官网链接地址: https://cn.vuejs.org/
组件的结构
switch组件
的结构还是比较简单的,主要分为两部分:
switch组件切换小圆点按钮
switch组件切换容器
组件的实现思路
基本的switch
切换布局结构
在实现switch组件
的时候,switch组件切换容器
可以直接画一个div
去表示,那么 switch组件切换小圆点按钮
我们也需要画一个div
吗?其实不用的。
- 我们可以使用
伪元素
先画出一个切换小圆点按钮
(结合定位) - 然后需要定义一个
标识布尔值
,用于更改切换组件开启关闭状态
- 当状态变化的时候,去更改
切换小圆点按钮
在左侧或在右侧的位置(通过class),即实现了切换功能 - 再加上过渡效果,这样的话,
switch组件
的切换(开启关闭)就会很丝滑了
开启关闭switch组件
的说明文字功能注意事项
如下图:
- 关于开启时候文字在左侧,关闭时候文字在右侧,也开始可以通过弹性盒样式控制
justifyContent:flex-start / flex-end;
,当然动态padding
也要加上,详情见代码 - 若将文字加入切换框内部,那么就需要让切换框背景容器dom的宽度自适应,即根据内容文字的多少来控制,所以要提到
width: fit-content;属性
(使用fit-content属性,让宽度随着内容文字多少自适应)
关于 fit-content 详情见官方文档: https://developer.mozilla.org/en-US/docs/Web/CSS/fit-content
给伪元素加上hover效果的写法
给伪元素加悬浮效果是先hover再::after(不要搞反了)
正确写法:.target:hover::after { background-color: red; }
错误写法!!!:.target::after:hover { background-color: red; }
这里举一个例子代码效果图,复制粘贴即可使用,如下:
<!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录制软件推荐一下 ^_^
复制粘贴即可使用哦
使用代码
<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的组件便可明了。详见:
https://cn.vuejs.org/guide/essentials/forms.html#checkbox-2