封装一个简单的 switch 组件

292 阅读5分钟

封装一个简单的 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>
​
​
​

可以看到,测试的四个开关打开和关闭时,得到的值都是正确的

1.png

2.png

使用下方的按钮改变开关的值,也是可以正常运行的。

3.png

4.png

不过这里仍然有两个问题,第一,是在组件内部写了两个 watch,每次修改都会触发,不够精简。第二,通过观察可以发现,直接点击开关切换和通过点击按钮切换开关,watch 组件内部执行的逻辑并不相同,因为直接点击开关,首先是触发currentValuewatch ,而通过点击按钮切换,实际上是先修改组件监听的值,也就是修改 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>
​

5.png

6.png

打开开关触发 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 源码 暂时没看懂,这篇文章说源码是通过 customRefwatchSyncEffect 去实现的,然而我看源码,这部分是 useModel 的内容,不理解,先记录下。

相关文章:

一文搞懂 Vue3 defineModel 双向绑定:告别繁琐代码!

🔥 vue3.4新出了defineModel() 宏,让你轻松实现双向数据绑定🚀🚀