原本不打算寫輸入框的分析,心想一個輸入框能有多複雜,還能怎麼封裝,後來瀏覽了下源碼,發現仍是有不少本身不知道的知識點,因而打算仍是寫,下圖就是一個Element的最基本的輸入框css
首先仍是先要搞懂Element封裝後的input的html結構才行,下面是簡化後的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,前置內容,後置內容,後置元素這幾部分構成,那麼它們分別表明啥呢?下圖就是答案vue
這裏值得注意的是先後置元素和input主體的佈局,修改先後置元素內容能夠發現,中間input的寬度是自適應的,以下圖git
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便可,下面看下輸入框的cssgithub
-webkit-appearance:none,outline:none
這些用法在和各個組件內都很廣泛,目的就是去掉瀏覽器本身渲染出的樣式,統一規定樣式。這裏的
transition
竟然使用了貝塞爾曲線進行過渡,話說過渡時間才0.2秒,使用貝塞爾曲線能看出來麼?直接
ease
應該也能夠啊!
禁用很簡單,經過用戶傳入的disabled
屬性來控制,以下代碼web
<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
的顏色也是能夠控制的,但要注意兼容性bash
&::placeholder {
color: $--input-disabled-placeholder-color;
}
複製代碼
經過查看組件裏原生input的屬性,瞭解了不少知識點app
<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變量來控制
<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隨着內容增多則會出現滾動條
function calcTextareaHeight(){
...
let height = hiddenTextarea.scrollHeight;
const result = {};
...
result.height = `${ height }px`;
return result
}
複製代碼
這裏讓height等於scrollHeight,也就是滾動條捲去的高度,這裏就將height變大了,而後返回該height並綁定到input的style中從而動態改變textarea的height,具體代碼很複雜,還要處理最大最小高度等,參考github