UI组件库打造——复选框(Checkbox)

2,244 阅读4分钟

前言

该文章是Vue复习姿势系列的第三篇,分享复选框组件的实现过程。

脚手架请参考第一篇: juejin.cn/post/701798…
组件示例地址: oversnail.github.io/over-snail-…
git地址: github.com/overSnail/o…

第一篇:Vue复习姿势系列之UI组件——按钮(Button)
第二篇:Vue复习姿势系列之UI组件——单选框(Radio)

介绍

基础表单组件,功能是提供一组选项给用户进行多选。

功能实现

1. 基础功能

src/packages目录下新建checkbox文件夹,文件夹内创建checkbox.vueindex.js
src/styles目录下心新建checkbox.scss,并在src/styles/index.scss中引入。

结构上仍然是label元素包裹input[type="checbox"]span元素,一个作为选项框,一个作为选项文本。

checkbox的打勾图标实现思路有两个。

  1. iconfont图标+scale缩放,加缩放的原因是大部分浏览器的最小字体尺寸是12px,无法显示小于12px的字体图标。
  2. 伪元素设置两个相邻边框,然后角度旋转,模仿一个打勾图标出来(element-ui的方案)。 这里采用第二种
// checkbox.vue
<template>
  <label
    class="my-checkbox"
    :class="{
      'my-checkbox-checked': checked,
    }"
  >
    <input type="checkbox" class="my-checkbox-input" @click="handleClick" />
    <span
      class="my-checkbox-icon"
      :class="{
        'my-checkbox-icon-checked': checked,
      }"
    >
    </span>
    <span class="my-checkbox-label">
      <slot>选项一</slot>
    </span>
  </label>
</template>

<script>
export default {
  name: 'MyCheckbox',
  data() {
    return {
      checked: false, // 是否被选中
    }
  },
  props: {
    // v-model的值
    value: {
      type: [Boolean, Number, String],
      default: false,
    },
  },
  watch: {
    value: {
      handler(newVal) {
        this.checked = newVal
      },
      immediate: true,
    },
  },
  methods: {
    /**
     * @description 复选框点击事件
     */
    handleClick() {
      this.checked = !this.checked
      this.$emit('input', this.checked)
      this.$emit('change', this.checked)
    },
  },
}
</script>
// checkbox.scss
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';

@include b(checkbox) {
  box-sizing: border-box;
  display: inline-block;
  cursor: pointer;
  height: 20px;
  font-size: $--font-size-large;
  vertical-align: top;
  margin-right: 20px;
  &-input {
    display: none;
  }
  &-icon {
    display: inline-block;
    box-sizing: border-box;
    position: relative;
    top: 2px;
    width: 14px;
    height: 14px;
    border-radius: 2px;
    border: 1px solid $--border-color;
    &:after {
      content: "";
      position: absolute;
      box-sizing: border-box;
      width: 7px;
      height: 3px;
      border-top: 1px solid #fff;
      border-right: 1px solid #fff;
      left: 3px;
      top: 3.5px;
      transform: rotate(135deg);
    }
    &-checked {
      background: $--color-primary;
      border-color: $--color-primary;
    }
  }
  &-label {
    display: inline-block;
    height: 100%;
    margin-left: 3px;
  }
  &-checked {
    color: $--color-primary;
  }
}
// styles/index.scss
@import "checkbox";

image.png

2. 禁用状态

因为label标签的作用将input的鼠标事件扩散到整个组件上,所以用input[type="checkbox"]的原生disabled属性即可。

// checkbox.vue 省略部分代码
<template>
  <label
    class="my-checkbox"
    :class="{
      'my-checkbox-checked': isChecked,
      'my-checkbox-disabled': disabled
    }"
  >
    <input
      type="checkbox"
      class="my-checkbox-input"
      @click="handleClick"
      :disabled="disabled"
    />
    <span
      class="my-checkbox-icon"
      :class="{
        'my-checkbox-icon-checked': isChecked,
        'my-checkbox-icon-disabled': disabled,
        'my-checkbox-icon-checked-disabled': disabled && isChecked
      }"
    >
    </span>
    <span class="my-checkbox-label">
      <slot>选项一</slot>
    </span>
  </label>
</template>
<script>
export default {
  ......
  props: {
    // 是否禁用该组件
    disabled: {
      type: Boolean,
      default: false,
    },
  }
}
</script>
// checkbox.scss 省略部分代码
......
@include b(checkbox) {
  &-icon {
  ......
    &-checked {
      background: $--color-primary;
      border-color: $--color-primary;
      &-disabled {
        border-color: $--color-disabled;
        &:after {
          border-color: $--color-disabled!important;
          
        }
      }
    }
    &-disabled {
      background-color: #edf2fc;
      &:after {
        border-color: #edf2fc;
      } 
    }
  }
  ......
  &-disabled {
    cursor: not-allowed;
    color: $--color-disabled;
  }
}

image.png

3. 多选框组

  1. 该功能实现方式是创建个checkbox-group组件将checkbox包裹,checkbox功能由父级接管。
  2. 会用组件通信中的$dispatch$broadcast,前者向上派发事件,后者向下广播事件。
  3. vue组件生命周期是由内而外的:父created -> 子created -> 子mounted -> 父mounted,父组件要在created中监听事件,不能在mounted中监听。
  4. checkbox-groupdisabled具体逻辑比较简单,只需根据disabled值来调整checkbox组件内的myDisabled属性即可。而input[type="checkbox"]disabled属性由myDisableddisabled共同决定。
    创建checkbox-group组件:
    src/packages目录下新建checkbox-group文件夹,文件夹内创建checkbox-group.vueindex.js
    src/styles 目录下心新建checkbox-group.scss,并在src/styles/index.scss中引入。
// checkbox-group.vue
<template>
  <div class="my-checkbox-group">
    <slot></slot>
  </div>
</template>

<script>
export default {
  name: 'myCheckboxGroup',
  data() {
    return {
      options: [], // 选项
    }
  },
  props: {
    // 选中值
    value: {
      type: Array,
      default: () => [],
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  watch: {
    value: {
      handler(newVal) {
        this.initValue()
      },
      immediate: true,
    },
    // 禁用状态
    disabled: {
      handler(newVal) {
        this.syncOptionsDisable(newVal)
      },
      immediate: true,
    },
  },
  created() {
    // 监听on-checkbox-add事件,将checkbox实例存到options上
    this.$on('on-checkbox-add', (checkbox) => {
      this.options.push(checkbox)
      this.initValue()
      this.syncOptionsDisable(this.disabled)
    })
    // 监听on-checkbox-remove事件,将checkbox实例从options中移除
    this.$on('on-checkbox-remove', (checkbox) => {
      this.options.splice(this.options.indexOf(checkbox), 1)
      this.syncValue();
    })
    // 监听checkbox的选中事件,做value值同步
    this.$on('on-checkbox-select', () => {
      this.syncValue();
    })
  },
  methods: {
    initValue() {
      this.options.forEach((cell) => {
        cell.checked = this.value.find((d) => d === cell.label)
      })
    },
    /**
     * @description value值同步
     */
    syncValue() {
      let currentValue = this.options.reduce((currentArr, cell) => {
        cell.checked && currentArr.push(cell.label);
        return currentArr;
      }, [])
      this.$emit('input', currentValue)
    },
    /**
     * @description 设置子选项的myDisabled属性
     * @param {Boolean} disabled 是否禁用
     */
    syncOptionsDisable(disabled) {
      this.options.forEach((d) => {
        d.myDisabled = disabled
      })
    },
  },
}
</script>
// checkbox.vue 省略部分代码
<template>
  <label
    class="my-checkbox"
    :class="{
      'my-checkbox-checked': checked,
      'my-checkbox-disabled': disabled || myDisabled
    }"
  >
    <input
      type="checkbox"
      class="my-checkbox-input"
      @click="handleClick"
      :disabled="disabled || myDisabled"
    />
    <span
      class="my-checkbox-icon"
      :class="{
        'my-checkbox-icon-checked': checked,
        'my-checkbox-icon-disabled': disabled || myDisabled,
        'my-checkbox-icon-checked-disabled': (myDisabled || disabled) && checked
      }"
    >
    </span>
    <span class="my-checkbox-label">
      <slot>选项一</slot>
    </span>
  </label>
</template>

<script>

export default {
  ......
  data() {
    return {
      ......
      myDisabled: false, // 是否被禁用,该属性由父级控制
    }
  },
  props: {
    ......
    // 选中状态下的值,在多选时发挥作用。
    label: {
      type: [Boolean, Number, String],
      default: ""
    }
  },
  ......
  mounted() {
    // 通知myCheckboxGroup组件调用on-checkbox-add方法,参数为当前checkbox实例
    this.dispatch('myCheckboxGroup', 'on-checkbox-add', this)
  },
  beforeDestroy() {
    // 移除时,调用myCheckboxGroup组件的on-checkbox-remove方法
    this.dispatch('myCheckboxGroup', 'on-checkbox-remove', this)
  },
  methods: {
    /**
     * @description 复选框点击事件
     */
    handleClick() {
      ......
      this.dispatch('myCheckboxGroup', 'on-checkbox-select', this)
    },
  },
}
</script>

image.png

4. 可选项数量限制

  1. 数量限制具体表现为:当等于最小数量限制时,已选项不能取消;当等于最大数量时,未选项不可点击。
  2. checkbox增加新属性limitDisabled用来控制因父级数量限制而导致的不可使用情况。所以checkbox的禁用功能由disabledmyDisabledlimitDisabled三个属性决定,为了方便,我们采用computed计算属性处理,computed属性为forbidden
checkbox.vue 省略部分代码
<template>
  <label
    class="my-checkbox"
    :class="{
      'my-checkbox-checked': checked,
      'my-checkbox-disabled': forbidden
    }"
  >
    <input
      type="checkbox"
      class="my-checkbox-input"
      @click="handleClick"
      :disabled="forbidden"
    />
    <span
      class="my-checkbox-icon"
      :class="{
        'my-checkbox-icon-checked': checked,
        'my-checkbox-icon-disabled': forbidden,
        'my-checkbox-icon-checked-disabled': forbidden && checked
      }"
    >
    </span>
    <span class="my-checkbox-label">
      <slot>选项一</slot>
    </span>
  </label>
</template>

<script>

export default {
  ......
  data() {
    return {
      ......
      limitDisabled: false, // 是否因为父级数量限制而被禁用
    }
  },
  ......
  computed: {
    forbidden() {
      return (this.disabled || this.myDisabled || this.limitDisabled)
    }
  },
  ......
}
</script>
// checkbox-group.vue 省略部分代码
......
<script>
export default {
  props: {
    ......
    // 限制最多选择数量, -1为不限制
    max: {
      type: Number,
      default: -1,
    },
    // 限制最少选择数量, -1为不限制
    min: {
      type: Number,
      default: -1,
    },
  },
  ......
  methods: {
    initValue() {
      this.options.forEach((cell) => {
        cell.checked = this.value.find((d) => d === cell.label)
      })
      this.$nextTick(() => {
        this.optionsLimit()
      })
    },
    /**
     * @description value值同步
     */
    syncValue() {
      let currentValue = this.options.reduce((currentArr, cell) => {
        cell.checked && currentArr.push(cell.label)
        return currentArr
      }, [])
      this.$emit('input', currentValue)
      this.optionsLimit()
    },
    /**
     * @description 数量限制操作
     */
    optionsLimit() {
      this.$nextTick(() => {
        this.options.forEach((cell) => {
          cell.limitDisabled = false
        })

        const length = this.value.length
        // 最大值限制
        if (length >= this.max && this.max > -1) {
          this.options.forEach((cell) => {
            if (!cell.checked) {
              cell.limitDisabled = true
            }
          })
        }
        // 最小值限制
        if (length <= this.min && this.min > -1) {
          this.options.forEach((cell) => {
            if (cell.checked) {
              cell.limitDisabled = true
            }
          })
        }
      })
    },
  },
}
</script>

image.png

5. 带有边框

  1. 增加sizeborder属性,设计4种尺寸不同的带边框复选框。
// checkbox.vue 省略部分代码
<template>
  <label
    class="my-checkbox"
    :class="{
      'my-checkbox-checked': checked,
      'my-checkbox-disabled': forbidden,
      [`my-checkbox-${size}-border`]: border
    }"
  >
  ......
  </label>
</template>

<script>
// 工具函数,用于判断传入的值是否符合条件
import { oneOf } from '../../utils/assist'
......
export default {
  ......
  props: {
    ......
    // 是否绘制边框
    border: {
      type: Boolean,
      default: false,
    },
    // 尺寸
    size: {
      validator(value) {
        return oneOf(value, ['large', 'medium', 'small', 'mini'])
      },
      type: String,
      default: 'medium',
    },
  },
  ......
}
</script>
// checkbox.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';

@include b(checkbox) {
  ......
  // border相关样式
  &-large-border {
    height: 40px;
    padding: 8px 8px 12px 8px;
    border: 1px solid $--border-color;
    border-radius: 4px;
  }
  &-medium-border {
    height: 36px;
    padding: 6px 8px 10px 8px;
    border: 1px solid $--border-color;
    border-radius: 4px;
  }
  &-small-border {
    height: 32px;
    padding: 4px 8px 8px 8px;
    border: 1px solid $--border-color;
    border-radius: 4px;
    font-size: $--font-size-medium;
  }
  &-mini-border {
    height: 28px;
    padding: 2px 8px 6px 8px;
    border: 1px solid $--border-color;
    border-radius: 4px;
    font-size: $--font-size-medium;
  }
}

image.png

6. 按钮样式

  1. checkbox渲染成按钮样式,也是对css的操作。
  2. button属性设置给checkbox-group,由父级接管该功能。
// checkbox.vue 省略部分代码
<template>
  <label
    class="my-checkbox"
    :class="{
      'my-checkbox-checked': checked,
      'my-checkbox-disabled': forbidden,
      [`my-checkbox-${size}-border`]: border,
      [`my-checkbox-${size}-button`]: button,
      'my-checkbox-checked-button': checked && button,
    }"
  >
  ......
  </label>
</template>

<script>
export default {
  ......
  data() {
    return {
      ......
      button: false, // 是否渲染成按钮样式
    }
  },
  ......
}
</script>

// checkbox.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';

@include b(checkbox) {
  ......
  &-icon {
    ......
    &-button {
      display: none;
    }
  }
  // 按钮样式相关样式
  &-large-button {
    float: left;
    background-color: #fff;
    height: 40px;
    line-height: 38px;
    padding: 0 15px 0 12px;
    margin: 0;
    border: 1px solid $--border-color;
  }
  &-medium-button {
    float: left;
    background-color: #fff;
    height: 36px;
    line-height: 34px;
    padding: 0 15px 0 12px;
    margin: 0;
    border: 1px solid $--border-color;
  }
  &-small-button {
    float: left;
    background-color: #fff;
    height: 32px;
    line-height: 30px;
    padding: 0 15px 0 12px;
    margin: 0;
    border: 1px solid $--border-color;
    font-size: $--font-size-medium;
  }
  &-mini-button {
    float: left;
    background-color: #fff;
    height: 28px;
    line-height: 26px;
    padding: 0 15px 0 12px;
    margin: 0;
    border: 1px solid $--border-color;
    font-size: $--font-size-medium;
  }
}

checkbox-group.vue 省略部分代码<template>
  <div class="my-checkbox-group">
    <slot></slot>
  </div>
</template>

<script>
export default {
  ......
  props: {
    ......
    // 是否启用按钮样式
    button: {
      type: Boolean,
      default: false,
    },
  },
  watch: {
    ......
    // 是否使用按钮样式
    button: {
      handler(newVal) {
        this.syncOptionsButtonStyle(newVal)
      },
      immediate: true,
    },
  },
  created() {
    // 监听on-checkbox-add事件,将checkbox实例存到options上
    this.$on('on-checkbox-add', (checkbox) => {
      ......
      this.syncOptionsButtonStyle(this.button)
    })
  },
  methods: {
    ......
    /**
     * @description 设置子选项的button属性,用以控制按钮样式
     * @param {Boolean} value 是否设置
     */
    syncOptionsButtonStyle(value) {
      this.options.forEach((d) => {
        d.button = value
      })
    },
  },
}
</script>
// checkbox-group.scss
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';

@include b(checkbox-group) {
  overflow: hidden;
  .my-checkbox-large-button,
  .my-checkbox-medium-button,
  .my-checkbox-small-button,
  .my-checkbox-mini-button {
    &:first-child {
      border-top-left-radius: 4px;
      border-bottom-left-radius: 4px;
    }
    &:last-child {
      border-top-right-radius: 4px;
      border-bottom-right-radius: 4px;
    }
  }
}

image.png

结语

相对于单选框而言,复选框大多数的功能与其都是高度一致的。除了可选项数量限制这个功能,这个功能其实是在做数学题,只要掌握了它的表现规律(当等于最小数量限制时,已选项不能取消;当等于最大数量时,未选项不可点击),实现起来就很简单啦。