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

1.1 属性

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

参数 说明 类型 可选值 默认值
value / v-model 绑定值 string / number / boolean -- --
label Radio 的 value string / number / boolean -- --
disabled 是否禁用 boolean -- false
border 是否显示边框 boolean -- false
size Radio 的尺寸,仅在 border 为真时有效 string medium / 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 文件:

html 复制代码
<template>
  <label
    ...
  </label>
</template>
<script>
export default {
   name: 'ElRadio',
   componentName: 'ElRadio',
}

</script>

main.js 注册组件:

javaScript 复制代码
import Radio from './components/radio/radio';

const components = [
  Radio,
]

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

});

3.2 完成组件基本的 template 部分

html 复制代码
<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 的值传过去。

html 复制代码
<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 文件:

html 复制代码
<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 文件:

html 复制代码
<!-- 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 传入值。

html 复制代码
<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 值传递给父组件。
html 复制代码
<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 事件。
html 复制代码
<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 的改变。
js 复制代码
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 监听父组件传递的 disabled 属性
    disabled: Boolean,
},
  computed: {
    ...
    // 设置计算属性 isDisabled 监听 disabled 的改变
    isDisabled() {
      return this.disabled;
    }
  },
}
</script>
  1. Input 标签增加 disabled 属性,并且在最外层的 labelRadio 左侧的点击按钮分别增加 disabled 的样式。
html 复制代码
<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 属性。
js 复制代码
props: {
  disabled: Boolean,
}
  1. 在最外层的 label 上面增加 border 样式。
html 复制代码
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 给最外层的 label 增加 border 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-bordered': border },
      ...
    ]"
  >
    ...
  </label>
</template>

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

实现步骤:

  1. 监听从父组件传过来的 size 属性,并设置 radioSize 计算属性监听 size 的改变。
js 复制代码
props: {
  disabled: Boolean,
}
...
computed: {
  radioSize() {
    return this.size
  },
}
  1. 判断在设置 size 属性之前,是否设置了 border 属性,如果设置了将 size 的样式添加到最外层的 label 上面。
html 复制代码
<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 属性。
js 复制代码
props: {
  name: String,
}
  1. name 属性绑定在 input 上。
html 复制代码
<!-- 将 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

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

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

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

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

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

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

html 复制代码
<label
  @keydown.space.stop.prevent="model = isDisabled ? model : label"
>
</label>

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

html 复制代码
<input
  tabindex="-1"
</input>

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

html 复制代码
<!-- 禁止键盘事件冒泡 -->
<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
html 复制代码
// 最外层 label 绑定 class
{ 'is-focus': focus },

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

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

四. 整体代码详解

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

html 复制代码
<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>
相关推荐
yqcoder27 分钟前
Vue3 + Vite + Electron + TS 项目构建
前端·javascript·vue.js
ggdpzhk3 小时前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
活宝小娜7 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点7 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow7 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
刚刚好ā8 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
会发光的猪。11 小时前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js
天下代码客11 小时前
【vue】vue中.sync修饰符如何使用--详细代码对比
前端·javascript·vue.js
周全全11 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
ZwaterZ12 小时前
vue el-table表格点击某行触发事件&&操作栏点击和row-click冲突问题
前端·vue.js·elementui·c#·vue