vue3 封装一个 Switch组件

677 阅读2分钟

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

背景

由于个人的项目需要用的 Switch 组件,又想保持项目的轻量级引用,放弃了组件库的选择,选择了自己手撸一个组件。同时也给暂时还不会在vue3写组件的同学当个模版参考。

环境: vue3 + typescript

最终结果:

image.png

结构

其实我们可以看到样式结构其实非常简单,就是一个div里面包了一个小球。

于是我们可以很简单的写出 html模版

<div
    class="switch border-$default"
    rounded-full
    overflow-hidden
    border
    relative
  >
    <div absolute rounded-full class="switch__button bg-[#fff]"></div>
</div>

外层即为我们的容器,里面的div即为我们的小球 button。

我们在给它 给上我们的样式

.switch {
  width: v-bind(_width);
  height: v-bind(_height);
  transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  background-color: v-bind(_bgColor);

  &:focus {
    box-shadow: 0 0 0 2px rgba(24, 160, 88, 0.2);
  }

  &.active {
    background-color: v-bind(_bgActiveColor);
    .switch__button {
      left: calc(100% - $dotWidth - 1px);
    }
  }

  &__button {
    left: 1px;
    top: 1px;
    width: $dotWidth;
    height: $dotHeight;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  }
}

为了保证组件样式可配置的最大化 其实可以看到,我组件样式的主题其实是痛过外部变量控制的。 于是我进行样式的属性定义。

$dotWidth: calc(v-bind(_height) - 4px);
$dotHeight: calc(v-bind(_height) - 4px);

props

组件最大的可配置性,还是在于组件对外扩展的属性是什么样。

为了保证高自由度,我提供了一下熟悉对外扩展

const props = withDefaults(
  defineProps<{
    size?: 'small' | 'medium' | 'large'
    value?: string | number | boolean
    loading?: boolean
    defaultValue?: string | number | boolean
    disabled?: boolean
    bgColor?: string
    bgActiveColor?: string
  }>(),
  {
    size: 'medium',
    defaultValue: false,
    disabled: false,
    bgColor: 'rgba(0, 0, 0, 0.14)',
    bgActiveColor: '#60bc95'
  }
)

然后将我的属性样式可以传到css中供样式使用(在css中通过v-bind使用) 于是就有了上文出现的变量。

const { _width, _height } = useSize(props.size)
const _bgColor = computed(() => props.bgColor)
const _bgActiveColor = computed(() => props.bgActiveColor)

v-model

在组件中,我们需要对外暴露我们的状态值供外部使用。

为了代码简单,推荐大家使用Vueuse这个库,里面集成了很多种实用的工具方法。

于是我们有:

const emits = defineEmits<{
  (event: 'change', status: boolean): void
  (event: 'update:value', status: boolean): void
}>()

const status = useVModel(props, 'value', emits)

在vue3 的 setup 环境中,我们需要通过defineEmits api 提供组件对外暴露的属性和方法。 例如上面,我就向外抛出了一个 v-modelchange事件。

我们在对组件的禁用规则作出调整。当时disable为 true 时,我们需要切换禁用的样式和进行状态值的更改。 于是我们可以在点击事件中作出判断

const handleClick = () => {
  if (!props.disabled) {
    status.value = !status.value
    emits('change', status.value)
  }
}

然后再对html模版进行优化。

<template>
  <div
    class="switch border-$default"
    :class="[disabled && 'op-50', status && 'active']"
    rounded-full
    overflow-hidden
    border
    relative
    @click="handleClick"
  >
    <div absolute rounded-full class="switch__button bg-[#fff]"></div>
  </div>
</template>

这样我们就大功告成了。

尝试引入

const testRef = ref(true)
<Switch v-model:value="testRef" size="small" />
<p>{{ testRef }}</p>

结果:

image.png

这样就完美实现了。

具体实现可以参考我的项目:具体源码