Skip to content

二.数据输入(单/复选框)

前言 --> 单/复选框组件特点

单选框组件多个选择框点击选择只能选择一个,选择之前会先清空选择项,然后选中选择项,点击选中的会清空这个选项;复选框组件多个选择项点击选择,会直接选中选择项,点击选中的会清空这个选项

1.目录结构

sh
├── checkbox
   ├── checkbox-button.vue
   ├── checkbox-group.vue
   ├── checkbox.vue
   └── index.js

1.Checkbox 组件概览

多选框组件也是由两个组件组成:CheckboxGroup 和 Checkbox。单独使用时,只需要一个 Checkbox,组合使用时,两者都要用到。

单独使用时,常见的场景有注册时勾选以同意注册条款,它只有一个独立的 Checkbox 组件,并且绑定一个布尔值,示例如下:

vue
<template>
  <i-checkbox v-model="single">单独选项</i-checkbox>
</template>
<script>
export default {
  data() {
    return {
      single: false,
    }
  },
}
</script>

两者结合使用的场景就很多了,填写表单时会经常用到,它的结构如下:

vue
<template>
  <i-checkbox-group v-model="multiple">
    <i-checkbox label="option1">选项 1</i-checkbox>
    <i-checkbox label="option2">选项 2</i-checkbox>
    <i-checkbox label="option3">选项 3</i-checkbox>
    <i-checkbox label="option4">选项 4</i-checkbox>
  </i-checkbox-group>
</template>
<script>
export default {
  data() {
    return {
      multiple: ["option1", "option3"],
    }
  },
}
</script>

v-model用在了 CheckboxGroup 上,绑定的值为一个数组,数组的值就是内部 Checkbox 绑定的 label。

用法看起来比 Form 要简单多,不过也有两个技术难点:

  • Checkbox 要同时支持单独使用和组合使用的场景;
  • CheckboxGroup 和 Checkbox 内可能嵌套其它的布局组件。

对于第一点,要在 Checkbox 初始化时判断是否父级有 CheckboxGroup,如果有就是组合使用的,否则就是单独使用。而第二点,正好可以用上一节的通信方法,很容易就能解决。

两个组件并行开发,很容易理不清逻辑,不妨我们先开发独立的 Checkbox 组件。

2.单独使用的 Checkbox

设计一个组件时,还是要从它的 3 个 API 入手:prop、event、slot。

因为要在 Checkbox 组件上直接使用v-model来双向绑定数据,那必不可少的一个 prop 就是value,还有 eventinput

理论上,我们只需要给value设置为布尔值即可,也就是 true/false,不过为了扩展性,我们再定义两个 props:trueValuefalseValue,它们允许用户指定value用什么值来判断是否选择。因为实际开发中,数据库中并不直接保存 true/false,而是 1/0 或其他字符串,如果强制使用 Boolean,使用者就要再额外转换一次,这样的 API 设计不太友好。

除此之外,还需要一个disabled属性来表示是否禁用。

自定义事件 events 上文已经说了一个input,用于实现v-model语法糖;另一个就是on-change,当选中/取消选中时触发,用于通知父级状态发生了变化。

slot 使用默认的就好,显示辅助文本。

理清楚了 API,先来写一个基础的v-model功能,这在大部分组件中都类似。

src/components下新建目录checkbox,并新建两个文件checkbox.vuecheckbox-group.vue

vue
<!-- checkbox.vue -->
<template>
  <label>
    <span>
      <input
        type="checkbox"
        :disabled="disabled"
        :checked="currentValue"
        @change="change"
      />
    </span>
    <slot></slot>
  </label>
</template>
<script>
export default {
  name: "iCheckbox",
  props: {
    disabled: {
      type: Boolean,
      default: false,
    },
    value: {
      type: [String, Number, Boolean],
      default: false,
    },
    trueValue: {
      type: [String, Number, Boolean],
      default: true,
    },
    falseValue: {
      type: [String, Number, Boolean],
      default: false,
    },
  },
  data() {
    return {
      currentValue: this.value,
    }
  },
  methods: {
    change(event) {
      if (this.disabled) {
        return false
      }

      const checked = event.target.checked
      this.currentValue = checked

      const value = checked ? this.trueValue : this.falseValue
      this.$emit("input", value)
      this.$emit("on-change", value)
    },
  },
}
</script>

因为value被定义为 prop,它只能由父级修改,本身是不能修改的,在<input>触发 change 事件,也就是点击选择时,不能由 Checkbox 来修改这个 value,所以我们在 data 里定义了一个currentValue,并且把它绑定在<input :checked="currentValue">,这样就可以在 Checkbox 内修改currentValue。这是自定义组件使用v-model的“惯用伎俩”。

代码看起来都很简单,但有三个细节需要额外说明:

1.选中的控件,直接使用了<input type="checkbox">,而没有用 div+css 来自己实现选择的逻辑和样式,这样的好处是,使用 input 元素,你的自定义组件任然为 html 内置的基础组件,可以使用浏览器默认的行为和快捷键,也就是说,浏览器知道这是一个选择框,而换成 div+css,浏览器可不知道这是什么鬼。

2.<input><slot>都是包裹在一个<label>元素内的,这样做的好处是,当点击<slot>里的文字时,<input>选框也会被触发,否则只有点击那个小框才会触发,那样不太容易选中,影响用户体验。

3.currentValue任然是布尔值,因为它是组件 Checkbox 自己使用的,对于使用者无需关心,而 value 可以是 String、Number 或 Boolean,这取决于trueValuefalseValue的定义。

现在实现的v-model,只是由内而外的,也就是说,通过点击<input>选择,会通知到使用者,而使用者手动修改了 prop value,Checkbox 是没有做响应的,那继续补充代码:

vue
<script>
export default {
  watch: {
    value(value) {
      if (value === this.trueValue || value === this.falseValue) {
        this.updateMode()
      } else {
        throw `Value should be trueValue of falseValue`
      }
    },
  },
  methods: {
    updateModel() {
      this.currentValue = this.value === this.trueValue
    },
  },
}
</script>

我们对 prop value使用 watch 进行了监听,当父级修改它时,会调用updateModel方法,同步修改内部的currentValue。不过,不是所有的值父级都能修改,所以用 if 条件判断了父级修改的值是否符合 trueValue/falseValue 所设置的,否则会抛错。

Checkbox 是一个基础的表单类组件,它完全可以集成到 Form 里,所以使用 Emitter 在 change 事件触发时,向 Form 派发一个事件,这样你就可以用第 5 节的 Form 组件类做数据校验了:

vue
<script>
import Emitter from "../../mixins/emitter.js"

export default {
  mixins: [Emitter],
  methods: {
    change(event) {
      this.$emit("input", value)
      this.$emit("on-change", value)
      this.dispatch("iFormItem", "on-form-change", value)
    },
  },
}
</script>

至此,Checkbox 已经可以单独使用了,并至此 Fomr 的数据校验

3.组合使用的 CheckboxGroup

CheckboxGroup 的 API 很简单:

  • props:value,与 Checkbox 的类似,用于 v-model 双向绑定数据,格式为数组;
  • event: on-change,同 Checkbox
  • slot:默认,用于放置 Checkbox

如果写了 CheckboxGroup,那就代表你要组合使用多选框,而非单独使用,只能用其一,而判断的依据就是是否用了 CheckboxGroup 组件。

vue
<template>
  <label>
    <span>
      <input
        v-if="group"
        type="checkbox"
        :disabled="disabled"
        :value="label"
        v-model="model"
        @change="change"
      />
      <input
        v-else
        type="checkbox"
        :disabled="disabled"
        :checked="currentValue"
        @change="change"
      />
    </span>
    <slot></slot>
  </label>
</template>
<script>
import { findComponentUpward } from "../../utils/assist.js"

export default {
  name: "iCheckbox",
  props: {
    label: {
      type: [String, Number, Boolean],
    },
  },
  data() {
    return {
      model: [],
      group: false,
      parent: null,
    }
  },
  mounted() {
    this.parent = findComponentUpward(this, "iCheckboxGroup")
    if (this.parent) {
      this.group = true
    }
    if (this.group) {
      this.parent.updateModel(true)
    } else {
      this.updateModel()
    }
  },
}
</script>

在 mounted 时,通过 findComponentUpward 方法,来判断父级是否有 CheckboxGroup 组件,如果有,那将group置为 true,并触发 CheckboxGroup 的updateModel方法,下文会介绍它的作用。

在 template 里,我们又写了一个<input>来区分是否是 group 模式。Checkbox 的 data 里新增加的model数据,其实就是父级 CheckboxGroup 的value,会在下文的updateModel方法里给 Checkbox 赋值。

Checkbox 新增的 prop:label只会在组合使用时有效,组合model来使用。

在组合模式下,Checkbox 选中,就不用对 Form 派发事件了,应该在 CheckboxGroup 中派发,所以对 Checkbox 做最后的修改:

vue
<script>
export default {
  methods: {
    change(event) {
      if (this.disabled) {
        return false
      }
      const checked = event.target.checked
      this.currentValue = checked
      const value = checked ? this.trueValue : this.falseValue
      this.$emit("input", value)
      if (this.group) {
        this.parent.change(this.model)
      } else {
        this.$emit("on-change", value)
        this.dispatch("iFormItem", "on-form-change", value)
      }
    },
    updateModel() {
      this.currenValue = this.value === this.trueValue
    },
  },
}
</script>

剩余的工作,就是完成 checkbox-group.vue 文件

vue
<!-- checkbox-group.vue -->
<template>
  <div>
    <slot></slot>
  </div>
</template>
<script>
import { findComponentsDownward } from "../../utils/assist.js"
import Emitter from "../../mixins/emitter.js"

export default {
  name: "iCheckboxGroup",
  mixins: [Emitter],
  props: {
    value: {
      type: Array,
      default() {
        return []
      },
    },
  },
  data() {
    return {
      currentValue: this.value,
      childrens: [],
    }
  },
  methods: {
    updateModel(update) {
      this.childrens = findComponentsDownward(this, "iCheckbox")
      if (this.childrens) {
        const { value } = this
        this.childrens.forEach((child) => {
          child.model = value

          if (update) {
            child.currentValue = value.indexOf(child.label) >= 0
            child.group = true
          }
        })
      }
    },
    change(data) {
      this.currentValue = data
      this.$emit("input", data)
      this.$emit("on-change", data)
      this.dispatch("iFormItem", "on-form-change", data)
    },
  },
  mounted() {
    this.updateModel(true)
  },
  watch: {
    value() {
      this.updateModel(true)
    },
  },
}
</script>

代码很容易理解,需要介绍的就是updateModel方法。可以看到,一个有 3 个地方调用updateModel,其中两个是 CheckboxGroup 的 moutend 初始化和 watch 监听的 value 变化时调用;另一个是在 Checkbox 里的 mounted 初始化时调用。这个方法的作用就是在 CheckboxGroup 里通过findComponentsDownward方法找到所有的 Checkbox,然后把 CheckboxGroup 的 value,赋值给 Checkbox 的model,并根据 Checkbox 的label,设置一次当前 Checkbox 的选中状态。这样无论是由内而外选择,或由外向内修改数据,都是双向绑定的,而且支持动态增加 Checkbox 的数量。

总结

通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响