一、基礎(chǔ)回顧
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 元素不會向用戶呈現(xiàn)任何特殊效果,label的for屬性應(yīng)當(dāng)與相關(guān)元素的 id 屬性相同,input配合label使用可以點擊label的內(nèi)容,聚焦到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用法與源碼
基礎(chǔ)的引用方式如下:
<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>
接受的參數(shù)如下
參數(shù) | 說明 | 類型 | 可選值 | 默認值 |
---|---|---|---|---|
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: {
// 判斷當(dāng)前組件的父組件是否是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元素的設(shè)置name屬性,用于達到相同name互斥的效果
border
: 是否為選項添加邊框,如果為true,則設(shè)置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配合使用時才生效,用來設(shè)置選項按鈕大小
disabled
:給原生input元素設(shè)置disable屬性,使其禁用。
label
:Radio 的 value值
value / v-model
:用來實現(xiàn)雙向數(shù)據(jù)綁定的。
三、功能點解密
樣式設(shè)置
el-radio也是覆蓋了radio原有的樣式。label下面嵌套了2個span,第一個span是圖標(biāo)部分,第二個span是文字部分,
第一個span中又嵌套了一個span和input,里面的span是模擬的圓形按鈕,input是真正的radio標(biāo)簽,el-input隱藏了原有的input樣式,然后用span標(biāo)簽去模擬input標(biāo)簽。
隱藏原生input的樣式,將其設(shè)置opacity為0,使其在頁面中不可見,但是可以點擊
.el-radio__original {
opacity: 0;
outline: none;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
設(shè)置span的樣式,模擬input
// 空心,未選中轉(zhuǎn)臺的按鈕
.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;
}
// 選中狀態(tài)下的按鈕
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下面的值來實現(xiàn)雙向數(shù)據(jù)綁定,也就是說有時候即使我們不設(shè)置name屬性,也可以達到互斥的效果,所以我們?nèi)タ聪?code>v-model/value是如何實現(xiàn)的呢?
其實v-model/value只是v-bind和v-on的語法糖,在使用v-model綁定數(shù)據(jù)以后,既綁定了數(shù)據(jù),又添加了事件監(jiān)聽,這個事件就是input事件。
官方文檔給出:
<input v-model="something">
這不過是以下示例的語法糖:
<input
v-bind:value="something"
v-on:input="something = $event.target.value">
這就相當(dāng)于對input元素的input事件進行監(jiān)聽,來實現(xiàn)value值的綁定,至于如何實現(xiàn)互斥,其實就是如果單選框的value值和v-model值相同,那么就給當(dāng)前input元素添加一個checked屬性,表示被選中,其他不相等的,就不添加checked屬性,就實現(xiàn)了互斥的效果。
在el-radio的源碼中,對input設(shè)置的是v-model="model",并且對model設(shè)置了getter和setter,然后emit一個input事件,在官網(wǎng)中可以看到解釋.
允許一個自定義組件在使用 v-model 時定制 prop 和 event。默認情況下,一個組件上的 v-model 會把 value 用作 prop 且把 input 用作 event。
<my-checkbox v-model="foo" value="some value"></my-checkbox>
上述代碼相當(dāng)于
<my-checkbox
:checked="foo"
@change="val => { foo = val }"
value="some value">
</my-checkbox>
tabindex和:aria-&
tabIndex和aria-* 都是屬于無障礙學(xué)習(xí)的一些設(shè)置。
html中的tabIndex屬性可以設(shè)置鍵盤中的TAB鍵在控件中的移動順序,即焦點的順序。幾乎所有瀏覽器均 tabindex 屬性,除了 Safari。
tabindex有三個值:0 ,-1, 以及X(X里32767是界點)。
當(dāng)tabindex>=1時,該元素可以用tab鍵獲取焦點,數(shù)字越小,越先定位到。
tabIndex=0 ,將排列在所有tabIndex>=1的控件之后。默認情況下tabIndex=0
當(dāng)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,這些是為屏幕閱讀器準(zhǔn)備的,aria由一套屬性組成,屬性分為role以及對應(yīng)的states和properties,aria將html元素分為六種role,每種有對應(yīng)的states和properties,用以模擬一些tag,更詳細的可點次查看《aria初探(一)》
@keydown.space.stop.prevent="model = isDisabled ? model : label"
這句也很巧妙,查了才知道,原來是為了tab切換不同選項時,按空格可以快速選擇目標(biāo)項。
mixin
mixin用來封裝vue組件的可復(fù)用功能,一個混入對象可以包含任意組件選項。當(dāng)組件使用混入對象時,所有混入對象的選項將被“混合”進入該組件本身的選項。
當(dāng)組件和混入對象含有同名選項時,這些選項將以恰當(dāng)?shù)姆绞竭M行“合并”。
- 數(shù)據(jù)對象在內(nèi)部會進行遞歸合并,并在發(fā)生沖突時以組件數(shù)據(jù)優(yōu)先。
- 值為對象的選項,例如 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接受三個參數(shù)、componentName組件名,eventName事件名,params事件參數(shù)、
dispatch主要作用就是找到距離自己最近的目標(biāo)父組件,然后調(diào)用目標(biāo)組件的 目標(biāo)事件,并傳遞參數(shù)。
這里調(diào)用目標(biāo)事件使用的是parent.$emit.apply(parent, [eventName].concat(params));
, 你可能會有疑問,為什么這么調(diào)用,不直接parent.$emit(eventName,...params)
?
首先,vm.$emit( event, arg )的作用是觸發(fā)當(dāng)前實例上的事件,apply主要作用是改變this指向,那么那個調(diào)用方式就是用parent對象去調(diào)用parent對象的eventName。
即parent拿到parent的$emit
方法,再傳遞對應(yīng)的事件參數(shù)。
broadcast
broadcast也接受三個參數(shù)、componentName組件名,eventName事件名,params事件參數(shù)、
broadcast主要作用是像后代組件傳值,會遍歷所有后代組件,如果是目標(biāo)組件,就調(diào)用目標(biāo)子組件的目標(biāo)方法,并傳遞參數(shù)。
與dispatch類似。
四、button-group、radio-button
el-radio中很多地方都計算了isGroup,這是因為ele還提供了一個el-radio-group組件,適用于在多個互斥的選項中選擇的場景,所以在設(shè)置class或者,觸發(fā)input事件時都先判斷是否是isGroup,如果isGroup,那么就采用isGroup的值或者事件。
radio-button和el-radio功能一樣,只是樣式的區(qū)別。