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

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

1.1 属性

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

参数 说明 类型 可选值 默认值
value / v-model 绑定值 string / number / boolean -- --
size 单选框组尺寸,仅对按钮形式的 Radio 或带有边框的 Radio 有效 string medium / small / mini --
disabled 是否禁用 boolean -- false
text-color 按钮形式的 Radio 激活时的文本颜色 string -- #ffffff
fill 按钮形式的 Radio 激活时的填充色和边框色 string -- #409EFF

1.2 事件

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

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

  1. 注册组件。
  2. 组件的基本 template 部分。
  3. 引入组件的部分。
  4. Radio 组件里面增加是否是 Radio-group 中的 Radio 的判断。
  5. value/v-model 的处理。
  6. disabled 属性的处理。
  7. size 属性的处理。
  8. filltext-color 属性的处理。
  9. 键盘事件上、下、左、右的处理。
  10. 屏幕阅读器相关 tabIndex 的处理。

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

3.1 注册组件

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

3.2 组件的基本 template 部分

采用 is 属性来动态渲染组件的标签,并且用插槽接收父组件传过来的标签内内容。

html 复制代码
<component
    :is="_elTag"
    class="el-radio-group"
    role="radiogroup"
  >
    <slot></slot>
</component>

计算属性 _elTag 首先判断 $vnode.data 是否存在 tag 标签,如果不存在或者 tagcomponent 则默认为 div 标签,否则取 $vnode.data 中的 tag 属性。

js 复制代码
computed: {
  _elTag() {
    let tag = (this.$vnode.data || {}).tag;
    if (!tag || tag === 'component') tag = 'div';
    return tag;
  },
},

3.3 在 Radio 组件里面增加是否是 Radio-group 中的 Radio 的判断

radio.vue 文件中增加计算属性 isGroup,判断 radio 是否被包含在 radio-group 组件中,如果被包含在 radio-group 组件中,isGroup 返回 true,否则返回 false

js 复制代码
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;
      }
    }
    // 查找不到则返回 false
    return false;
  },
}

3.4 value/v-model 的处理

实现步骤:

  1. radio-group 组件使用 v-model 绑定值,组件内部默认使用 value 接收值。
js 复制代码
props: {
  value: {}
},
  1. radio.vue 文件中的计算属性 modelgetter 方法中,先判断 isGroup 是否为 true,如果为 true,获取变量 _radioGroupvalue,否则获取 radio 组件的 valueradio-group 组件的 value 优先级高于 radio 组件的 value
js 复制代码
model: {
  get() {
    return this.isGroup ? this._radioGroup.value : this.value;
  },
  ...
},
  1. radio.vue 文件中引入 Emitter,用来向上查找 radio-group 组件,并在 modelgetter 方法中判断 isGroup 是否为 true,如果是 true,则查找到 ElRadioGroupinput 事件,并调用。
js 复制代码
model: {
  ...
  set(val) {
    if (this.isGroup) {
      this.dispatch('ElRadioGroup', 'input', [val]);
    } else {
      this.$emit('input', val);
    }
    ...
  }
},
  1. 改变选中时让 radio-group 组件监听到 change 事件,在 radio 组件的 handleChange 方法中增加如果是单选按钮组,则向上查找 radio-group 组件的自定义事件 handleChange,并触发该事件。
js 复制代码
methods: {
  handleChange() {
    this.$nextTick(() => {
      ...
      this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
    });
  }
}
  1. radio-group 组件中增加监听自定义事件 handleChange,并且在 handleChange 事件中触发父组件的 change 事件。
js 复制代码
created() {
  this.$on('handleChange', value => {
    this.$emit('change', value);
  });
},

3.5 disabled 属性的处理

实现步骤:

  1. props 监听父组件传入的 disabled 属性。
js 复制代码
props: {
  ...
  disabled: Boolean
}
  1. 修改 radio.vue 文件中的 isDisabled 计算属性,判断 isGroup 是否为 true,如果为 true,优先取 radio-groupdisabled 属性,然后再取 radiodisabled 属性。
js 复制代码
computed: {
  ...
  isDisabled() {
    return this.isGroup
      ? this._radioGroup.disabled || this.disabled
      : this.disabled;
  },
}

3.6 size 属性的处理

实现步骤:

  1. props 监听父组件传入的 size 属性。
js 复制代码
props: {
  ...
  size: String,
}
  1. 设置计算属性 radioGroupSize 监听 size 的改变。
js 复制代码
computed: {
  ...
  radioGroupSize() {
    return this.size;
  }
}
  1. 修改 radio.vue 文件中的 radioSize 计算属性,isGroup 是否为 true,如果为 true,优先取 radio-groupradioGroupSize 属性,然后再取 radiosize 属性。
js 复制代码
computed: {
  radioSize() {
    return this.isGroup
      ? this._radioGroup.radioGroupSize || this.size
      : this.size;
  },
}

3.7 fill 和 text-color 属性的处理

props 监听父组件传入的 filltext-color,由于 filltext-colorradio-button 组件中用到的,所以这里先接收一下传入的属性,讲到 radio-button 组件时再去说明这部分。

js 复制代码
props: {
  fill: String,
  textColor: String,
}

3.8 键盘事件上、下、左、右的处理

Chrome 浏览器会自带这个功能,写这部分功能时可以用 safari 浏览器调试。

实现步骤:

  1. 设置监听的键盘事件对象。

Object.freeze() 方法用于冻结一个对象,冻结的对象不能被新增、删除、修改属性,并且不能修改已有属性的可枚举性、可配置性、可写性。并且该对象的原型也不能被修改。

Object.freeze() 是浅冻结,对于对象里面的对象是无法冻结的。

html 复制代码
const keyCode = Object.freeze({
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40
});
  1. radio-group 组件上监听 keydown 事件。
html 复制代码
<component
  ...
  @keydown="handleKeydown"
  >
  <slot></slot>
</component>
  1. handleKeydown() 方法处理键盘的上下左右。
js 复制代码
methods: {
  handleKeydown(e) {
    // 用 target 变量保存事件触发的元素
    const target = e.target;
    // 判断当前触发的是 input 元素还是 label 元素,用元素的属性来获取元素
    const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
    // 获取到当前组件模板根标签下的所有同类型元素,this.$el 指向当前组件模板的根标签
    const radios = this.$el.querySelectorAll(className);
    // 获取元素的整体长度
    const length = radios.length;
    // 获取触发的元素的索引值
    const index = [].indexOf.call(radios, target);
    // 获取到当前组件模板根标签下的全部 role=radio 的元素
    const roleRadios = this.$el.querySelectorAll('[role=radio]');
    switch (e.keyCode) {
      case keyCode.LEFT:
      case keyCode.UP:
        e.stopPropagation();
        e.preventDefault();
        // 按下左或上,则让前一个元素点击并聚焦,如果当前是第一个元素,则让最后一个元素点击并聚焦
        if (index === 0) {
          roleRadios[length - 1].click();
          roleRadios[length - 1].focus();
        } else {
          roleRadios[index - 1].click();
          roleRadios[index - 1].focus();
        }
        break;
      case keyCode.RIGHT:
      case keyCode.DOWN:
        // 按下右或下,则让下一个元素点击并聚焦,如果当前是最后一个元素,则让第一个元素点击并聚焦
        if (index === (length - 1)) {
          // 阻止冒泡
          e.stopPropagation();
          // 阻止默认事件
          e.preventDefault();
          roleRadios[0].click();
          roleRadios[0].focus();
        } else {
          roleRadios[index + 1].click();
          roleRadios[index + 1].focus();
        }
        break;
      default:
        break;
    }
  }
},

3.9 屏幕阅读器相关 tabIndex 的处理

实现步骤:

  1. radio.vue 文件中的计算属性 tabIndex,增加判断如果是按钮组,且该按钮的 value 值与按钮组传入的 value 值不一致,则不能聚焦。表示按钮组在有选中值的情况下,不能聚焦其余选项。
js 复制代码
tabIndex() {
  return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
  1. 如果 radio-group 按钮组没有默认的选项时,将第一个 radiotabIndex 设置为0,代表元素是可聚焦的。表示按钮组在没有默认的选项时,可以将第一个选项设置为可聚焦。
js 复制代码
mounted() {
  const radios = this.$el.querySelectorAll('[type=radio]');
  const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
  if (![].some.call(radios, radio => radio.checked) && firstLabel) {
    firstLabel.tabIndex = 0;
  }
},

四. 整体代码详解

包含 Radio-Group 与 Form 组件相互作用的部分。

html 复制代码
<template>
  <component
    :is="_elTag"
    class="el-radio-group"
    role="radiogroup"
    @keydown="handleKeydown"
  >
    <slot></slot>
  </component>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';

const keyCode = Object.freeze({
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40
});

export default {
  name: 'ElRadioGroup',
  
  mixins: [Emitter],
  
  inject: {
    elFormItem: {
      default: ''
    }
  },
  
  componentName: 'ElRadioGroup',
  
  props: {
    value: {},
    size: String,
    disabled: Boolean
  },
  
  computed: {
    // 计算包裹 radio-group 组件的标签
    _elTag() {
      let tag = (this.$vnode.data || {}).tag;
      if (!tag || tag === 'component') tag = 'div';
      return tag;
    },
    // 获取 Form-Item 组件的 elFormItemSize 变量
    _elFormItemSize() {
      return (this.elFormItem || {}).elFormItemSize;
    },
    // Radio-Group 组件的尺寸计算
    radioGroupSize() {
      // 优先级:group-size > Form-Item > Form
      return this.size || this._elFormItemSize;
    }
  },
  
  created() {
    this.$on('handleChange', value => {
      this.$emit('change', value);
    });
  },
  
  mounted() {
    // 如果 radio-group 按钮组没有默认的选项时,将第一个 radio 的 tabIndex 设置为0。代表元素是可聚焦的
    const radios = this.$el.querySelectorAll('[type=radio]');
    const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
    if (![].some.call(radios, radio => radio.checked) && firstLabel) {
      firstLabel.tabIndex = 0;
    }
  },
  
  methods: {
    handleKeydown(e) {
      // 用 target 变量保存事件触发的元素
      const target = e.target;
      // 判断当前触发的是 input 元素还是 label 元素,用元素的属性来获取元素
      const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
      // 获取到当前组件模板根标签下的所有同类型元素,this.$el 指向当前组件模板的根标签
      const radios = this.$el.querySelectorAll(className);
      // 获取元素的整体长度
      const length = radios.length;
      // 获取触发的元素的索引值
      const index = [].indexOf.call(radios, target);
      // 获取到当前组件模板根标签下的全部 role=radio 的元素
      const roleRadios = this.$el.querySelectorAll('[role=radio]');
      switch (e.keyCode) {
        case keyCode.LEFT:
        case keyCode.UP:
          e.stopPropagation();
          e.preventDefault();
          // 按下左或上,则让前一个元素点击并聚焦,如果当前是第一个元素,则让最后一个元素点击并聚焦
          if (index === 0) {
            roleRadios[length - 1].click();
            roleRadios[length - 1].focus();
          } else {
            roleRadios[index - 1].click();
            roleRadios[index - 1].focus();
          }
          break;
        case keyCode.RIGHT:
        case keyCode.DOWN:
          // 按下右或下,则让下一个元素点击并聚焦,如果当前是最后一个元素,则让第一个元素点击并聚焦
          if (index === (length - 1)) {
            // 阻止冒泡
            e.stopPropagation();
            // 阻止默认事件
            e.preventDefault();
            roleRadios[0].click();
            roleRadios[0].focus();
          } else {
            roleRadios[index + 1].click();
            roleRadios[index + 1].focus();
          }
          break;
        default:
          break;
      }
    }
  },
  watch: {
    value(value) {
      // 监听 value 值的变化,当 value 值改变时,向 Form-Item 组件派发 el.form.change 自定义事件
      this.dispatch('ElFormItem', 'el.form.change', [value]);
    }
  }
}
</script>
相关推荐
midsummer_woo7 小时前
基于springboot+vue+mysql的中药实验管理系统设计与实现(源码+论文+开题报告)
vue.js·spring boot·mysql
haaaaaaarry9 小时前
Vue常见指令
前端·javascript·vue.js
paopaokaka_luck9 小时前
基于SpringBoot+Vue的汽车租赁系统(协同过滤算法、腾讯地图API、支付宝沙盒支付、WebsSocket实时聊天、ECharts图形化分析)
vue.js·spring boot·后端·websocket·算法·汽车·echarts
不讲道理的柯里昂9 小时前
Vue导出Html为Word中包含图片在Microsoft Word显示异常问题
vue.js·html·word
代码老y10 小时前
Vue3 从 0 到 ∞:Composition API 的底层哲学、渲染管线与生态演进全景
前端·javascript·vue.js
小离a_a10 小时前
vue实现el-table-column中自定义label
前端·javascript·vue.js
爱宇阳10 小时前
Vue3 中使用 Element Plus 实现自定义按钮的 ElNotification 提示框
前端·javascript·vue.js
伍哥的传说11 小时前
Vue3 Anime.js超级炫酷的网页动画库详解
开发语言·前端·javascript·vue.js·vue·ecmascript·vue3
霸道流氓气质12 小时前
Vue中使用vue-3d-model实现加载3D模型预览展示
前端·javascript·vue.js
追光的栗子13 小时前
vue3+vite 项目中怎么引入 elementplus 组件库
前端·vue.js·element