element-ui組件解讀 (一) input組件

察見淵魚者不詳,智料隱匿者有殃。——《列子·說符》javascript

簡介


在平常開發PC管理端界面的時候,會用到element-ui或者iviewui框架,比較經常使用的input組件是怎麼封裝的呢,以element-ui框架的input組件爲例,分析一下它是怎麼封裝實現的,也能夠爲之後本身封裝框架提供思路。html

由於代碼仍是挺多的,主要分析幾個點:vue

  • 支持前置內容後置內容後置元素
  • 支持全部原聲的type,而且能夠切換password模式
  • 支持readonlydisabledautocompletemaxlengthminlength等等
  • 支持常規inputfocusblurchange,支持新事件compositionstartcompositionupdatecompositionend
  • element-ui是怎麼作到相似與vue的雙向綁定的
  • typetextare模式時,作到calcTextareaHeight

下面就一步一步分下,主要的點會着重分析,比較簡單或者比較不重要的點會快速帶過。java

源碼參考element-ui inputgit

前置、後置

<template v-if="type !== 'textarea'">
    <!-- 前置元素 -->
    <div class="el-input-group__prepend" v-if="$slots.prepend">
      <slot name="prepend"></slot>
    </div>
    <!-- 前置內容 -->
    <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
      <slot name="prefix"></slot>
      <i class="el-input__icon" v-if="prefixIcon" :class="prefixIcon">
      </i>
    </span>

    // 主題代碼 省略
    <input/>
    <!-- 後置內容 -->
    <span class="el-input__suffix" v-if="getSuffixVisible()">
      // 省略內容
    </span>
    <!-- 後置元素 -->
    <div class="el-input-group__append" v-if="$slots.append">
      <slot name="append"></slot>
    </div>
  </template>
  <textarea v-else>
    // 省略內容
  </textarea/>
複製代碼

上面代碼能夠首先經過type區分開textarea單獨處理,先後置又分爲兩類:github

  • 一類直接經過prop傳入prefix-iconsuffix-icon傳入先後置icon-class顯示不一樣的內容
  • 另外一類是經過vue中的內容分發機制slot顯示不一樣的內容,更靈活

在後置內容中也會處理clearablePwdVisibleshow-word-limit顯示不一樣的內容。element-ui

原聲type、其它原聲屬性

input爲例,代碼以下:app

// 其它地方省略
  <input :tabindex="tabindex" v-if="type !== 'textarea'" class="el-input__inner" v-bind="$attrs" :type="showPassword ? (passwordVisible ? 'text': 'password') : type" :disabled="inputDisabled" :readonly="readonly" :autocomplete="autoComplete || autocomplete" ref="input" @compositionstart="handleCompositionStart" @compositionupdate="handleCompositionUpdate" @compositionend="handleCompositionEnd" @input="handleInput" @focus="handleFocus" @blur="handleBlur" @change="handleChange" :aria-label="label" >
  // 其它地方省略
  <script> export default { props: { // 其它地方省略 disabled: Boolean, readonly: Boolean, autocomplete: { type: String, default: 'off' }, /** @Deprecated in next major version */ autoComplete: { type: String, validator(val) { process.env.NODE_ENV !== 'production' && console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.'); return true; } } // 其它地方省略 } } </script>
複製代碼

這裏先不關注事件,只關注屬性的設置,element-ui支持全部input原聲的屬性,其實就是經過prop傳入type傳入,若是是非password就直接賦值給input標籤的type屬性。 若是是password類型判斷是否傳入show-password字段,根據傳入判斷給inputtype複製爲password或者text框架

其它原聲屬性

其它屬性如disabledreadonlyautocomplete都是經過顯示的prop傳入,哪像一些沒有顯示接受的prop怎麼獲取到的呢,好比說maxlengthminlength這種沒有顯示接收的怎麼獲取到的呢。 是經過v-bind="$attrs"獲取到的,使用的時候直接經過this.$attrs.XXXX就可使用對應的屬性。element-ui中在檢測輸入長度是否超過設置的長度是有使用到以下:iview

// 忽略部分代碼
  isWordLimitVisible() {
    return this.showWordLimit &&
      this.$attrs.maxlength &&
      (this.type === 'text' || this.type === 'textarea') &&
      !this.inputDisabled &&
      !this.readonly &&
      !this.showPassword;
  },
  upperLimit() {
    return this.$attrs.maxlength;
  },
  textLength() {
    if (typeof this.value === 'number') {
      return String(this.value).length;
    }
    return (this.value || '').length;
  },
  inputExceed() {
    // show exceed style if length of initial value greater then maxlength
    return this.isWordLimitVisible &&
      (this.textLength > this.upperLimit);
  }
  // 忽略部分代碼
複製代碼

若是不太瞭解v-bind="$attrs"的使用能夠看我另外一篇文章Vue中v-model解析、sync修飾符解析

原聲事件、雙向綁定

支持常規inputfocusblurchange,支持新事件compositionstartcompositionupdatecompositionend

經常使用的時間就不用多作介紹了,就是比較新事件compositionstartcompositionupdatecompositionend是用來出裏一段文字輸入的事件。詳細信息請看: compositionstart compositionupdate compositionend

在外層就能夠監聽的到input標籤的inputfocusblurchange事件,是由於組件內部在每個方法中都有一個this.$emit('input/focus/blur/change', evnet.target.value/event)

那是怎麼作到修改外層經過v-model綁定的值的呢?

要想了解他是怎麼實現雙向綁定的就要了解v-model是什麼,v-model其實就是一個語法糖,在vue編譯階段就會解析爲:value="綁定的值"和默認的@input=(value) => {綁定的值 = value}。而後再是在input標籤上綁定的handleInput方法以下:

handleInput(event) {
      // should not emit input during composition
      // see: https://github.com/ElemeFE/element/issues/10516
      if (this.isComposing) return;
      // hack for https://github.com/ElemeFE/element/issues/8548
      // should remove the following line when we don't support IE
      if (event.target.value === this.nativeInputValue) return;
      this.$emit('input', event.target.value);
      // ensure native input value is controlled
      // see: https://github.com/ElemeFE/element/issues/12850
      this.$nextTick(this.setNativeInputValue);
    }
複製代碼

到這裏input的組件的屬性基本上就解釋完成,textare基本上都是同樣的,可是textarea有一點特殊的操做。

textarea高度自適應

它是怎麼實現高度自適應的,具體的實現代碼是在calcTextareaHeight,大體分爲幾部:

  • 建立一個臨時隱藏的textarea元素
  • 經過隱藏的textarea計算顯示的textarea元素的高度
  • 而且判斷minRowsmaxRows屬性
  • 最後刪除textarea元素,而且清空dom引用

input組件中經過建立watch監聽value值的變化,每次value變化重新計算textarea元素的高度。

建立一個臨時隱藏的textarea元素

let hiddenTextarea;
// 省略部分代碼
// 保證只執行一次
if (!hiddenTextarea) {
  // 建立textarea
  hiddenTextarea = document.createElement('textarea');
  // 添加到頁面中
  document.body.appendChild(hiddenTextarea);
}
// 省略部分代碼
複製代碼

經過隱藏的textarea計算顯示的textarea元素的高度

const HIDDEN_STYLE = ` height:0 !important; visibility:hidden !important; overflow:hidden !important; position:absolute !important; z-index:-1000 !important; top:0 !important; right:0 !important `;

const CONTEXT_STYLE = [
  'letter-spacing',
  'line-height',
  'padding-top',
  'padding-bottom',
  'font-family',
  'font-weight',
  'font-size',
  'text-rendering',
  'text-transform',
  'width',
  'text-indent',
  'padding-left',
  'padding-right',
  'border-width',
  'box-sizing'
];
function calculateNodeStyling(targetElement) {
  const style = window.getComputedStyle(targetElement);

  const boxSizing = style.getPropertyValue('box-sizing');

  const paddingSize = (
    parseFloat(style.getPropertyValue('padding-bottom')) +
    parseFloat(style.getPropertyValue('padding-top'))
  );

  const borderSize = (
    parseFloat(style.getPropertyValue('border-bottom-width')) +
    parseFloat(style.getPropertyValue('border-top-width'))
  );

  const contextStyle = CONTEXT_STYLE
    .map(name => `${name}:${style.getPropertyValue(name)}`)
    .join(';');

  return { contextStyle, paddingSize, borderSize, boxSizing };
}
// 省略部分代碼
let {
    paddingSize,
    borderSize,
    boxSizing,
    contextStyle
} = calculateNodeStyling(targetElement);
// 設置屬性
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
// 設置value
hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
// 獲取隱藏textarea滾動總體高度
let height = hiddenTextarea.scrollHeight;
const result = {};
// 判斷盒模型
if (boxSizing === 'border-box') {
  // 若是爲border-box自己的高度加上borderSize
  height = height + borderSize;
} else if (boxSizing === 'content-box') {
  // 若是爲content-box自己的高度加上paddingSize
  height = height - paddingSize;
}
// 把隱藏textarea的vlaue設置爲空
hiddenTextarea.value = '';
// 獲取每行高度: row的高度爲滾動高度-padding
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
// 省略部分代碼
複製代碼

而且判斷minRowsmaxRows屬性

// 省略部分代碼
  // 判斷是否有設置minRows
  if (minRows !== null) {
    // 最小高度 = 每行高度 * 最小行數
    let minHeight = singleRowHeight * minRows;
    if (boxSizing === 'border-box') {
      // 若是boxSizing爲border-box時: 最小高度 = 原最小高度 + padding + border;
      minHeight = minHeight + paddingSize + borderSize;
    }
    // 取當前計算的最小高度和上面計算出的scrollHeight度中的最大值
    height = Math.max(minHeight, height);
    // 賦值給result.minHeight
    result.minHeight = `${ minHeight }px`;
  }
  // 判斷是否有設置maxRows
  if (maxRows !== null) {
    // 最大高度 = 每行高度 * 最大行數
    let maxHeight = singleRowHeight * maxRows;
    if (boxSizing === 'border-box') {
      // 若是boxSizing爲border-box時: 最小高度 = 原最小高度 + padding + border;
      maxHeight = maxHeight + paddingSize + borderSize;
    }
    // 取當前計算的最大高度和上面計算出的scrollHeight度中的最小值
    height = Math.min(maxHeight, height);
  }
  // 賦值給result.minHeight
  result.height = `${ height }px`;
  // 省略部分代碼
複製代碼

最後刪除textarea元素,而且清空dom引用

// 若是存在parentNode 就清除parentNode包含的hiddenTextarea 元素
  hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
  // 清空dom引用釋放變量
  hiddenTextarea = null;
  // 返回最小高度和最大高度
  return result;
複製代碼

到此textarea結束,可能還有不少細節沒有記錄到,若是有什麼意見請評論。

總結

整篇文章都是根據幾點分析的,以下:

  • 支持前置內容後置內容後置元素
  • 支持全部原聲的type,而且能夠切換password模式
  • 支持readonlydisabledautocompletemaxlengthminlength等等
  • 支持常規inputfocusblurchange,支持新事件compositionstartcompositionupdatecompositionend
  • element-ui是怎麼作到相似與vue的雙向綁定的
  • typetextare模式時,作到calcTextareaHeight

固然還有不少的點沒有記錄到好比說clearemitterMigrating等等,這個會在後面的文章中着重介紹。

參考

element-ui input

element-ui calcTextareaHeight

相關文章
相關標籤/搜索