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 组件的属性和事件
- 注册组件。
- 完成组件基本的
template
部分。 - 设置组件的绑定值
value/v-model
、Radio
的value
以及Radio
的中文描述。 - 设置禁用属性
disabled
。 - 设置是否显示边框属性
border
。 - 设置
Radio
组件尺寸的size
属性。 - 设置原生
name
属性。 - 支持屏幕阅读器的识别。
- 增加元素
focus
的样式。
三. Radio 组件属性和事件的具体实现
3.1 注册组件
新建一个 radio.vue
文件,设置组件的 name
和 componentName
都为 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
组件:
- 组件内部接收父组件传过来的
label
属性。 - 将
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 的中文描述
实现步骤:
- 用
slot
插槽接收父组件传过来的标签内的内容。 - 如果父组件标签内没有内容,则展示
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 组件:
Radio
组件内部默认使用value
去接收值,并且设置计算属性model
监听value
值的改变,并且为其设置getter
和setter
。在getter
中获取value
的值,在setter
中使用$emit
将input
事件发送给父组件,并且将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>
- 改变选中时给
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
实现步骤:
- 监听父组件传过来的
disabled
属性,并且设置计算属性isDisabled
监听disabled
的改变。
js
<script>
export default {
name: 'ElRadio',
componentName: 'ElRadio',
props: {
// 监听父组件传递的 disabled 属性
disabled: Boolean,
},
computed: {
...
// 设置计算属性 isDisabled 监听 disabled 的改变
isDisabled() {
return this.disabled;
}
},
}
</script>
- 给
Input
标签增加disabled
属性,并且在最外层的label
和Radio
左侧的点击按钮分别增加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
实现步骤:
- 监听父组件传过来的
border
属性。
js
props: {
disabled: Boolean,
}
- 在最外层的
label
上面增加border
样式。
html
<template>
<!-- 整体用 label 标签包裹 -->
<!-- 给最外层的 label 增加 border 样式 -->
<label
class="el-radio"
:class="[
{ 'is-bordered': border },
...
]"
>
...
</label>
</template>
3.3.6 设置Radio组件尺寸的 size 属性
实现步骤:
- 监听从父组件传过来的
size
属性,并设置radioSize
计算属性监听size
的改变。
js
props: {
disabled: Boolean,
}
...
computed: {
radioSize() {
return this.size
},
}
- 判断在设置
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
属性用于表单提交后向服务器传送数据。
实现步骤:
- 监听父组件传过来的
name
属性。
js
props: {
name: String,
}
- 将
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 支持屏幕阅读器识别
屏幕阅读器相关属性介绍:
role
属性:告诉辅助设备(如屏幕阅读器)这个元素扮演的角色,为了增强语义性。aria
开头的属性:可以为元素提供一些信息,以便屏幕阅读器可以正确的解读,使残障人士更容易访问Web内容或Web应用程序。tabindex
属性:为了网页的无障碍访问,表示根据 tab 键控制的元素焦点的次序。tabindex
为负整数,通常设置为-1,此时元素不能通过 tab 键聚焦,可以通过 js 聚焦。tabindex
设置为0,元素可以聚焦,聚焦的顺序是按照元素所处的 DOM 结构决定的。
实现步骤:
- 增加
aria-
开头的属性。
(1)在最外层的 role=radio
的 label
元素上面增加 aria-checked
和 aria-disabled
。
html
// 当 model 与 label 相等时,即为选中
:aria-checked="model === label"
// 当变量 isDisabled 为 true 时,即为禁用
:aria-disabled="isDisabled"
(2)在 Input 元素上面增加 aria-hidden
。
html
// 在屏幕阅读器上面隐藏该元素
aria-hidden="true"
- 增加使用 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
实现步骤:
- 增加
focus
变量,及focus
样式的class
。
html
// 最外层 label 绑定 class
{ 'is-focus': focus },
// input 增加 focus 和 blur 事件
@focus="focus = true"
@blur="focus = false"
data() {
return {
focus: false
};
},
- 有关
focus
样式的源代码处理。
css
&:focus:not(.is-focus):not(:active):not(.is-disabled){ /*获得焦点时 样式提醒*/
box-shadow: 0 0 2px 2px $--radio-button-checked-border-color;
}
四. 整体代码详解
包含 Radio
与 Radio-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>