第八集: 從零開始實現一套pc端vue的ui組件庫(input, textarea組件)

第八集: 從零開始實現(輸入框input,textarea組件)

本集定位:
input組件是交互的一大利器, 他與用戶的交流最爲密切, 因此奠基了他在組件界的重要地位.
textarea也算是一種input, 若是能夠的話, 本集也會一塊兒說完, 畢竟是一個類型的, 一塊兒學完收穫會很大.
古人云:"組件不封輸入框,一到面試就發慌"css

一. v-model 簡介
你們若是對 v-model這個指令的原理不熟悉, 建議去學習下vue源碼或者看看相關的分析文章, 很重要的知識, 封裝組件多了就會知道這個指令真是太棒了! 這裏我就簡單說一下他的規則.
1: 父級在組件上綁定了v-model時, 其實就是在往組件裏面傳遞value變量.
2: 你的組件在props上定義value, 就能夠取到值.
3: 每當組件裏this.$emit("input",n)往外面發送事件的時候, 外面會把這個n值 賦值給value
4: 這麼設計的緣由: 你在組件裏面無權改變傳入的值, 這個值你想改爲什麼值就要吐出去, 讓外面改.vue

好了說了這麼多開始實戰吧!git

二. 基本結構
vue-cc-ui/src/components/Input/index.js
老套路, 統一導出爲了適配vue.use的使用方式github

import Input from './main/input.vue'

Input.install = function(Vue) {
  Vue.component(Input.name, Input);
};

export default Input

vue-cc-ui/src/components/Input/main/input.vueweb

  1. type: 這個屬性比較重要, 由於要經過它來區分input與textarea, 還能夠爲input指定number模式.
  2. 命名依然是bem
  3. v-bind="$attrs" 解釋下這個的意思, $attrs指的就是用戶傳進來的屬性, 可是不包括咱們組件內部用props接收的屬性, 也不包括class style這種, 寫它是爲了用戶能夠傳不少input原生的屬性, 畢竟咱們不必把全部屬性都作處理, 讓組件保有原生功能.
  4. placeholder這種模式基本也被現代拋棄了, 針對他也能夠封裝成一個具體的組件, 這個屬性想調整屬性實在是太困難了, 更別說咱們如今還須要placeholder輪播,變色,點擊等等效果.
  5. vue 在行間寫事件的時候, 事件對象會以$event的形式傳給你使用, 其實從代碼的角度來講, 是監控到你這裏用了$event關鍵詞,則把對應的參數賦值爲事件對象.
<template>
  <div class="cc-input">
      <input type="text"
             class='cc-input__inner'
             :value="value"
             v-bind="$attrs"
             :placeholder="placeholder"
             @input="$emit('input',$event.target.value)"/>
    </div>
</template>
props: {
    value: [String, Number],
    placeholder: [String, Number],
    type: {
      type: String,
      default: "text"
    }
  },

三. 豐富事件面試

  1. 輸入框有不少種事件, 他們能給用戶更好的體驗性.
  2. 好比在手機端, 咱們項目以前遇到的問題就是, 用戶點擊輸入框的時候, 會彈出手機鍵盤, 可是彈出的鍵盤會把輸入框頂上去, 某些型號的手機會出現, 就算輸入完畢點擊完成, 但是輸入框仍是被頂上去的狀態, 後來我是藉助blur 與 focus事件才兼容了這寫手機
  3. 不少輸入框也採起節流與防抖, 好比作搜索的相關模糊匹配
  4. 有些以搜索爲主的頁面, 須要自動聚焦
<input :type="type"
     class='cc-input__inner'
     :value="value"
     v-bind="$attrs"
     :autofocus="autofocus" // 是否自動聚焦
     :placeholder="placeholder"
     @blur="blur($event.target.value)" 
     @input="$emit('input',$event.target.value)"
     // 這裏有個小細節, 就是這個事件綁定了兩個操做
     // 不只觸發聚焦事件, 還把變量focus設定爲真
     @focus="$emit('focus',$event.target.value);focus=true"
     @change="$emit('change',$event.target.value)" />

四. 各類狀態vue-router

  1. 禁用狀態, 置灰而且把鼠標變爲禁止狀態 (disabled)
  2. 只讀, 並不置灰, 可是也不能改 (readonly)

具體樣式會在後面出來詳細解釋vuex

<input :type="type"
         :disabled="disabled" // 都是原生屬性, 但要添加樣式
         :readonly="readonly" // 都是原生屬性, 不用添加樣式
         :class="{ 'cc-input--input__disabled':disabled }" />

五. 爲輸入框添加狀態, 並附上icon選項element-ui

  1. 不少輸入框左右都要放個icon充充門面, 分爲左側與右側icon
  2. 右側icon容許輸入文字, icon要有相應的點擊效果
  3. 當組件爲disabled狀態的時候, icon也要相應的置灰
<template>
  <div class="cc-input"
       :class="{
           //  對每種狀態給與相應的class
           'cc-input__error':error,
           'cc-input__normal':!disabled&&!normal,
           'cc-input__abnormal':normal,
           'cc-input__disabled':disabled,
       }"
       :style="{
          // 輸入框有懸停放大的效果, 這裏能夠調節放大的角度, 下面有圖演示
           'transform-origin':`${transformOrigin} 0`
       }">
      
      <nav v-if="leftIcon"
           class="cc-input__prefix is-left"
           // 返回相應的點擊事件
           @click="$emit('clickLeftIcon')">
        <ccIcon :name='leftIcon'
                :color='iconColor'
               // 這裏圖標也要置灰
                :disabled='disabled' />
      </nav>
      <input :type="type"
             class='cc-input__inner'
             :value="value"
             v-bind="$attrs"
             :disabled="disabled"
             :readonly="readonly"
             :autofocus="autofocus"
             :placeholder="placeholder"
             :class="{ 'cc-input--input__disabled':disabled }"
             @blur="blur($event.target.value)"
             @input="$emit('input',$event.target.value)"
             @focus="$emit('focus',$event.target.value);focus=true"
             @change="$emit('change',$event.target.value)" />
      <nav v-if="icon&&!clear"
           class="cc-input__prefix is-right"
           @click="$emit('clickRightIcon')">
        <ccIcon :name="clear?'cc-close':icon"
                :color='iconColor'
                :disabled='disabled' />
        // 容許用戶插入各類節點
        <slot />
      </nav>
    </div>
</template>

效果圖
圖片描述
圖片描述
圖片描述
圖片描述app

六. 清空按鈕
如今的輸入框基本都有這個清空按鈕, 畢竟能夠節省用的時間, 也算是個好功能,
當用戶傳入clear的時候會判斷, 是否禁止修改, 框內是否有值, 是不是hover狀態

hover事件放在父級上

<div class="cc-input"
       @mouseenter="hovering = true"
       @mouseleave="hovering = false">
<nav v-if="showClear"
           class="cc-input__clear"
           @click="clickClear">
        <ccIcon name="cc-close"
                :disabled='disabled' />
        // 這裏是爲了樣式的統一
        // 好比用戶在右側按鈕寫了不少文字
        // 那麼clear按鈕很差定位, 因此才寫了這個站位
        <span style=" opacity: 0;">
          <slot />
        </span>
      </nav>

清除事件, 對外返回空就ok

clickClear() {
      this.$emit("input", "");
      this.$emit("change", "");
    },

判斷是否顯示

computed: {
    showClear() {
      if (
        this.clear &&      // 開啓功能
        !this.disabled &&  // 不是禁用
        !this.readonly &&  // 不是隻讀
        this.value!== '' &&  // 不是空值
        (this.hovering || this.focus) // 聚焦或者hover狀態下
      )return true;
      return false;
    }
  },

vue-cc-ui/src/style/Input.scss

// 引入老四樣
@import './common/var.scss';
@import './common/extend.scss';
@import './common/mixin.scss';
@import './config/index.scss';
// 這裏畢竟是兩個月前寫的組件, 命名方面不是很好, 接下來會統一改正
@include b(input) {
    cursor: pointer;
    position: relative;
    align-items: center;
    display: inline-flex; // 直接flex會獨佔一行
    background-color: white;
    transition: all .3s;
    @include b(input__inner) {
        border: none;
        flex: 1;
        width: 100%;
        font-size: 1em;
        padding: 9px 16px;
        &:focus { outline: 0; } // 這樣寫對障礙閱讀不是很友好
        @include placeholder{ // placeholder設置顏色很頭疼, 請看下面
            color: $--color-input-placeholder;
        }
    };
    @include b(input__prefix) {
        align-items: center;
        display: inline-flex;
        &:hover{transform: scale(1.1)}
        @include when(left) {
            padding-left:6px;
        }
        @include when(right) {
            padding-right:6px;
        }
    };
    @include b(input__clear){
        position: absolute;
        right: 24px;
        &:hover{ animation: size .5s infinite linear;}
    };
    @include b(input--input__disabled){
        @include commonShadow(disabled);
    };
    @at-root {
        @include b(input__normal){
            @include commonShadow($--color-black);
            &:hover {
                z-index: 6;
                transform: scale(1.2);
            }
        }
        @include b(input__error){
            @include commonShadow(danger);
        }
        @include b(input__abnormal){
            @include commonShadow($--color-black);
        }
    }
}

element 這個處理作的也不錯

@mixin placeholder {
  &::-webkit-input-placeholder {
    @content;
  }

  &::-moz-placeholder {
    @content;
  }

  &:-ms-input-placeholder {
    @content;
  }
}

七. textarea 文本域

基本結構

  1. 在用戶type輸入的是textarea時候開啓
  2. 把上面的基礎功能複製下來, 直接放上就能夠用的
  3. textareaCalcStyle: 來設置他的寬高, 畢竟他與input不一樣, 可能須要很大面積
  4. 用戶能夠設置最大高度與最小高度
  5. 難點: 若是用戶選擇了自動適應高度那就麻煩了, 這個組件沒有提供原生的解決方案, 初版我是採用獲取其高度進行運算得出來的, 可是及特殊的狀況會有bug, 最後參考了element-ui的實現方式, 這裏也讓我學習到了.
<template>
  <div class="cc-input" ....>
    <template v-if="type !== 'textarea'">
      <input :type="type" ..../>
    </template>
    <textarea v-else
               // 必須獲取這個dom
              ref="textarea"
              class='cc-input__inner'
              :value="value"
              v-bind="$attrs" 
              :disabled="disabled"
              :readonly="readonly"
              :autofocus="autofocus"
              :placeholder="placeholder"
              @blur="$emit('blur',$event.target.value)"
              @input="$emit('input',$event.target.value)"
              @focus="$emit('focus',$event.target.value)"
              @change="$emit('change',$event.target.value)"
              :style="{
                  width:rows,
                  height:cols,
                  ...textareaCalcStyle}"
              :class="{ 
                  'cc-input--input__disabled':disabled,
                  'cc-input--input__autosize':autosize}" />
    </div>
</template>

針對textarea獲取其真實高度進行高度的動態賦值;
我來講說他的原理, 製做一個與textarea對象相同的元素, 獲取他的滾動距離與高度, 計算出總的高度, 而後賦值給真正的textarea, 這裏的亮點就是怎麼作一個相同的dom, 由於用戶可能給這個dom不一樣的樣式, 不一樣的class, 各類各樣的父級, 腹肌還會影響這個元素的樣式;

// 我的建議, 這種生命週期函數都放在最底部, 而且要保持單一職責
  mounted() {
    this.$nextTick(this.resizeTextarea);
  }

1: 判斷是否是 autosize自動高度, 而且是組件autosize
2: 用戶是否設置了最大高度與最小高度的限制
3: 這個函數只負責處理是否進行計算 calcTextareaHeight 負責計算.

resizeTextarea() {
      const { autosize, type } = this;
      if (type !== "autosize" || !autosize) return;
      const minRows = autosize.min;
      const maxRows = autosize.max;
      this.textareaCalcStyle = this.calcTextareaHeight(
        this.$refs.textarea,
        minRows,
        maxRows
      );
    },

calcTextareaHeight

calcTextareaHeight(el, min, max) {
     // 也算是單例模式, 製做一個元素就好了
      if (!window.hiddenTextarea) {
        window.hiddenTextarea = document.createElement("textarea");
        document.body.appendChild(window.hiddenTextarea);
      }
      // 取得他的屬性, 具體獲取屬性函數下面會講
      let [boxSizing, paddingSize, borderSize] = this.calculateNodeStyling(el);
      // 滾動距離
      let height = window.hiddenTextarea.scrollHeight;
      // 是不是怪異盒模型, 進行分別的計算
      if (boxSizing === "border-box") {
        height = height + borderSize;
      } else {
        height = height - paddingSize;
      }
      // 及時清理,讓用戶看不到這個元素
      window.hiddenTextarea.parentNode &&
        window.hiddenTextarea.parentNode.removeChild(window.hiddenTextarea);
      window.hiddenTextarea = null;

      if (min && height < min) height = min;
      else if (max && height > max) height = max;
      return { height: height + "px" };
    }

calculateNodeStyling

calculateNodeStyling(el) {
// 模擬元素經過值的輸入模擬真正的元素
      window.hiddenTextarea.value = this.value;
      const style = window.getComputedStyle(el);
      const boxSizing = style.getPropertyValue("box-sizing");
      const paddingTop = style.getPropertyValue("padding-top");
      const paddingBottom = style.getPropertyValue("padding-bottom");
      const borderTopWidth = style.getPropertyValue("border-top-width");
      const borderBottomWidth = style.getPropertyValue("border-bottom-width");
      const contextStyle = this.CONTEXT_STYLE.map(
        name => `${name}:${style.getPropertyValue(name)}`
      ).join(";");
      window.hiddenTextarea.setAttribute(
        "style",
        `${contextStyle};${this.HIDDEN_STYLE}`
      );
      return [
        boxSizing,
        parseInt(paddingBottom) + parseInt(paddingTop),
        parseInt(borderBottomWidth) + parseInt(borderTopWidth)
      ];
    },

上面 用到的this.CONTEXT_STYLE數據是樣式的列表

data() {
    return {
      focus: false, // 監聽輸入框的聚焦失焦
      hovering: false,
      textareaCalcStyle: {},
      CONTEXT_STYLE: [
        "width",
        "font-size",
        "box-sizing",
        "line-height",
        "padding-top",
        "font-family",
        "font-weight",
        "text-indent",
        "border-width",
        "padding-left",
        "padding-right",
        "letter-spacing",
        "padding-bottom",
        "text-rendering",
        "text-transform"
      ]
    };
  },

至此才把這個組件作完, 好辛苦
圖片描述
圖片描述

end
若是想作到面面俱到就沒有簡單的組件, element上的每一個組件都值得借鑑.
其實不少原理明白以後學習才能更快捷, 最近拿出時間與你們風向一下vue的實現原理, vue-router vuex等等的實現原理, 但願能對你們對我本身都有幫助吧,, 只能說學海無涯懸崖勒馬😁.

但願你們一塊兒進步, 實現自我價值!!
下一集準備聊聊 計數器
更多好玩的效果請關注我的博客: 連接描述
github: 連接描述

相關文章
相關標籤/搜索