Introduction to the

If you write an HTML native radio box, it is indeed very simple, but encapsulating a complete radio component is not so simple. Next, we first introduce some principles of Vue radio box, and then analyze Element’s radio box implementation

Native radio Vs Vue radio

Native radio boxes are simple. If we were to implement a male-female radio button group, the code would be the following

<input type="radio" name="sex" value="male"Male checked > < / input > < inputtype="radio" name="sex" value="female"> women < / input >Copy the code

You can add onchange and onclick events to each input to retrieve its value. You can also iterate over all radio input buttons with a single button click. Get the item whose checked property is true, and then get its value notice how you can make a set of radio items mutually exclusive, meaning that only one radio item can be selected at any one time, and that’s what the name property does, by setting the name of some radio buttons to the same value, This is mutually exclusive whereas Vue’s checkboxes are different. Here’s the code

v-model
v-model
name

function genRadioModel (el: ASTElement, value: string, modifiers: ? ASTModifiers) {
  const number = modifiers && modifiers.number
  let valueBinding = getBindingAttr(el, 'value') | |'null'
  valueBinding = number ? `_n(${valueBinding}) ` : valueBinding
  addProp(el, 'checked'.`_q(${value}.${valueBinding}) `)
  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null.true)}Copy the code

GenRadioModel value is the value of the input value and valueBinding value is the value of v-bind:value in the V-model

 <input type="radio" id="jack" value="Jack" v-model="name">
Copy the code

If the example looks like this, then the addProp method puts the checked property value _q(‘Jack’,name) into the property list, where _q is shorthand for looseEqual, which stands for a loose comparison (if an object, converted to a string comparison via js.stringify, If the value of the checkbox is the same as the value of the V-Model, then checked is checked. If the value of the other checkboxes is checked, then checked is checked. So it’s not checked, it has no checked property, so it’s mutually exclusive

Source code analysis

The entire radio component of the source code is not too long, but there are a lot of knowledge, first on the source code, the official website code point here

<template>
  <label
    class="el-radio"
    :class="[ border && radioSize ? 'el-radio--' + radioSize : '', { 'is-disabled': isDisabled }, { 'is-focus': focus }, { 'is-bordered': border }, { 'is-checked': model === label } ]"
    role="radio"
    :aria-checked="model === label"
    :aria-disabled="isDisabled"
    :tabindex="tabIndex"
    @keydown.space.stop.prevent="model = isDisabled ? model : label"
  >
    <span class="el-radio__input"
      :class="{ 'is-disabled': isDisabled, 'is-checked': model === label }"
    >
      <span class="el-radio__inner"></span>
      <input
        class="el-radio__original"
        :value="label"
        type="radio"
        aria-hidden="true"
        v-model="model"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :disabled="isDisabled"
        tabindex="1"
      >
    </span>
    <span class="el-radio__label" @keydown.stop>
      <slot></slot>
      <template v-if=! ""$slots.default">{{label}}</template>
    </span>
  </label>
</template>
<script>
  import Emitter from 'element-ui/src/mixins/emitter';
  export default {
    name: 'ElRadio',
    mixins: [Emitter],
    inject: {
      elForm: {
        default: ' '
      },
      elFormItem: {
        default: ' '
      }
    },
    componentName: 'ElRadio',
    props: {
      value: {},
      label: {},
      disabled: Boolean,
      name: String,
      border: Boolean,
      size: String
    },
    data() {
      return {
        focus: false
      };
    },
    computed: {
      isGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName ! = ='ElRadioGroup') {
            parent = parent.$parent;
          } else {
            this._radioGroup = parent;
            return true; }}return false;
      },
      model: {
        get() {
          return this.isGroup ? this._radioGroup.value : this.value;
        },
        set(val) {
          if (this.isGroup) {
            this.dispatch('ElRadioGroup'.'input', [val]);
          } else {
            this.$emit('input', val); }}},_elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      radioSize() {
        const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
        return this.isGroup
          ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;
      },
      isDisabled() {
        return this.isGroup
          ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
      tabIndex() {
        return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1;
      }
    },
    methods: {
      handleChange() {
        this.$nextTick(() => {
          this.$emit('change', this.model);
          this.isGroup && this.dispatch('ElRadioGroup'.'handleChange', this.model); }); }}}; </script>Copy the code

To analyze the template section, you need to understand the HTML structure of the component first. The above code structure is simplified as follows

<label ... > <span class='el-radio__input'>
        <span class='el-radio__inner'></span>
        <input type='radio'. /> </span> <span class='el-radio__label'>
        <slot></slot>
        <template v-if=! ""$slots.default">{{label}}</template>
    </span>
</label>
Copy the code

We know that native radio labels are ugly and have different styles in different browsers, so we have to implement all the radio button styles ourselves. The general approach is to hide the real input and use div or SPAN to simulate the input tag. The label is placed on the outer layer to extend the range of mouse clicks. Clicking on either text or input can trigger a response, as can be done by binding the ID attribute of input to the for attribute

<input id='t' type='radio'>
<label for='t'> here < / label >Copy the code

The former is called an implicit link, while the latter is a display link. Obviously, the former does not need an ID, so the former must be better. The two inlined spans in the label are arranged horizontally, as shown in the following figure

display:none
visibility:hidden
onlyopacity:0To get there, and that’s one thing to be careful about

Now look at the second span in the label, which is the text we’re filling in

<span class='el-radio__label'>
        <slot></slot>
        <template v-if=! ""$slots.default">{{label}}</template>
</span>
Copy the code

The span is handled, the slot defaults to render the text we have between

and
, notice the template, if we fill in nothing, say we write like this

<el-radio label='1'></el-radio>
Copy the code

The final text is rendered as the value of its label

$slot.default

Label Label analysis

Label Has a bunch of attributes, so let’s look at them one by one

 <label
    class="el-radio"
    :class="[ border && radioSize ? 'el-radio--' + radioSize : '', { 'is-disabled': isDisabled }, { 'is-focus': focus }, { 'is-bordered': border }, { 'is-checked': model === label } ]"
    role="radio"
    :aria-checked="model === label"
    :aria-disabled="isDisabled"
    :tabindex="tabIndex"
    @keydown.space.stop.prevent="model = isDisabled ? model : label"
  >
Copy the code

First of all, class=”el-radio” indicates that the base class of label, class, what’s in it?

@include b(radio) {
  color: $--radio-color;
  font-weight: $--radio-font-weight;
  line-height: 1;
  position: relative;
  cursor: pointer;
  display: inline-block;
  white-space: nowrap;
  outline: none;
  font-size: $--font-size-base;
Copy the code

The second sentence :class indicates the dynamically bound class, whether it is disabled, whether it has focus, whether it has borders, whether it is selected, etc. First check whether to disable the IS-disabled class. Part of the SCSS code is as follows

 .el-radio__inner {
    background-color: $--radio-disabled-input-fill;
    border-color: $--radio-disabled-input-border-color;
    cursor: not-allowed;

    &::after {
      cursor: not-allowed;
      background-color: $--radio-disabled-icon-color;
}
Copy the code

The visible disable class changes the background color, border color and mouse style to disable. Of course, this is only a style ban. How to implement the function ban? This function isDisabled by setting the input property to disabled, which is the real input property in the source code :disabled=”isDisabled”

<input
        class="el-radio__original"
        :value="label"
        type="radio"
        aria-hidden="true"
        v-model="model"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :disabled="isDisabled"
        tabindex="1"
 >
Copy the code

IsDisabled isa calculated property, as shown below

 isDisabled() {
        return this.isGroup
          ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
          : this.disabled || (this.elForm || {}).disabled;
      },
Copy the code

First, the isGroup is used to determine whether you are in a radio group. The radio group is also an Element component, and the code is as follows. The operation is performed by putting a series of radio buttons together to form a box group

 <el-radio-group v-model="radio2">
    <el-radio :label="3"> Alternative </el-radio> <el-radio :label="6"> Alternative </el-radio> <el-radio :label="9"</el-radio> </el-radio-group>Copy the code

IsGroup is a calculated property that first retrieves the parent of the current component and then checks whether its component name is ElRadioGroup (a check box group). If not, it continues to check the parent’s parent, as described in the previous article. This method finds its nearest parent ElRadioGroup component

isGroup() {
        let parent = this.$parent;
        while (parent) {
          if (parent.$options.componentName ! = ='ElRadioGroup') {
            parent = parent.$parent;
          } else {
            this._radioGroup = parent;
            return true; }}return false;
      },
Copy the code

This is normal. After all, the entire box group is disabled, so it is disabled. If only a single box component is disabled, then disabled is its own disabled prop

Disable logic ends, then {‘ IS-focus ‘: Focus}, this means that the label gets the focus of the IS-focus class, which is controlled by focus, and the focus is processed in the @foucus and @blur of the input above. Next, is-bordered controls whether the box has a border through the border property passed in by the user, followed by is-Checked classes that represent the style of the current radio button being checked, controlled by Model ===label, which is a calculated property

model: {
        get() {
          return this.isGroup ? this._radioGroup.value : this.value;
        },
        set(val) {
          if (this.isGroup) {
            this.dispatch('ElRadioGroup'.'input', [val]);
          } else {
            this.$emit('input', val); }}},Copy the code

The getter and setter are defined here, and the getter first checks if it’s in the checkbox component. If it returns a value from the checkbox group, otherwise it’s its own value, and the label is a property passed in by the user that represents the value of the checkbox component. Value is a prop, but there is no value for users to define on the official website. In fact, this is the use of V-model on the component. The official website is introduced as follows

v-bind:value
Therefore, declare a prop called Value inside the radio component so that the value of the user-defined V-Model can be fetched
set
this.$emit('input', val)
dispatch


role="radio"
:aria-checked="model === label"
:aria-disabled="isDisabled"
Copy the code

The role of a non-standard tag is to describe the actual role of a non-standard tag. For example, if you use div as a button, set div role=”button” and the assistive tool will recognize that it is actually a button. Aria stands for Accessible Rich Internet Application, and aria-* is used to describe specific information about this tag in a visual context. Such as:

<div role="checkbox" aria-checked="checked"></div>
Copy the code

The assistifier will know that the div is actually a checkbox role, which is the checked state, followed by

:tabindex="tabIndex"
@keydown.space.stop.prevent="model = isDisabled ? model : label"
Copy the code

Tabindex specifies the order in which the element gets the focus when the TAB key is pressed, also a calculated property

tabIndex() {
    return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1;
}
Copy the code

If it is in the disabled state and tabIndex is -1, you cannot use the TAB key to get the focus of the element. If it is not in the disabled state, you can use the TAB key to get the focus of the element if the radio button is in the check box group component and is selected. Otherwise, you cannot use the TAB key to get the focus. When tabIndex > 0 is switched, the tabIndex = 0 element is switched, and the switch is performed in the order in which it appears

Behind this @ keydown. Space. Stop. Prevent = “model = isDisabled? Model: label” model: label” : label” model: label” : label” : label”

With options

Note the js section of Mixins :[Emitter]. Mixins are a very flexible way to distribute reusable functions in Vue components. Mixin objects can contain any component option. When a component uses a mixin object, all mixin options are mixed with the component’s own options. We’re mixing it with emitters, which means that all components have methods from Emitters. Blending is an array. Let’s go inside Emitters.

export default {
  methods: {
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      while(parent && (! name || name ! == componentName)) { parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName; }}if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); }}};Copy the code

Obviously, methods are mixed in with the dispatch and broadcast methods, so why not just write them in the component’s methods? The reason is that this will increase the amount of code. Since common methods are used in many places, mixing method can reduce the amount of code and realize code reuse. For example, if there are 10 components that need to use these two methods, then only one line of code can be written by mixing each component, which is much simpler.

The mixed methods will be merged with the original methods of the component. If they collide, the methods in the component’s methods will be retained. Then we will look at the Dispatch method, which implements the logic of sending events to the nearest specific parent component. The third parameter is the event parameter, which is an array or a single value. The logic is simple: continuously fetch the parent component to determine whether it is the target component. If it is not, then call $emit on the parent component to trigger the event

parent.$emit.apply(parent, [eventName].concat(params));
Copy the code

Can not write

parent.$emit(eventName,... params)Copy the code

$emit must be called on apply because the event is triggered on the parent component and not at dispatch. No, parent.$emit just gets the emit method and doesn’t say where to call it! Be careful here

And then we look at where the dispatch method is used, and the answer is radio component methods, right

 methods: {
      handleChange() {
        this.$nextTick(() => {
          this.$emit('change', this.model);
          this.isGroup && this.dispatch('ElRadioGroup'.'handleChange', this.model); }); }}Copy the code

The handleChange here is bound to the input within the radio component and fires when the radio button loses focus

<input @change="handleChange". />Copy the code

The button’s native onchange event is triggered when a different radio button is clicked. Here a change event is thrown to the parent because the radio component needs an @change to indicate the event that is triggered when the binding value changes and passes the value of this.model to the user to get the value, as shown in the following code

<el-radio v-model="v" label='1' @change="radioChange"></el-radio>
Copy the code

Then, if the radio component is inside the radio group component, a handleChange event is sent to the parent to tell the radio group component that my value has changed. How else to inform the parent component of its own value!

And then finally, this $nextTick, this is a little tricky, try to get rid of the nextTick, find that radio component, when you click on the new component, it prints out the value of the old component, that’s a problem, $nextTick is used to defer the callback until the next DOM update loop, but why does it get the value of the newly clicked radio component? I don’t understand. I hope someone can explain