封装一个简单的 switch 组件
最近在写项目的时候,封装了一个很简单的开关组件,想法是这样的,如果用户没有传 valueMap,默认使用字符串 0 1,如果传了 valueMap ,则使用用户传递的值来作为开关返回的参数。代码如下:
<template>
<div class='risk-element-switch'>
<input type='checkbox' :disabled='disabled' v-model='currentValue' />
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number, Boolean],
default: ''
},
valueMap: {
type: Array,
default: () => ['0', '1']
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'on-change'])
const toBoolean = (val) => val===props.valueMap[1];
const toRaw = (val) => props.valueMap[val ? 1 : 0];
const currentValue = ref(toBoolean(props.modelValue))
watch(() => currentValue.value, (val) => {
console.log('watch->currentValue:',val,toRaw(val))
emit('update:modelValue', toRaw(val))
emit('on-change', toRaw(val))
})
watch(() => props.modelValue, (val) => {
console.log('watch->update:modelValue:',val,toBoolean(val))
currentValue.value = toBoolean(val)
})
</script>
<style scoped>
.risk-element-switch {
--base-color: #f58d3c;
--bg-color: #fff4d9;
--base-bg-color: linear-gradient(180deg, #ffc661 2%, #faa54e 96%);
float: right;
input[type="checkbox"] {
border: 1px solid var(--base-color);
appearance: none;
width: 64px;
height: 28px;
position: relative;
border-radius: 16px;
cursor: pointer;
background-color: var(--base-bg-color);
&:before {
content: "";
position: absolute;
width: 26px;
height: 26px;
background: var(--bg-color);
left: 1px;
top: 0;
border-radius: 50%;
transition: left cubic-bezier(0.3, 1.5, 0.7, 1) 0.3s;
}
&:after {
content: "开 关";
text-indent: 12px;
word-spacing: 4px;
display: inline-block;
white-space: nowrap;
color: var(--base-color);
font: 14px/30px monospace;
font-weight: bold;
}
&:checked {
background-color: var(--base-color);
&:before {
left: 36px;
}
&:after {
color: white;
}
}
&:hover:before {
box-shadow: inset 0 0 5px 0 var(--base-color);
}
}
}
</style>
在页面中使用:
<script setup>
import RiskElementSwitch from './risk-element-switch.vue'
/**
* @typedef {Array} testList
* @property {string} ctrlName 控件名称
* @property {string|number|boolean} initValue 初始值
* @property {Array} valueMap 值映射
*/
/**
*
* @type {Reactive<testList>}
*/
const arrList = reactive([
{
ctrlName: '测试1',
initValue: '0',
valueMap: ['0', '1']
},
{
ctrlName: '测试2',
initValue: false,
valueMap: [false, true]
},
{
ctrlName: '测试3',
initValue: 'false',
valueMap: ['false', 'true']
},
{
ctrlName: '测试4',
initValue: 0,
valueMap: [0, 1]
},
])
const changeValue=(index)=>{
const indexMap={
0:1,
1:0
}
arrList[index].initValue=arrList[index].valueMap[indexMap[arrList[index].valueMap.indexOf(arrList[index].initValue)]]
}
/**
* @param {testList} item
*/
const onRiskElementChange = ({ ctrlName, initValue, valueMap }) => {
console.log(ctrlName, initValue, valueMap, 'val--onRiskElementChange')
}
</script>
<template>
<div class='content-element theme-yellow' v-for='(item) in arrList'>
<div class='element-title'>{{ item.ctrlName }}</div>
<div class='element-control'>
<risk-element-switch v-model='item.initValue' :value-map='item.valueMap' @on-change='onRiskElementChange(item)' />
</div>
</div>
<div class='button-box'>
<button @click='changeValue(0)'>按钮1:改变测试1中的值</button>
<button @click='changeValue(1)'>按钮2:改变测试2中的值</button>
<button @click='changeValue(2)'>按钮3:改变测试3中的值</button>
<button @click='changeValue(3)'>按钮4:改变测试4中的值</button>
</div>
</template>
<style scoped>
.content-element {
margin: 5px 20px;
min-height: 40px;
color: #323233;
border-bottom: 1px solid #ebedf0;
display: flex;
align-items: center;
.element-title {
padding: 0 10px;
font-size: 15px;
font-weight: bold;
}
.element-control {
flex: 1;
}
}
.button-box{
display: flex;
flex-direction: column;
margin: 0 auto;
}
</style>
可以看到,测试的四个开关打开和关闭时,得到的值都是正确的
使用下方的按钮改变开关的值,也是可以正常运行的。
不过这里仍然有两个问题,第一,是在组件内部写了两个 watch,每次修改都会触发,不够精简。第二,通过观察可以发现,直接点击开关切换和通过点击按钮切换开关,watch 组件内部执行的逻辑并不相同,因为直接点击开关,首先是触发currentValue 的 watch ,而通过点击按钮切换,实际上是先修改组件监听的值,也就是修改 props.modelValue。那有没有精简一些的方法呢?
实际上,我们可以使用计算属性来实现相同的功能:
<template>
<div class='risk-element-switch'>
<input type='checkbox' :disabled='disabled' v-model='currentValue' />
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: [String, Number, Boolean],
default: ''
},
valueMap: {
type: Array,
default: () => ['1', '0']
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'on-change'])
const currentValue = computed({
get() {
console.log('val--get',props.modelValue, props.modelValue===props.valueMap[1])
return props.modelValue===props.valueMap[1]
},
set(val) {
let value=props.valueMap[val ? 1 : 0];
console.log('val--set',val, value)
emit('update:modelValue', value)
emit('on-change', value)
}
})
</script>
<style scoped>
.risk-element-switch {
--base-color: #f58d3c;
--bg-color: #fff4d9;
--base-bg-color: linear-gradient(180deg, #ffc661 2%, #faa54e 96%);
float: right;
input[type="checkbox"] {
border: 1px solid var(--base-color);
appearance: none;
width: 64px;
height: 28px;
position: relative;
border-radius: 16px;
cursor: pointer;
background-color: var(--base-bg-color);
&:before {
content: "";
position: absolute;
width: 26px;
height: 26px;
background: var(--bg-color);
left: 1px;
top: 0;
border-radius: 50%;
transition: left cubic-bezier(0.3, 1.5, 0.7, 1) 0.3s;
}
&:after {
content: "开 关";
text-indent: 12px;
word-spacing: 4px;
display: inline-block;
white-space: nowrap;
color: var(--base-color);
font: 14px/30px monospace;
font-weight: bold;
}
&:checked {
background-color: var(--base-color);
&:before {
left: 36px;
}
&:after {
color: white;
}
}
&:hover:before {
box-shadow: inset 0 0 5px 0 var(--base-color);
}
}
}
</style>
打开开关触发 set ,访问触发 get,完美实现想要的功能。
不过还有一种方式实现,通过观察发现,以上这种情况实际上完美符合使用 defineModel 的情形,具体可以看官方文档,我们可以将组件内部修改为如下代码,这里需要注意的是,此时的 get 中是有参数的,外部 v-model 的值修改时,触发的是这里的 get,可以使用下方的按钮触发来验证,点击上方的开关时,先触发 set,这时,修改了 modelValue 的值,这会使得组件 v-model的值被改变,这会触发 get 。所以这里的 set 是内部值修改时触发的,get 是外部 v-model 的值变化时触发的。
<template>
<div class='risk-element-switch'>
<input type='checkbox' :disabled='disabled' v-model='modelValue' />
</div>
</template>
<script setup>
const props = defineProps({
valueMap: {
type: Array,
default: () => ['1', '0']
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['on-change'])
const [modelValue] = defineModel({
type: [String, Number, Boolean],
get(value){
console.log('val--get',value, value===props.valueMap[1])
return value===props.valueMap[1]
},
set(value){
let nValue= props.valueMap[value ? 1 : 0]
console.log('val--set',value, nValue)
emit('on-change', nValue)
return nValue
}
})
</script>
<style scoped>
.risk-element-switch {
--base-color: #f58d3c;
--bg-color: #fff4d9;
--base-bg-color: linear-gradient(180deg, #ffc661 2%, #faa54e 96%);
float: right;
input[type="checkbox"] {
border: 1px solid var(--base-color);
appearance: none;
width: 64px;
height: 28px;
position: relative;
border-radius: 16px;
cursor: pointer;
background-color: var(--base-bg-color);
&:before {
content: "";
position: absolute;
width: 26px;
height: 26px;
background: var(--bg-color);
left: 1px;
top: 0;
border-radius: 50%;
transition: left cubic-bezier(0.3, 1.5, 0.7, 1) 0.3s;
}
&:after {
content: "开 关";
text-indent: 12px;
word-spacing: 4px;
display: inline-block;
white-space: nowrap;
color: var(--base-color);
font: 14px/30px monospace;
font-weight: bold;
}
&:checked {
background-color: var(--base-color);
&:before {
left: 36px;
}
&:after {
color: white;
}
}
&:hover:before {
box-shadow: inset 0 0 5px 0 var(--base-color);
}
}
}
</style>
defineModel 源码 暂时没看懂,这篇文章说源码是通过 customRef 和 watchSyncEffect 去实现的,然而我看源码,这部分是 useModel 的内容,不理解,先记录下。
相关文章: