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>