Element UI组件el-input

2,356 阅读2分钟

引言

学习Element UI的input模块笔记。Element UI的input组件主要分成两大类:input和textarea。其中最主要和常用的是input类型。

渲染的源码:

<!-- 前置元素 -->
  <div class="el-input-group__prepend" v-if="$slots.prepend">
    <slot name="prepend"></slot>
  </div>
  <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"
  >
  <!-- 前置内容 -->
  <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>
  <!-- 后置内容 -->
  <span
    class="el-input__suffix"
    v-if="getSuffixVisible()">
    <span class="el-input__suffix-inner">
      <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
        <slot name="suffix"></slot>
        <i class="el-input__icon"
          v-if="suffixIcon"
          :class="suffixIcon">
        </i>
      </template>
      <i v-if="showClear"
        class="el-input__icon el-icon-circle-close el-input__clear"
        @mousedown.prevent
        @click="clear"
      ></i>
      <i v-if="showPwdVisible"
        class="el-input__icon el-icon-view el-input__clear"
        @click="handlePasswordVisible"
      ></i>
      <span v-if="isWordLimitVisible" class="el-input__count">
        <span class="el-input__count-inner">
          {{ textLength }}/{{ upperLimit }}
        </span>
      </span>
    </span>
    <i class="el-input__icon"
      v-if="validateState"
      :class="['el-input__validateIcon', validateIcon]">
    </i>
  </span>
  <!-- 后置元素 -->
  <div class="el-input-group__append" v-if="$slots.append">
    <slot name="append"></slot>
  </div>
</template>

解释1:组件内部通过判断是否使用插槽prepend和append来渲染前置元素和后置元素

---> 使用方式
<div>
   <el-input placeholder="请输入内容" v-model="input1">
      <template slot="prepend">前置元素</template>
   </el-input>
</div>
<div style="margin-top: 15px;">
   <el-input placeholder="请输入内容" v-model="input2">
      <template slot="append">后置元素</template>
   </el-input>
</div>

---> 源码
<!-- 前置元素 -->
<div class="el-input-group__prepend" v-if="$slots.prepend">
    <slot name="prepend"></slot>
</div>
<!-- 后置元素 -->
<div class="el-input-group__append" v-if="$slots.append">
    <slot name="append"></slot>
</div>

解释2:使用带Icon的输入框有两张方法,一种是slot方式,一种是属性方式。

<!-- 前置内容 -->
<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>

解释3:使用Icon的有两种类型,一种是前置内容,一种是后置内容,其中前置内容比较简单,这里不讲,后置内容比较复杂,包含清除(clearable)、显示密码(show-password)、显示输入文本数量(show-word-limit)和文本状态等。

<!-- 后置内容 -->
<span
  class="el-input__suffix"
  v-if="getSuffixVisible()">
  <span class="el-input__suffix-inner">
    <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
      <slot name="suffix"></slot>
      <i class="el-input__icon"
        v-if="suffixIcon"
        :class="suffixIcon">
      </i>
    </template>
    <i v-if="showClear"
      class="el-input__icon el-icon-circle-close el-input__clear"
      @mousedown.prevent
      @click="clear"
    ></i>
    <i v-if="showPwdVisible"
      class="el-input__icon el-icon-view el-input__clear"
      @click="handlePasswordVisible"
    ></i>
    <span v-if="isWordLimitVisible" class="el-input__count">
      <span class="el-input__count-inner">
        {{ textLength }}/{{ upperLimit }}
      </span>
    </span>
  </span>
  <i class="el-input__icon"
    v-if="validateState"
    :class="['el-input__validateIcon', validateIcon]">
  </i>
</span>

组件的computed和methods

computed

computed: {
      // 计算组件el-from-item的size属性
      _elFormItemSize() {
        return (this.elFormItem || {}).elFormItemSize;
      },
      // 计算表单验证的状态
      validateState() {
        return this.elFormItem ? this.elFormItem.validateState : '';
      },
      needStatusIcon() {
        return this.elForm ? this.elForm.statusIcon : false;
      },
      // 计算表单验证的图标
      validateIcon() {
        return {
          validating: 'el-icon-loading',
          success: 'el-icon-circle-check',
          error: 'el-icon-circle-close'
        }[this.validateState];
      },
      // 计算textarea输入框的高度
      textareaStyle() {
        return merge({}, this.textareaCalcStyle, { resize: this.resize });
      },
      // 计算输入框的长度,三种,优先级从左到右,第一个是组件的el-input的size属性,第二个是组件el-form-item的属性size,第三个是全局设置的属性size。
      inputSize() {
        return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
      },
      // 判断输入框是否可以使用,条件有两个,一个是组件el-input的disabled属性,一个是组件el-from的disabled属性。
      inputDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      },
      // 把输入框内容字符串化。
      nativeInputValue() {
        return this.value === null || this.value === undefined ? '' : String(this.value);
      },
      // 判断是否显示“删除”按钮
      showClear() {
        return this.clearable &&
          !this.inputDisabled &&
          !this.readonly &&
          this.nativeInputValue &&
          (this.focused || this.hovering);
      },
      // 判断是否显示“查看密码”按钮
      showPwdVisible() {
        return this.showPassword &&
          !this.inputDisabled &&
          !this.readonly &&
          (!!this.nativeInputValue || this.focused);
      },
      // 判断是否显示输入框的文本长度提示框。
      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);
      }
}

input里面的计算属性,部分是用来判断是否显示图标,比如:取消按钮、查看密码按钮等,其中inputDisabled计算属性的this.elForm是由组件el-form注入,通过el-from组件的属性disabled可以控制el-input是否可用。

methods

methods: {
      // 输入框自动聚焦
      focus() {
        this.getInput().focus();
      },
      // 输入框自动失去焦点
      blur() {
        this.getInput().blur();
      },
      // 无被使用
      getMigratingConfig() {
        return {
          props: {
            'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
            'on-icon-click': 'on-icon-click is removed.'
          },
          events: {
            'click': 'click is removed.'
          }
        };
      },
      // 输入框失去焦点触发事件
      handleBlur(event) {
        this.focused = false;
        this.$emit('blur', event);
        if (this.validateEvent) {
          this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
        }
      },
      select() {
        this.getInput().select();
      },
      // 计算textarea框的高度
      resizeTextarea() {
        if (this.$isServer) return;
        const { autosize, type } = this;
        if (type !== 'textarea') return;
        if (!autosize) {
          this.textareaCalcStyle = {
            minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
          };
          return;
        }
        const minRows = autosize.minRows;
        const maxRows = autosize.maxRows;

        this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
      },
      setNativeInputValue() {
        const input = this.getInput();
        if (!input) return;
        if (input.value === this.nativeInputValue) return;
        input.value = this.nativeInputValue;
      },
      // 输入框获取焦点触发事件
      handleFocus(event) {
        this.focused = true;
        this.$emit('focus', event);
      },
      // 输入法编辑器开始新的输入合成时会触发事件
      handleCompositionStart() {
        this.isComposing = true;
      },
      // 当一个字符被添加到正在编写的文本段落中时触发这个事件
      handleCompositionUpdate(event) {
        const text = event.target.value;
        const lastCharacter = text[text.length - 1] || '';
        this.isComposing = !isKorean(lastCharacter);
      },
      // 当文本段落的组成完成或取消时, 这个事件将被触发
      handleCompositionEnd(event) {
        if (this.isComposing) {
          this.isComposing = false;
          this.handleInput(event);
        }
      },
      // 输入框输入事件
      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);
      },
      // 输入框值改变触发事件
      handleChange(event) {
        this.$emit('change', event.target.value);
      },
      // 计算图标的偏移位置
      calcIconOffset(place) {
        let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
        if (!elList.length) return;
        let el = null;
        for (let i = 0; i < elList.length; i++) {
          if (elList[i].parentNode === this.$el) {
            el = elList[i];
            break;
          }
        }
        if (!el) return;
        const pendantMap = {
          suffix: 'append',
          prefix: 'prepend'
        };

        const pendant = pendantMap[place];
        if (this.$slots[pendant]) {
          el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
        } else {
          el.removeAttribute('style');
        }
      },
      // 更新图标的偏移位置
      updateIconOffset() {
        this.calcIconOffset('prefix');
        this.calcIconOffset('suffix');
      },
      // 清除图标点击事件
      clear() {
        this.$emit('input', '');
        this.$emit('change', '');
        this.$emit('clear');
      },
      // 显示密码点击事件
      handlePasswordVisible() {
        this.passwordVisible = !this.passwordVisible;
        this.focus();
      },
      // 获取输入框的dom
      getInput() {
        return this.$refs.input || this.$refs.textarea;
      },
      // 是否显示后置内容
      getSuffixVisible() {
        return this.$slots.suffix ||
          this.suffixIcon ||
          this.showClear ||
          this.showPassword ||
          this.isWordLimitVisible ||
          (this.validateState && this.needStatusIcon);
      }
}

methods的方法大部分比较简单,需要注意的有:

  • 输入框失去焦点事件触发的函数handleBlur中的this.dispatch('ElFormItem', 'el.form.blur', [this.value]) 操作,这个主要是触发组件el-form-item的表单验证的。
  • calcIconOffset方法是用于计算前置元素prepend和后置元素append的偏移量,在组件生命周期mounted、updated和watch的type的方法里边会被调用,这个方法比较复杂。

结语

el-input组件的源码相对比较简单,只要使用过vue,组件el-input,加上Element-UI的官方文档对el-input的说明,基本上都能看懂。