6. Combination of multi-selection box components – CheckboxGroup and Checkbox

In Section 5, we have completed the component Form with data validation function, and this section continues to develop a new component – the combined multi-selection box Checkbox. As a basic component, it can also be integrated in Form and apply its validation rules.

Checkbox component overview

The multi-select box component is also composed of two components: CheckboxGroup and Checkbox. When used alone, only one Checkbox is required, and when used in combination, both are used. Results as shown below:

Used alone, the common scenario is to check to agree to the registration terms when registering. It has only one independent Checkbox component and binds a boolean value. The example is as follows:

<template>
  <i-checkbox v-model="single">Single option</i-checkbox>
</template>
<script>
  export default {<!-- -->
    data () {<!-- -->
      return {<!-- -->
        single: false
      }
    }
  }
</script>

There are many scenarios for combined use, and it is often used when filling out forms. Its structure is as follows:

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

v-model is used on CheckboxGroup, the bound value is an array, and the value of the array is the label bound to the internal Checkbox.

Usage looks much simpler than Form, but there are two technical difficulties:

  • Checkbox should support both single use and combined use scenarios;
  • Other layout components may be nested inside CheckboxGroup and Checkbox.

For the first point, it is necessary to determine whether the parent has a CheckboxGroup when the Checkbox is initialized. If there is, it is used in combination, otherwise it is used alone. As for the second point, you can use the communication method in the previous section, which can be easily solved.

If two components are developed in parallel, it is easy to confuse the logic. Let’s develop an independent Checkbox component first.

Checkbox used alone

When designing a component, we still need to start with its three APIs: prop, event, and slot.

Because you want to use v-model directly on the Checkbox component to bind data bidirectionally, the essential prop is value, and event input, because v-model is essentially a syntactic sugar (if you are not clear about this usage, you can read the last extended reading 1).

In theory, we only need to set value to a Boolean value, that is, true / false, but for scalability, we define two more props: trueValue and falseValue, they allow users to specify what value value uses to determine whether it is selected. Because in actual development, true/false is not directly saved in the database, but 1/0 or other strings. If Boolean is forced to be used, the user will have to convert it again. Such an API design is not very friendly.

In addition, a disabled attribute is required to indicate whether it is disabled.

Custom event events As mentioned above, one input is used to implement v-model syntactic sugar; the other is on-change, which is triggered when selected/unselected. Used to notify the parent that state has changed.

The slot is good to use the default, displaying auxiliary text.

After clarifying the API, first write a basic v-model function, which is similar in most components.

Create a new directory checkbox under src/components, and create two new files checkbox.vue and checkbox-group.vue. Let’s look at Checkbox first:

<!-- 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>

Because value is defined as prop, it can only be modified by the parent, and it cannot be modified by itself. When triggers the change event, that is, when you click to select, you cannot The value is modified by Checkbox, so we define a currentValue in data and bind it to , so Then you can modify currentValue in Checkbox. This is the “idiom” for custom components using v-model .

The code looks very simple, but there are three details that need additional explanation:

  1. The selected control directly uses instead of using div + css to implement the logic and style of the selection. The advantage of this is that using the input element, you The custom component of html is still a built-in basic component of html, and you can use the default behavior and shortcut keys of the browser, that is to say, the browser knows that this is a selection box, but if it is replaced with div + css, the browser does not know what it is ghost. If you think the original input is ugly, it doesn’t matter, it can be beautified with css, but this is not the focus of this booklet, so I won’t introduce it here.
  2. , are wrapped in a element, the advantage of this is that when you click on , the checkbox will also be triggered, otherwise it will only be triggered when the small box is clicked, which is not easy to select and affects the user experience.
  3. currentValue is still a Boolean value (true / false), because it is used by the component Checkbox itself, and users don’t need to care about it, and value can be String, Number or Boolean, depending on trueValue and falseValue definitions.

The v-model implemented now is only from the inside out, that is to say, by clicking the selection, the user will be notified, and the user can modify it manually Prop value , Checkbox does not respond, then continue to add code:

<!-- checkbox.vue, some codes are omitted -->
<script>
  export default {<!-- -->
    watch: {<!-- -->
      value (val) {<!-- -->
        if (val === this.trueValue || val === this.falseValue) {<!-- -->
          this. updateModel();
        } else {<!-- -->
          throw 'Value should be trueValue or falseValue.';
        }
      }
    },
    methods: {<!-- -->
      updateModel () {<!-- -->
        this.currentValue = this.value === this.trueValue;
      }
    }
  }
</script>

We monitor prop value using watch. When the parent modifies it, it will call the updateModel method to synchronously modify the internal currentValue . However, not all values can be modified by the parent, so the if condition is used to judge whether the value modified by the parent meets the setting of trueValue / falseValue, otherwise an error will be thrown.

Checkbox is also a basic form class component, which can be fully integrated into Form, so we use Emitter to dispatch an event to Form when the change event is triggered, so that you can use the Form component in Section 5 for data validation up:

<!-- checkbox.vue, some codes are omitted -->
<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>

So far, Checkbox can be used alone and supports Form data validation. Let’s look at the combination below.

CheckboxGroup used in combination

Friendly reminder: Please read the https://cn.vuejs.org/v2/guide/forms.html#checkbox content of the Vue.js documentation first.

The API of CheckboxGroup is simple:

  • props: value, similar to Checkbox, used for v-model two-way binding data, the format is an array;
  • events: on-change, same as Checkbox;
  • slots: default, used to place Checkbox.

If you write CheckboxGroup, it means that you need to use multiple selection boxes in combination instead of using them alone. Only one of the two modes can be used, and the basis for judging is whether the CheckboxGroup component is used. So in the Checkbox component, we use the findComponentUpward method in the previous section to determine whether the parent component has a CheckboxGroup:

<!-- checkbox.vue, some codes are omitted -->
<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>

When mounted, use the findComponentUpward method to determine whether the parent has a CheckboxGroup component. If so, set group to true and trigger the updateModel method of CheckboxGroup, which will be discussed below Describe what it does.

In the template, we wrote another to distinguish whether it is a group mode. The newly added model data in the data of the Checkbox is actually the value of the parent CheckboxGroup, which will be assigned to the Checkbox in the updateModel method below.

The newly added prop of Checkbox: label is only valid when used in combination. It can be used in conjunction with model. The usage has been introduced in the Vue.js documentation https://cn. vuejs.org/v2/guide/forms.html#Checkboxes.

In the combination mode, if the Checkbox is selected, there is no need to dispatch events to the Form. It should be dispatched in the CheckboxGroup, so make the final modification to the Checkbox:

<!-- checkbox.vue, some codes are omitted -->
<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.currentValue = this.value === this.trueValue;
      },
    },
  }
</script>

The remaining work is to complete the checkbox-gourp.vue file:

<!-- 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,
        children: []
      };
    },
    methods: {<!-- -->
      updateModel (update) {<!-- -->
        this.childrens = findComponentsDownward(this, 'iCheckbox');
        if (this. children) {<!-- -->
          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>

The code is easy to understand, all that needs to be introduced is the updateModel method. It can be seen that updateModel is called in a total of 3 places, two of which are called when the mounted initialization of CheckboxGroup and the value of watch monitoring change; the other is called when the mounted initialization in Checkbox. The function of this method is to find all Checkboxes in the CheckboxGroup through the findComponentsDownward method, and then assign the value of the CheckboxGroup to the model of the Checkbox, and According to the label of the Checkbox, set the selected state of the current Checkbox once. In this way, whether it is selected from the inside out or the data is modified from the outside to the inside, it is two-way binding, and it supports dynamically increasing the number of Checkboxes.

The above is the entire content of the combined multi-select component – CheckboxGroup & amp; Checkbox, I don’t know if you got it!

Two small homework left:

  1. Integrate CheckboxGroup and Checkbox components in Form to complete an example of data validation;
  2. Refer to the code in this section to implement a radio component Radio and RadioGroup.

Conclusion

The simple components you see are actually not simple.

Extended reading

  • How does the v-model directive work in components

Note: Part of the code in this section refers to iView.