Element-ui 之 Radio 组件的源码分析

此篇文章将从Radio组件提供的属性和事件的角度,逐步分析element-uiRadio组件源代码,从而介绍如何实现一个完整的Radio组件。

一. Radio 组件提供的属性和事件

1.1 属性

以下是 Radio 组件提供的属性:

参数说明类型可选值默认值
value / v-model绑定值string / number / boolean
labelRadio 的 valuestring / number / boolean
disabled是否禁用booleanfalse
border是否显示边框booleanfalse
sizeRadio 的尺寸,仅在 border 为真时有效stringmedium / small / mini
name原生 name 属性string

1.2 事件

事件名称说明回调参数
input绑定值变化时触发的事件选中的 Radio label 值

二. 按照步骤去实现Radio 组件的属性和事件

  1. 注册组件。
  2. 完成组件基本的 template 部分。
  3. 设置组件的绑定值 value/v-modelRadiovalue 以及Radio的中文描述。
  4. 设置禁用属性 disabled
  5. 设置是否显示边框属性 border
  6. 设置 Radio 组件尺寸的 size 属性。
  7. 设置原生 name 属性。
  8. 支持屏幕阅读器的识别。
  9. 增加元素 focus 的样式。

三. Radio 组件属性和事件的具体实现

3.1 注册组件

新建一个 radio.vue 文件,设置组件的 namecomponentName 都为 ElRadio
main.js 文件里面引入这个组件,并且使用 Vue.component 来全局注册这个组件。

radio.vue 文件:

<template>
  <label
    ...
  </label>
</template>
<script>
export default {
   name: 'ElRadio',
   componentName: 'ElRadio',
}

</script>

main.js 注册组件:

import Radio from './components/radio/radio';

const components = [
  Radio,
]

components.forEach(component => {
  Vue.component(component.name, component);

});

3.2 完成组件基本的 template 部分

<template>
  <!-- 整体用 label 标签包裹 -->
  <label
   class="el-radio"
   role="radio"
  >
   <!-- radio 左侧的点击按钮,用 span 做样式,将 input type=radio 放置在 span 标签的下方 -->
   <span class="el-radio__input">
     <span class="el-radio__inner"></span>
     <input
       class="el-radio__original"
       type="radio"
       autocomplete="off"
     />
   </span>
   <!-- radio 右侧的描述 -->
   <span class="el-radio__label">
     ...
   </span>
  </label>
</template>
  • Radio 组件整体用 label 标签包裹。
  • Radio 组件左侧的点击按钮是用 span 标签做样式,将 input type=radio 放置在 span 标签的下方。
  • Radio 组件右侧的描述用 span 标签包裹。

3.3 设置组件的绑定值 value/v-model 、 Radio 的 value 以及Radio的中文描述

3.3.1 设置 Radio 的 value

描述:
将父组件传入的 label 属性作为 Radio 组件中 input type="radio" 标签的 value 属性值。

实现步骤:

父组件:将 label 的值传过去。

<template>
  <div>
   <el-radio label="1">选项1</el-radio>
   <el-radio label="2">选项2</el-radio>
  </div>
</template>

Radio 组件:

  1. 组件内部接收父组件传过来的 label 属性。
  2. type=radio 的单选按钮的 value 设置为 label 的值。

Radio.vue 文件:

<template>
  <!-- 整体用 label 标签包裹 -->
  <label
   class="el-radio"
   role="radio"
  >
   <!-- radio 左侧的点击按钮,用 span 做样式,将 input type=radio 放置在 span 标签的下方 -->
   <span class="el-radio__input">
     <span class="el-radio__inner"></span>
     <!-- 将 type=radio 的单选按钮的 value 设置为 label 的值 -->
     <input
       class="el-radio__original"
       type="radio"
       :value="label"
       autocomplete="off"
     />
   </span>
   <!-- radio 右侧的描述 -->
   <span class="el-radio__label">
    ...
   </span>
  </label>
</template>
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 接收父组件传过来的 label 属性
    label: {}
  }
}
</script>

3.3.2 设置 Radio 的中文描述

实现步骤:

  1. slot 插槽接收父组件传过来的标签内的内容。
  2. 如果父组件标签内没有内容,则展示 label 属性内容。

Radio.vue 文件:

<!-- radio 右侧的描述 -->
<span class="el-radio__label">
  <slot></slot>
  <template v-if="!$slots.default">{{ label }}</template>
</span>
  • $slots 用来访问被插槽分发的内容,$slots.default 包含了所有没有被包含在具名插槽下的节点。

3.3.3 实现绑定值 value/v-model

实现步骤:

父组件:使用 v-model 传入值。

<template>
  <div>
    <el-radio v-model="radio" label="1">选项1</el-radio>
    <el-radio v-model="radio" label="2">选项2</el-radio>
  </div>
</template>
<script>
export default {
  data() {
    return {
      radio: "1"
    }
  }
}
</script>
  • 在自定义组件使用 v-model="radio" 相当于对其绑定了 value 值和 input 方法 :value="radio" @input="radio = arguments[0]"

Radio 组件:

  1. Radio 组件内部默认使用 value 去接收值,并且设置计算属性 model 监听 value 值的改变,并且为其设置 gettersetter。在 getter 中获取 value 的值,在 setter 中使用 $emitinput 事件发送给父组件,并且将 value 值传递给父组件。
<template>
  ...
  <span class="el-radio__input">
    <span class="el-radio__inner"></span>
    <!-- 将 input 的 v-model 设置为 model 变量 -->
    <input
      class="el-radio__original"
      type="radio"
      v-model="model"
      :value="label"
      autocomplete="off"
    />
  </span>
  ...
</template>
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 自定义组件的 v-model 会默认把 value 用作 prop
    value: {},
  },
  computed: {
    model: {
      get() {
        // 获取 value 的值
        return this.value;
      },
      set(val) {
        // 在 setter 中使用 $emit 将 input 事件发送给父组件,并且将 value 值传递给父组件
        this.$emit('input', val)
      }
    }
  }
}
</script>
  1. 改变选中时给 input 增加 checked 属性并增加选中样式,并且让父组件监听到 change 事件。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 当 model 与 label 相等时增加 is-checked 的 class 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-checked': model === label },
    ]"
    role="radio"
  >
    ...
    <!-- 给 Input 增加 change 事件 -->
    <input
      ref="radio"
      class="el-radio__original"
      type="radio"
      v-model="model"
      :value="label"
      @change="handleChange"
      autocomplete="off"
    />
    ...
  </label>
</template>
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 自定义组件的 v-model 会默认把 value 用作 prop
    value: {},
  },
  computed: {
    model: {
      get() {
        // 获取 value 的值
        return this.value;
      },
      set(val) {
        // 在 setter 中使用 $emit 将 input 事件发送给父组件,并且将 value 值传递给父组件
        this.$emit('input', val)
        // 当 model 与 label 相等时,给 radio 增加 checked 属性
        this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
      }
    }
  },
  methods: {
    handleChange() {
      this.$nextTick(() => {
        // 当 Input 触发 change 事件时,触发父组件的change事件,并且将最新的值传递给父组件
        this.$emit('change', this.model);
      })
    }
  }
}
</script>

3.3.4 设置禁用属性 disabled

实现步骤:

  1. 监听父组件传过来的 disabled 属性,并且设置计算属性 isDisabled 监听 disabled 的改变。
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 监听父组件传递的 disabled 属性
    disabled: Boolean,
},
  computed: {
    ...
    // 设置计算属性 isDisabled 监听 disabled 的改变
    isDisabled() {
      return this.disabled;
    }
  },
}
</script>
  1. Input 标签增加 disabled 属性,并且在最外层的 labelRadio 左侧的点击按钮分别增加 disabled 的样式。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 给最外层的 label 增加 disabled 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-disabled': isDisabled },
      ...
    ]"
  >
    <!-- 给点击按钮增加 disabled 样式 -->
    <span class="el-radio__input"
      :class="[
        { 'is-disabled': isDisabled },
        ...
      ]"
    >
      <span class="el-radio__inner"></span>
      <!-- 给 Input 增加 disabled 属性 -->
      <input
        ref="radio"
        class="el-radio__original"
        type="radio"
        v-model="model"
        @change="handleChange"
        :value="label"
        :disabled="isDisabled"
      />
    </span>
    ...
  </label>
</template>

3.3.5 设置是否显示边框属性 border

实现步骤:

  1. 监听父组件传过来的 border 属性。
props: {
  disabled: Boolean,
}
  1. 在最外层的 label 上面增加 border 样式。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 给最外层的 label 增加 border 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-bordered': border },
      ...
    ]"
  >
    ...
  </label>
</template>

3.3.6 设置Radio组件尺寸的 size 属性

实现步骤:

  1. 监听从父组件传过来的 size 属性,并设置 radioSize 计算属性监听 size 的改变。
props: {
  disabled: Boolean,
}
...
computed: {
  radioSize() {
    return this.size
  },
}
  1. 判断在设置 size 属性之前,是否设置了 border 属性,如果设置了将 size 的样式添加到最外层的 label 上面。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 判断在设置 size 属性之前,是否设置了 border 属性,如果设置了将 size 的样式添加到最外层的 label 上面 -->
  <label
    class="el-radio"
    :class="[
      border && radioSize ? 'el-radio--' + radioSize : '',
      ...
    ]"
  >
    ...
  </label>
</template>

3.3.7 设置原生name属性

name 属性可设置或返回单选按钮的名称,name 属性用于表单提交后向服务器传送数据。

实现步骤:

  1. 监听父组件传过来的 name 属性。
props: {
  name: String,
}
  1. name 属性绑定在 input 上。
<!-- 将 name 属性绑定在 input 上 -->
<input
  ref="radio"
  class="el-radio__original"
  type="radio"
  v-model="model"
  @change="handleChange"
  :name="name"
  :value="label"
  :disabled="isDisabled"
  autocomplete="off"
/>

3.3.8 支持屏幕阅读器识别

屏幕阅读器相关属性介绍:

  1. role 属性:告诉辅助设备(如屏幕阅读器)这个元素扮演的角色,为了增强语义性。
  2. aria 开头的属性:可以为元素提供一些信息,以便屏幕阅读器可以正确的解读,使残障人士更容易访问Web内容或Web应用程序。
  3. tabindex 属性:为了网页的无障碍访问,表示根据 tab 键控制的元素焦点的次序。
    • tabindex 为负整数,通常设置为-1,此时元素不能通过 tab 键聚焦,可以通过 js 聚焦。
    • tabindex 设置为0,元素可以聚焦,聚焦的顺序是按照元素所处的 DOM 结构决定的。

实现步骤:

  1. 增加 aria- 开头的属性。

(1)在最外层的 role=radiolabel 元素上面增加 aria-checkedaria-disabled

// 当 model 与 label 相等时,即为选中
:aria-checked="model === label"
// 当变量 isDisabled 为 true 时,即为禁用
:aria-disabled="isDisabled"

(2)在 Input 元素上面增加 aria-hidden

// 在屏幕阅读器上面隐藏该元素
aria-hidden="true"
  1. 增加使用 tab 键控制元素焦点,使用空格键选中的功能。

(1)设置计算属性 tabIndex,监听如果 disabled 不为 true 则可以聚焦,否则不可以聚焦,并且将 tabindex 设置给外层的 label

<label
  :tabindex="tabIndex"
>
</label>
...
computed: {
  tabIndex() {
    return this.isDisabled ? -1 : 0;
  }
}

(2)外层 label 在点击空格时,判断该选项是否是禁用的,如果是禁用的,则 model 不变,否则 model 等于当前 label

<label
  @keydown.space.stop.prevent="model = isDisabled ? model : label"
>
</label>

(3)内层 input 元素的 tabindex 设置为 -1,禁止聚焦。

<input
  tabindex="-1"
</input>

(4)右侧描述禁止键盘事件冒泡。当 Radio 组件传入的 slot 为输入框时,避免点空格的时候,冒泡到上层元素,变为切换选中。

<!-- 禁止键盘事件冒泡 -->
<span class="el-radio__label" @keydown.stop>
  <slot></slot>
  <template v-if="!$slots.default">{{ label }}</template>
</span>

3.3.9 增加元素 focus 的 class

实现步骤:

  1. 增加 focus 变量,及 focus 样式的 class
// 最外层 label 绑定 class
{ 'is-focus': focus },

// input 增加 focus 和 blur 事件
@focus="focus = true"
@blur="focus = false"

data() {
  return {
    focus: false
  };
},
  1. 有关 focus 样式的源代码处理。
&:focus:not(.is-focus):not(:active):not(.is-disabled){ /*获得焦点时 样式提醒*/
  box-shadow: 0 0 2px 2px $--radio-button-checked-border-color;
}

四. 整体代码详解

包含 RadioRadio-Group 组件以及 Form 组件相互作用的部分。

<template>
  <!-- 整体用 label 标签包裹 -->
  <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"
  >
    <!-- radio 左侧的点击按钮,用 span 做样式,将 input type=radio 放置在 span 标签的下方 -->
    <span class="el-radio__input"
      :class="[
        { 'is-disabled': isDisabled },
        { 'is-checked': model === label }
      ]"
    >
      <span class="el-radio__inner"></span>
      <input
        ref="radio"
        class="el-radio__original"
        type="radio"
        aria-hidden="true"
        v-model="model"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :value="label"
        :disabled="isDisabled"
        tabindex="-1"
        autocomplete="off"
      />
    </span>
    <!-- radio 右侧的描述 -->
    <span class="el-radio__label">
      <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() {
      // parent 变量保存组件的父级元素信息
      let parent = this.$parent;
      while (parent) {
        if (parent.$options.componentName !== 'ElRadioGroup') {
          // 如果父级的 componentName 不是 ElRadioGroup 则继续向上查找
          parent = parent.$parent;
        } else {
          // 如果父级的 componentName 是 ElRadioGroup
          // 用变量 _radioGroup 保存父级信息,并且返回 true
          this._radioGroup = parent;
          return true;
        }
      }
      return false;
    },
    // input 绑定的 v-model
    model: {
      get() {
        // 判断是不是按钮组,如果是按钮组,取按钮组的 value 值,否则取当前组件的 value 值
        return this.isGroup ? this._radioGroup.value : this.value;
      },
      set(val) {
        // 当值改变时,判断是不是按钮组,如果是按钮组,则触发按钮组组件的 input 事件,否则触发当前组件的 input 事件
        if (this.isGroup) {
          this.dispatch('ElRadioGroup', 'input', [val]);
        } else {
          this.$emit('input', val);
        }
        // 设置当前 radio 的 checked 属性,如果 model 与 label 相等,则为选中
        this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
      }
    },
    // 获取 Form-Item 组件的 elFormItemSize 变量
    _elFormItemSize() {
      return (this.elFormItem || {}).elFormItemSize;
    },
    // Radio 的尺寸计算
    radioSize() {
      // 设置变量 temRadioSize,优先获取组件内部的 size,再获取 Form-Item 组件的 elFormItemSize
      const temRadioSize = this.size || this._elFormItemSize;
      // 按钮组优先级:group-size > 组件内部size > Form-Item > Form
      // 按钮优先级:组件内部size > Form-Item > Form
      return this.isGroup
          ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;
    },
    // disabled 计算
    isDisabled() {
      // 按钮组优先级:group > 组件内部 disabled > Form
      // 按钮优先级:组件内部 disabled > Form
      return this.isGroup
        ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
        : this.disabled || (this.elForm || {}).disabled;;
    },
    // 判断是否用 tab 键可以聚焦
    tabIndex() {
      // 如果是禁用的,或者是按钮组且当前选中的按钮不是这个,则不能聚焦,否则可以聚焦
      return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
    }
  },
  
  methods: {
    handleChange() {
      this.$nextTick(() => {
        // 给组件增加 change 事件
        this.$emit('change', this.model);
        this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
      })
    }
  }
}
</script>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值