Element源碼分析系列5-Input(輸入框)

簡介

原本不打算寫輸入框的分析,心想一個輸入框能有多複雜,還能怎麼封裝,後來瀏覽了下源碼,發現仍是有不少本身不知道的知識點,因而打算仍是寫,下圖就是一個Element的最基本的輸入框css

結果一看源碼,個人鬼鬼,源碼居然300多行!咋會這麼複雜,看過官網的文檔後,發現確實應該這麼複雜,由於這個輸入框不只僅是隻有一個input這麼簡單,還附帶了不少的其餘內容,上圖僅是一個最基本的形式而已,下面咱們依次分析,官網源碼 點此
原本打算貼出所有源碼,可是發現這樣篇幅太長,所以咱們只分析重點,分析部分源碼

輸入框源碼html結構

首先仍是先要搞懂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輸入框,先後2個都是輔助性的內容,這2個就是先後置元素,而輸入框內的搜索和日期Icon就是先後置內容,所以要封裝這麼個完整的input,代碼量確實比較多

這裏值得注意的是先後置元素和input主體的佈局,修改先後置元素內容能夠發現,中間input的寬度是自適應的,以下圖git

中間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便可,下面看下輸入框的cssgithub

輸入框實際上是有左右padding的,爲了更美觀,這裏不是用text-indent來控制光標位置

能夠看出 -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元素的屬性

經過查看組件裏原生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變量來控制

上圖中點擊空格後纔會填入實際的文本,輸入英文或數字則沒有這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內容清空,以下圖

先看html結構,下面是後置內容的html代碼

<!-- 後置內容 -->
      <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

相關文章
相關標籤/搜索