簡介
本來不打算寫輸入框的分析,心想一個輸入框能有多複雜,還能怎麼封裝,後來瀏覽了下原始碼,發現還是有很多自己不知道的知識點,於是打算還是寫,下圖就是一個Element的最基本的輸入框
結果一看原始碼,我的鬼鬼,原始碼竟然300多行!咋會這麼複雜,看過官網的文件後,發現確實應該這麼複雜,因為這個輸入框不僅僅是隻有一個input這麼簡單,還附帶了很多的其他內容,上圖僅是一個最基本的形式而已,下面我們依次分析,官網原始碼點此本來打算貼出全部原始碼,但是發現這樣篇幅太長,因此我們只分析重點,分析部分原始碼
輸入框原始碼html結構
首先還是先要搞懂Element封裝後的input的html結構才行,下面是簡化後的html結構
<template>
<div ...>
<template v-if="type !== 'textarea'">
<!-- 前置元素 -->
<div class="el-input-group__prepend" v-if="$slots.prepend">
<slot name="prepend"></slot>
</div>
<!--主體input-->
<input ...>
<!-- 前置內容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
...
</span>
<!-- 後置內容 -->
<span
...
</span>
<!-- 後置元素 -->
<div class="el-input-group__append" v-if="$slots.append">
...
</div>
</template>
<textarea v-else>
</textarea>
</div>
</template>
複製程式碼
是不是看著很頭大?其實很簡單,最外層一個div作為wrapper包裹裡面的元素,然後裡面是template標籤(template實際不會渲染出來)的v-if,最下面是textarea的v-else,說明type
這個選項控制輸入框元件是顯示input
還是textarea
,對於v-else就一個textarea,沒啥可說的,關鍵在於前面的v-if,仔細看這個結構,是由前置元素,主體input,前置內容,後置內容,後置元素這幾部分構成,那麼它們分別代表啥呢?下圖就是答案
這裡值得注意的是前後置元素和input主體的佈局,修改前後置元素內容可以發現,中間input的寬度是自適應的,如下圖
中間input自動變窄,那麼這哥佈局是咋回事呢,這哥佈局類似於左列寬度不定,右列自適應,左列不定的意思是寬度由內容撐開來,檢視css程式碼得知,這是table-cell
佈局,我們知道table內表格寬度都是自適應的,某一列很寬的話,另外的列就會變窄,因此這個思想可以用到這裡來,下面就是示例佈局(左列寬度不定,右列自適應),注意外層容器設定display:table
<div style="display:table" class='wrapper'>
<div style="display:table-cell" class='left'>
</div>
<div style="display:table-cell" class='right'>
</div>
</div>
複製程式碼
這個佈局用flex也可以實現,具體就是left元素不設定寬度,right元素設定flex:1即可,下面看下輸入框的css
輸入框其實是有左右padding的,為了更美觀,這裡不是用text-indent來控制游標位置 可以看出-webkit-appearance:none,outline:none
這些用法在和各個元件內都很普遍,目的就是去掉瀏覽器自己渲染出的樣式,統一規定樣式。這裡的transition
居然使用了貝塞爾曲線進行過渡,話說過渡時間才0.2秒,使用貝塞爾曲線能看出來麼?直接ease
應該也可以啊!
禁用狀態的實現
禁用很簡單,通過使用者傳入的disabled
屬性來控制,如下程式碼
<el-input
placeholder="請輸入內容"
v-model="input1"
:disabled="true">
</el-input>
複製程式碼
原始碼裡通過<input :disabled="inputDisabled" ...>
來控制input的功能禁用,這個inputDisabled
是個計算屬性
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
複製程式碼
這裡因為要判斷如果input被包含在表單內,如果表單禁用,那麼自然自己也就被禁用了。輸入框樣式上的禁用是由最外層的div的class控制的
<div :class=[{'is-disabled': inputDisabled}...]>...</div>
複製程式碼
這裡沒有放在裡面的input上進行控制,原因是放在最外層可以統一控制裡面的textarea和input,減少程式碼冗餘,通過子選擇器選擇到input和textarea進行控制,這裡placeholder
的顏色也是可以控制的,但要注意相容性
&::placeholder {
color: $--input-disabled-placeholder-color;
}
複製程式碼
input元素的屬性
通過檢視元件裡原生input的屬性,瞭解了很多知識點
<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:type="type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete"
:value="currentValue"
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
複製程式碼
哇,居然這麼多屬性和方法~~這就是一個成熟元件需要實現的東西,先看tabindex
,就是控制tab鍵按下後的訪問順序,由使用者傳入tabindex如果設定為負數則無法通過tab鍵訪問,設定為0則是在最後訪問。然後v-if="type !== 'textarea'"
控制了這個input的渲染與否,使用者傳入type屬性進行控制,然後是input的類el-input__inner
,前面介紹過,然後是v-bind="$attrs"
這句話,這句話是幹嘛的?翻開官網得知
<el-input maxlength="5" minlength="2">
</el-input>
複製程式碼
這裡我們給<el-input>
元件新增了2個原生屬性,注意這2個原生屬性並沒有在prop裡面,這2個屬性是控制input的最大輸入和最小輸入長度的,那麼這2個屬性現在僅僅放在了父元素<el-input>
上,如何將其傳遞給素<el-input>
內的原生input子元素呢?不傳遞則這2個屬性不起作用,因為子input上沒有這2個屬性。答案就是通過v-bind="$attrs"
來實現,它將父元素所有非prop的特性都繫結在了子元素input上,否則你還得在props裡宣告maxlength,minlength,程式碼量增大。這就是$attrs
的優勢所在
往下看:readonly="readonly" :autocomplete="autoComplete"
,這2個屬性都是原生的屬性,由使用者傳入,控制輸入框只讀和是否自動補全,然後是輸入框的value:value="currentValue"
這裡的currentValue是在data裡面
currentValue: this.value === undefined || this.value === null
? ''
: this.value,
複製程式碼
如果使用者沒有在<el-input>
上寫v-model(v-model原理參考官網),那麼就沒有傳入value,所以currentValue就是空字串,否則就是傳入的值,接著ref="input"
一句,ref用來給元素或子元件註冊引用資訊。引用資訊將會註冊在父元件的 $refs 物件上,這是為了方便後續程式碼直接拿到原生input的dom
然後是這3句話
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
複製程式碼
這可不能小瞧,這3個方法是原生的方法,這裡簡單介紹下,官方定義如下compositionstart 事件觸發於一段文字的輸入之前(類似於 keydown 事件,但是該事件僅在若干可見字元的輸入之前,而這些可見字元的輸入可能需要一連串的鍵盤操作、語音識別或者點選輸入法的備選詞) 簡單來說就是切換中文輸入法時在打拼音時(此時input內還沒有填入真正的內容),會首先觸發compositionstart,然後每打一個拼音字母,觸發compositionupdate,最後將輸入好的中文填入input中時觸發compositionend。觸發compositionstart時,文字框會填入 “虛擬文字”(待確認文字),同時觸發input事件;在觸發compositionend時,就是填入實際內容後(已確認文字),所以這裡如果不想觸發input事件的話就得設定一個bool變數來控制
上圖中點選空格後才會填入實際的文字,輸入英文或數字則沒有這3個事件的觸發那麼問題來了,為啥Element要設定這3個事件的處理函式呢?原因很簡單,我們肯定不希望在輸入拼音的過程中就直接觸發input事件改變
<el-input v-model="inputValue"></el-input>
中inputValue的值,而是希望輸入完成後再改變,所以需要特殊處理,我們來看handleComposition
的原始碼,注意這裡只寫了一個方法而不是3個,通過event.type來判斷事件型別從而簡化程式碼,可以借鑑
handleComposition(event) {
if (event.type === 'compositionend') {
this.isOnComposition = false;
this.currentValue = this.valueBeforeComposition;
this.valueBeforeComposition = null;
this.handleInput(event);
} else {
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
this.isOnComposition = !isKorean(lastCharacter);
if (this.isOnComposition && event.type === 'compositionstart') {
this.valueBeforeComposition = text;
}
}
},
複製程式碼
這裡首先在data中定義了一個bool變數isOnComposition
,這個變數就是用來判斷是否在打拼音的過程中,初始為false,當開始打拼音後,觸發compositionstart
事件,更新isOnComposition
,通過this.isOnComposition = !isKorean(lastCharacter)
來更新,這裡的邏輯是判斷輸入的字元的最後一個是不是韓文,韓文通過正規表示式來判斷,至於為啥要判斷韓文的最後一個字元,不清楚~ 如果是中文,則isOnComposition
為true,這裡比較難理解的是後面這個if,當正在打拼音的過程中且是compositionstart
事件時,則用一個valueBeforeComposition
變數儲存當前的文字,也就是儲存此次打字前input中的文字內容,這個valueBeforeComposition
的作用後面介紹,接下來看if (event.type === 'compositionend')
中的內容,當打完拼音後,觸發compositionend
,此時設定isOnComposition
為false表明打字完成,然後注意這裡會手動觸發一個this.handleInput(event)
(handleInput就是input上繫結的v-on:input),這是因為最後輸入完成時,compositionend
會在input
事件後觸發,此時isOnComposition
還是true,無法觸發下面handleInput中的emit將新的input的value傳遞給父元件,所以這裡需要手動呼叫一次handleInput,這裡請仔細理解!
handleInput(event) {
const value = event.target.value;
this.setCurrentValue(value);
if (this.isOnComposition) return;
this.$emit('input', value);
},
複製程式碼
handleInput中當isOnComposition
為true時表明正在打拼音輸入,則不觸發emit事件,這是合理且正常的
可清空的實現
<el-input>
中如果新增了clearable
屬性則輸入文字後會出現一個叉的圖示,點選後input內容清空,如下圖
<!-- 後置內容 -->
<span
class="el-input__suffix"
v-if="$slots.suffix || suffixIcon || showClear || validateState && needStatusIcon">
<span class="el-input__suffix-inner">
<template v-if="!showClear">
<slot name="suffix"></slot>
<i class="el-input__icon"
v-if="suffixIcon"
:class="suffixIcon">
</i>
</template>
<i v-else
class="el-input__icon el-icon-circle-close el-input__clear"
@click="clear"
></i>
</span>
<i class="el-input__icon"
v-if="validateState"
:class="['el-input__validateIcon', validateIcon]">
</i>
</span>
複製程式碼
中間這段<i>
就是清空按鈕,它是一個i標籤,有一個click事件,前面通過showClear
來判斷是否需要顯示清空按鈕,邏輯如下
showClear() {
return this.clearable &&
!this.disabled &&
!this.readonly &&
this.currentValue !== '' &&
(this.focused || this.hovering);
}
複製程式碼
這個計算屬性第一步得看使用者是否新增了顯示清空按鈕的屬性,如果沒有則不顯示,如果有則繼續判斷,在非禁用且非只讀狀態下才且當前input的value不是空且該input獲得焦點或者滑鼠移動上去才顯示,條件略多啊
然後看clear清空這個方法
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
this.setCurrentValue('');
this.focus();
}
複製程式碼
居然有5句話,但都不能少,第一個emit是通知父元件自己的value值變成了空,從而更新<el-input v-model="v">
中的v這個data為空,第二句emit觸發了父元件的change事件,這樣在<el-input v-model="v" @change="inputChange">
中的inputChange中就能監聽到該事件了,第3個emit觸發父元件的@clear方法,讓父元件知道自己已經清空了,第四句話更新自己的currentValue為空,第五局讓input獲得焦點便於輸入內容
textarea高度自適應的實現
這個就比較難了,這裡只簡單分析其原理,原生的textarea隨著內容增多則會出現滾動條
而Element的處理卻能夠讓其自適應高度,也就是不出現滾動條 核心原理是在textarea的input事件中進行邏輯判斷,每觸發一次input就判斷一次,具體在下面函式中進行處理function calcTextareaHeight(){
...
let height = hiddenTextarea.scrollHeight;
const result = {};
...
result.height = `${ height }px`;
return result
}
複製程式碼
這裡讓height等於scrollHeight,也就是滾動條捲去的高度,這裡就將height變大了,然後返回該height並繫結到input的style中從而動態改變textarea的height,具體程式碼很複雜,還要處理最大最小高度等,參考github