一. 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 组件的属性和事件
- 注册组件。
- 组件的基本
template
部分。 - 引入组件的部分。
- 在
Radio
组件里面增加是否是Radio-group
中的Radio
的判断。 value/v-model
的处理。disabled
属性的处理。size
属性的处理。fill
和text-color
属性的处理。- 键盘事件上、下、左、右的处理。
- 屏幕阅读器相关
tabIndex
的处理。
三. Radio-Group 组件属性和事件的具体实现
3.1 注册组件
新建一个 radio-Group.vue
文件,设置组件的 name
和 componentName
都为 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
标签,如果不存在或者 tag
为 component
则默认为 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 的处理
实现步骤:
radio-group
组件使用v-model
绑定值,组件内部默认使用value
接收值。
js
props: {
value: {}
},
- 在
radio.vue
文件中的计算属性model
的getter
方法中,先判断isGroup
是否为true
,如果为true
,获取变量_radioGroup
的value
,否则获取radio
组件的value
。radio-group
组件的value
优先级高于radio
组件的value
。
js
model: {
get() {
return this.isGroup ? this._radioGroup.value : this.value;
},
...
},
- 在
radio.vue
文件中引入Emitter
,用来向上查找radio-group
组件,并在model
的getter
方法中判断isGroup
是否为true
,如果是true
,则查找到ElRadioGroup
的input
事件,并调用。
js
model: {
...
set(val) {
if (this.isGroup) {
this.dispatch('ElRadioGroup', 'input', [val]);
} else {
this.$emit('input', val);
}
...
}
},
- 改变选中时让
radio-group
组件监听到change
事件,在radio
组件的handleChange
方法中增加如果是单选按钮组,则向上查找radio-group
组件的自定义事件handleChange
,并触发该事件。
js
methods: {
handleChange() {
this.$nextTick(() => {
...
this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
});
}
}
- 在
radio-group
组件中增加监听自定义事件handleChange
,并且在handleChange
事件中触发父组件的change
事件。
js
created() {
this.$on('handleChange', value => {
this.$emit('change', value);
});
},
3.5 disabled 属性的处理
实现步骤:
props
监听父组件传入的disabled
属性。
js
props: {
...
disabled: Boolean
}
- 修改
radio.vue
文件中的isDisabled
计算属性,判断isGroup
是否为true
,如果为true
,优先取radio-group
的disabled
属性,然后再取radio
的disabled
属性。
js
computed: {
...
isDisabled() {
return this.isGroup
? this._radioGroup.disabled || this.disabled
: this.disabled;
},
}
3.6 size 属性的处理
实现步骤:
props
监听父组件传入的size
属性。
js
props: {
...
size: String,
}
- 设置计算属性
radioGroupSize
监听size
的改变。
js
computed: {
...
radioGroupSize() {
return this.size;
}
}
- 修改
radio.vue
文件中的radioSize
计算属性,isGroup
是否为true
,如果为true
,优先取radio-group
的radioGroupSize
属性,然后再取radio
的size
属性。
js
computed: {
radioSize() {
return this.isGroup
? this._radioGroup.radioGroupSize || this.size
: this.size;
},
}
3.7 fill 和 text-color 属性的处理
props
监听父组件传入的 fill
和 text-color
,由于 fill
和 text-color
是 radio-button
组件中用到的,所以这里先接收一下传入的属性,讲到 radio-button
组件时再去说明这部分。
js
props: {
fill: String,
textColor: String,
}
3.8 键盘事件上、下、左、右的处理
Chrome 浏览器会自带这个功能,写这部分功能时可以用 safari 浏览器调试。
实现步骤:
- 设置监听的键盘事件对象。
Object.freeze()
方法用于冻结一个对象,冻结的对象不能被新增、删除、修改属性,并且不能修改已有属性的可枚举性、可配置性、可写性。并且该对象的原型也不能被修改。
Object.freeze()
是浅冻结,对于对象里面的对象是无法冻结的。
html
const keyCode = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
});
- 在
radio-group
组件上监听keydown
事件。
html
<component
...
@keydown="handleKeydown"
>
<slot></slot>
</component>
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 的处理
实现步骤:
radio.vue
文件中的计算属性tabIndex
,增加判断如果是按钮组,且该按钮的value
值与按钮组传入的value
值不一致,则不能聚焦。表示按钮组在有选中值的情况下,不能聚焦其余选项。
js
tabIndex() {
return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
- 如果
radio-group
按钮组没有默认的选项时,将第一个radio
的tabIndex
设置为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>