一、基础回顾
el-radio是单选组件,是对原生<input type="radio">
的封装。
先来回顾原生的radio单选,比如做一个单选题的单选按钮
<input type="radio" name ="fruit" value="apple" checked>大苹果
<input type="radio" name ="fruit" value="banana">大香蕉
checked
:表示被选中的
value
: 表示单选选项的值
name
: 定义 input 元素的名称,具有相同name可以达到互斥的效果,表示同一时刻只能有一个按钮被选中
获取value的值,可以通过click、onchange等事件遍历元素,checked=true的那一项的value值,就是在最后选中的value值。
有时候还会input和label配合使用,label 元素不会向用户呈现任何特殊效果,label的for属性应当与相关元素的 id 属性相同,input配合label使用可以点击label的内容,聚焦到input
<input type="radio" name ="fruit" id="apple" checked value="apple">
<label for="apple">大苹果</label>
<input type="radio" name ="fruit" id="banana" value="banana">
<label for="banana">大香蕉</label>
因为原生单选在不同浏览器下的默认显示效果不一样,所以通常情况下,我们都会采用障眼法覆盖其原生的样式。
label{
line-height: 24px;
height: 24px;
display: inline-block;
margin-left: 5px;
margin-right:15px;
color: #777;
}
.radio_type{
width: 20px;
height:20px;
appearance: none;
-moz-appearance:none; /* Firefox */
-webkit-appearance:none; /* Safari 和 Chrome */
position: relative;
}
.radio_type:before{
content: '';
width: 20px;
height: 20px;
border: 2px solid #EDD19D;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
}
.radio_type:checked:before{
content: '';
width: 20px;
height: 20px;
border: 2px solid #EDD19D;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
}
.radio_type:checked:after{
content: '';
width: 12px;
height: 12px;
text-align: center;
background:#EDD19D;
border-radius: 50%;
display: block;
position: absolute;
top: 6px;
left: 6px;
}
.radio_type:checked+label{
color: #EDD19D;
}
二、el-radio用法与源码
基础的引用方式如下:
<template>
<el-radio v-model="radio" label="1">备选项</el-radio>
<el-radio v-model="radio" label="2">备选项</el-radio>
</template>
<script>
export default {
data () {
return {
radio: '1'
};
}
}
</script>
接受的参数如下
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
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 | — | — |
源码如下
<template>
<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"
>
<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"
: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>
<span class="el-radio__label" @keydown.stop>
<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: {
// 判断当前组件的父组件是否是ElRadioGroup(单选框组)
isGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
this._radioGroup = parent;
return true;
}
}
return false;
},
model: {
get() {
return this.isGroup ? this._radioGroup.value : this.value;
},
set(val) {
if (this.isGroup) {
this.dispatch('ElRadioGroup', 'input', [val]);
} else {
this.$emit('input', val);
}
this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
}
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
radioSize() {
const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
return this.isGroup
? this._radioGroup.radioGroupSize || temRadioSize
: temRadioSize;
},
isDisabled() {
return this.isGroup
? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
tabIndex() {
return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
},
methods: {
handleChange() {
this.$nextTick(() => {
this.$emit('change', this.model);
this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
});
}
}
};
</script>
name
:用于给原生input元素的设置name属性,用于达到相同name互斥的效果
border
: 是否为选项添加边框,如果为true,则设置is-bordered类名为元素添加边框
.el-radio.is-bordered {
padding: 12px 20px 0 10px;
border-radius: 4px;
border: 1px solid #dcdfe6;
box-sizing: border-box;
height: 40px;
}
size
:与border配合使用时才生效,用来设置选项按钮大小
disabled
:给原生input元素设置disable属性,使其禁用。
label
:Radio 的 value值
value / v-model
:用来实现双向数据绑定的。
三、功能点解密
样式设置
el-radio也是覆盖了radio原有的样式。label下面嵌套了2个span,第一个span是图标部分,第二个span是文字部分,
第一个span中又嵌套了一个span和input,里面的span是模拟的圆形按钮,input是真正的radio标签,el-input隐藏了原有的input样式,然后用span标签去模拟input标签。
隐藏原生input的样式,将其设置opacity为0,使其在页面中不可见,但是可以点击
.el-radio__original {
opacity: 0;
outline: none;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
设置span的样式,模拟input
// 空心,未选中转台的按钮
.el-radio__inner {
border: 1px solid #dcdfe6;
border-radius: 100%;
width: 14px;
height: 14px;
background-color: #fff;
position: relative;
cursor: pointer;
display: inline-block;
box-sizing: border-box;
}
// 选中状态下的按钮
el-radio__input.is-checked .el-radio__inner {
border-color: #409eff;
background: #409eff;
}
.el-radio__inner {
border: 1px solid #dcdfe6;
border-radius: 100%;
width: 14px;
height: 14px;
background-color: #fff;
position: relative;
cursor: pointer;
display: inline-block;
box-sizing: border-box;
}
// 用伪类,模拟中间小圆点
.el-radio__input.is-checked .el-radio__inner:after {
transform: translate(-50%,-50%) scale(1);
}
.el-radio__inner:after {
width: 4px;
height: 4px;
border-radius: 100%;
background-color: #fff;
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%) scale(0);
transition: transform .15s ease-in;
}
v-model/value
单独引用el-radio组件的时候,都会使用v-model去绑定data下面的值来实现双向数据绑定,也就是说有时候即使我们不设置name属性,也可以达到互斥的效果,所以我们去看下v-model/value
是如何实现的呢?
其实v-model/value只是v-bind和v-on的语法糖,在使用v-model绑定数据以后,既绑定了数据,又添加了事件监听,这个事件就是input事件。
官方文档给出:
<input v-model="something">
这不过是以下示例的语法糖:
<input
v-bind:value="something"
v-on:input="something = $event.target.value">
这就相当于对input元素的input事件进行监听,来实现value值的绑定,至于如何实现互斥,其实就是如果单选框的value值和v-model值相同,那么就给当前input元素添加一个checked属性,表示被选中,其他不相等的,就不添加checked属性,就实现了互斥的效果。
在el-radio的源码中,对input设置的是v-model="model",并且对model设置了getter和setter,然后emit一个input事件,在官网中可以看到解释.
允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event。
<my-checkbox v-model="foo" value="some value"></my-checkbox>
上述代码相当于
<my-checkbox
:checked="foo"
@change="val => { foo = val }"
value="some value">
</my-checkbox>
tabindex和:aria-&
tabIndex和aria-* 都是属于无障碍学习的一些设置。
html中的tabIndex属性可以设置键盘中的TAB键在控件中的移动顺序,即焦点的顺序。几乎所有浏览器均 tabindex 属性,除了 Safari。
tabindex有三个值:0 ,-1, 以及X(X里32767是界点)。
当tabindex>=1时,该元素可以用tab键获取焦点,数字越小,越先定位到。
tabIndex=0 ,将排列在所有tabIndex>=1的控件之后。默认情况下tabIndex=0
当tabindex=-1时,该元素用tab键获取不到焦点,但是可以通过js获取.
支持 tabindex 属性的元素:<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>。
可以用以下代码,使用tab键感受以下。
<a href="0.com" tabindex="0">0</a>
<a href="-1.com" tabindex="-1">-1</a>
<a href="1.com" tabindex="1">1</a>
<a href="2.com" tabindex="2">2</a>
<a href="3.com" tabindex="3">3</a>
role、aria-checked、aria-disabled,这些是为屏幕阅读器准备的,aria由一套属性组成,属性分为role以及对应的states和properties,aria将html元素分为六种role,每种有对应的states和properties,用以模拟一些tag,更详细的可点次查看《aria初探(一)》
@keydown.space.stop.prevent="model = isDisabled ? model : label"
这句也很巧妙,查了才知道,原来是为了tab切换不同选项时,按空格可以快速选择目标项。
mixin
mixin用来封装vue组件的可复用功能,一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
- 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
- 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
这里是混入了一个Emitter,也就是说该组件拥有Emitter中的方法。
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
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));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
Emitter的源码中,是在methods混入了dispatch、broadcast两个方法,说明在其他的组件中也会多次使用这个两个方法。
dispatch
dispatch接受三个参数、componentName组件名,eventName事件名,params事件参数、
dispatch主要作用就是找到距离自己最近的目标父组件,然后调用目标组件的 目标事件,并传递参数。
这里调用目标事件使用的是parent.$emit.apply(parent, [eventName].concat(params));
, 你可能会有疑问,为什么这么调用,不直接parent.$emit(eventName,...params)
?
首先,vm.$emit( event, arg )的作用是触发当前实例上的事件,apply主要作用是改变this指向,那么那个调用方式就是用parent对象去调用parent对象的eventName。
即parent拿到parent的$emit
方法,再传递对应的事件参数。
broadcast
broadcast也接受三个参数、componentName组件名,eventName事件名,params事件参数、
broadcast主要作用是像后代组件传值,会遍历所有后代组件,如果是目标组件,就调用目标子组件的目标方法,并传递参数。
与dispatch类似。
四、button-group、radio-button
el-radio中很多地方都计算了isGroup,这是因为ele还提供了一个el-radio-group组件,适用于在多个互斥的选项中选择的场景,所以在设置class或者,触发input事件时都先判断是否是isGroup,如果isGroup,那么就采用isGroup的值或者事件。
radio-button和el-radio功能一样,只是样式的区别。