element-ui 单选框 radio 组件源码分析, 也是很常用的一个
单选框组件分为 3 部分
radio-group: 单选组, 适用于多个互斥的选项中选择的场景
radio: 单选
radio-button: 按钮样式的单选
2 可以单独使用, 也可与 1 组合使用, 3 和 1 要组合使用
radio-group
结构
很简单相当于是一个父容器, 并且提供了键盘上下左右选中的方法
- <div
- class="el-radio-group"
- role="radiogroup"
- @keydown="handleKeydown"
- >
- <slot></slot>
- </div>
复制代码
slot 接收的内容就是 radio 或 radio-button 了
script 部分
1. 导入 mixins
import Emitter from 'element-ui/src/mixins/emitter';
复制代码
这是其实就是用到 emitter.js 里的 dispatch 方法(向上找到指定组件并发布指定事件及传递值)
- // 接收组件名, 事件名, 参数
- 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));
- }
复制代码
在 watch 中监听 value 时用到
- watch: {
- // 监听选中值, 向上找到 from-item 组件发布 el.form.change(应该是用于表单验证)
- value(value) {
- this.dispatch('ElFormItem', 'el.form.change', [this.value]);
- }
- }
复制代码
2. 声明 冻结上下左右的 keyCode 组成的对象
- const keyCode = Object.freeze({
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40
- });
复制代码
Object.freeze() 方法可以冻结一个对象, 冻结指的是不能向这个对象添加新的属性, 不能修改其已有属性的值, 不能删除已有属性, 以及不能修改该对象已有属性的可枚举性, 可配置性, 可写性. 该方法返回被冻结的对象.
3. 若 form-item 组件注入属性影响 size(默认为空)
- inject: {
- elFormItem: {
- default: ''
- }
- }
复制代码
size 在 computed 里
- // 最终大小
- computed: {
- _elFormItemSize() {
- return (this.elFormItem || {}).elFormItemSize;
- },
- radioGroupSize() {
- return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
- }
- }
复制代码
4. 生命周期及 watch
- created() {
- // 触发 radio 组件发布的 handleChange 事件拿到选中值, 发布 change 事件暴露选中值
- this.$on('handleChange', value => {
- this.$emit('change', value);
- });
- },
- mounted() {
- // 当 radioGroup 没有默认选项时, 第一个可以选中 Tab 导航
- // 不知为何要这样做
- 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;
- }
- }
复制代码
5.keyDown 事件
- handleKeydown(e) { // 左右上下按键 可以在 radio 组内切换不同选项
- const target = e.target;
- // radio || label
- const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
- const radios = this.$el.querySelectorAll(className);
- const length = radios.length;
- const index = [].indexOf.call(radios, target);
- 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;
- }
- }
复制代码
switch case 语句没有 break 默认向下执行, 所以上左 和 下右 分别只写了一个执行函数和 break(执行相同)
radio
结构
1. 外层 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"
- >
- ...
- </label>
复制代码
role,aria-checked,aria-disabled 三个属性是无障碍页面应用的属性 (读屏软件会用到) 参考 https://www.zhangxinxu.com/wordpress/2012/03/wai-aria-无障碍阅读/
tabindex: 属性规定元素的 tab 键控制次序 ,0 为按照顺序,-1 为不受 tab 控制
@keydown.space: 空格 keydown 事件(可查阅 vue 官网按键修饰符)
2. 内层第一个 span 由 span 和不可见的 input(模拟 radio)组成(筛选框)
- <!-- 单选框 -->
- <span class="el-radio__input"
- :class="{
- 'is-disabled': isDisabled,
- 'is-checked': model === label
- }"
- >
- <span class="el-radio__inner"></span>
- <!-- 不可见 input 模拟 radio -->
- <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>
复制代码
aria-hidden: 也是无障碍页面应用的属性(读屏软件会用到), 为 true 时自动读屏软件会自动跳过, 毕竟这是一个隐藏元素
3. 内层第二个 span 显示(筛选框对应的内容)
- <!-- 单选文字 -->
- <!-- 阻止冒泡 -->
- <span class="el-radio__label" @keydown.stop>
- <!-- 接收到插槽, 显示插槽内容 -->
- <slot></slot>
- <!-- 没有接收到插槽, 显示 label -->
- <template v-if="!$slots.default">{{label}}</template>
- </span>
复制代码
$slots.default : 接收匿名插槽内容
script 部份
1. 引入 mixins
同上 用到的是 mixins 中的 dispatch 方法
- // 用到 mixins 中的 dispatch 方法, 向上寻找对应的组件并发布事件
- import Emitter from 'element-ui/src/mixins/emitter';
复制代码
运用在 input 的 change 事件中
- handleChange() {
- this.$nextTick(() => {
- // 发布 change 事件暴露 model
- this.$emit('change', this.model);
- // 如果被 radio-group 组件嵌套, 向上找到 radio-group 组件发布 handleChange 事件暴露 model
- this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
- });
- }
复制代码
$nextTick: 将回调延迟到下次 DOM 更新循环之后执行
2.provide 和 inject
- // form 注入
- inject: {
- elForm: {
- default: ''
- },
- elFormItem: {
- default: ''
- }
- }
复制代码
同上, 接收 form 组件注入属性, 影响 size 及 disabled. (computed 中可以看到)
3.computed
是否被 radio-group 包裹
- // 向上找 radio-group 组件 有则 true 无则 false
- isGroup() {
- let parent = this.$parent;
- while (parent) {
- if (parent.$options.componentName !== 'ElRadioGroup') {
- parent = parent.$parent;
- } else {
- this._radioGroup = parent;
- return true;
- }
- }
- return false;
- }
复制代码
实现 v-model
- // 实现 v-model
- model: {
- // 取值
- get() {
- // radio-group 的 value 或 value
- return this.isGroup ? this._radioGroup.value : this.value;
- },
- // 赋值
- set(val) {
- // 被 radio-group 组件包裹 radio-group 组件发布 input 事件数组形式暴露值
- if (this.isGroup) {
- this.dispatch('ElRadioGroup', 'input', [val]);
- } else {
- // 没有被 radio-group 组件包裹, 直接发布 input 事件暴露值
- this.$emit('input', val);
- }
- }
- }
复制代码
控制 size,disabled,tabIndex
- _elFormItemSize() {
- return (this.elFormItem || {}).elFormItemSize;
- },
- radioSize() {
- // props 的 size 及 form 注入的 size 及全局配置对象 ($ELEMENT, 此对象由引入时 Vue.use() 传入的默认空对象)的 size
- const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
- // 被 radio-group 组件包裹优先 radioGroupSize
- return this.isGroup
- ? this._radioGroup.radioGroupSize || temRadioSize
- : temRadioSize;
- },
- isDisabled() {
- // 被 radio-group 组件包裹, radioGroup 的 disabled || props 的 disabled || form 注入的 disabled,
- // 未被 radio-group 组件包裹则少第一个条件
- return this.isGroup
- ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
- : this.disabled || (this.elForm || {}).disabled;
- },
- // 控制 tab 是否可以选中
- tabIndex() {
- // 当 tabindex=0 时, 该元素可以用 tab 键获取焦点, 且访问的顺序是按照元素在文档中的顺序来 focus
- // 当 tabindex=-1 时, 该元素用 tab 键获取不到焦点, 但是可以通过 js 获取, 这样就便于我们通过 js 设置上下左右键的响应事件来 focus, 在 widget 内部可以用到.
- // 当 tabindex>=1 时, 该元素可以用 tab 键获取焦点, 而且优先级大于 tabindex=0; 不过在 tabindex>=1 时, 数字越小, 越先定位到.
- return !this.isDisabled ? (this.isGroup ? (this.model === this.label ? 0 : -1) : 0) : -1;
- }
复制代码
radio-button
结构
与 radio 类似, label 是 button 的样式, 少了一个单选框的结构(span),input 模拟 radio 并且不可见, 另一个依旧是显示对应单选框内容的 span
- <label
- class="el-radio-button"
- :class="[
- size ? 'el-radio-button--' + size : '',
- { 'is-active': value === label },
- { 'is-disabled': isDisabled },
- { 'is-focus': focus }
- ]" role="radio" :aria-checked="value === label" :aria-disabled="isDisabled" :tabindex="tabIndex" @keydown.space.stop.prevent="value = isDisabled ? value : label"
- >
- <input
- class="el-radio-button__orig-radio"
- :value="label"
- type="radio"
- v-model="value"
- :name="name"
- @change="handleChange"
- :disabled="isDisabled"
- tabindex="-1"
- @focus="focus = true"
- @blur="focus = false"
- >
- <span
- class="el-radio-button__inner"
- :style="value === label ? activeStyle : null"
- @keydown.stop>
- <slot></slot>
- <template v-if="!$slots.default">{{label}}</template>
- </span>
- </label>
复制代码
script 部分
逻辑与 radio 基本上一样, 来看下有区别的地方
选中时的填充色和边框色
- computed: {
- // radio-group 组件实例
- _radioGroup() {
- let parent = this.$parent;
- // 向上寻找 radio-group 组件 有就返回 radio-group 组件实例 没有返回 false
- while (parent) {
- if (parent.$options.componentName !== 'ElRadioGroup') {
- parent = parent.$parent;
- } else {
- return parent;
- }
- }
- return false;
- },
- activeStyle() {
- // 选中时的填充色和边框色
- return {
- backgroundColor: this._radioGroup.fill || '',
- borderColor: this._radioGroup.fill || '',
- boxShadow: this._radioGroup.fill ? `-1px 0 0 0 ${this._radioGroup.fill}` : '',
- color: this._radioGroup.textColor || ''
- };
- }
复制代码
fill: 是 radio-group 组件的属性(颜色)
来源: https://juejin.im/post/5b9730546fb9a05d00459387