el-input 源码

670 阅读3分钟

el-input 源码

学习element-ui 中input组件源码

el-input

基本结构

可以看到input组件使用的是原生的input元素

image.png

禁用状态

设置disabled属性,也是利用原生input的disabled属性;如果在form组件中设置了disabled,也会自动设置input组件。

inputDisabled() {
  return this.disabled || (this.elForm || {}).disabled;
},

可清空

设置clearable属性,可以清空input元素中的内容。后面的clear icon只会在输入框中有内容,或者hover时才会显示。

showClear() {
  return this.clearable &&
    !this.inputDisabled &&
    !this.readonly &&
    this.nativeInputValue &&
    (this.focused || this.hovering);
},

点击clear icon会清空元素中的内容,触发clear方法,因为用户在使用input组件时是通过v-model来绑定值的所以会触发input事件,设置值为空。

clear() {
  this.$emit('input', '');
  this.$emit('change', '');
  this.$emit('clear');
},

input没有绑定value值,而是在代码中通过ref来对input元素进行赋值。触发了input事件后,el-input组件通过computed重新设置了nativeInputValue

nativeInputValue() {
  return this.value === null || this.value === undefined ? '' : String(this.value);
},

通过watch监听nativeInputValue,来给input元素设置值

nativeInputValue() {
  this.setNativeInputValue();
},
setNativeInputValue() {
  const input = this.getInput();
  if (!input) return;
  if (input.value === this.nativeInputValue) return;
  input.value = this.nativeInputValue;
},

为什么不直接按以下方式重新设置呢,源代码中贴出了一个bug,主要是为了解决在完成输入之前如果触发了dom更新,输入框中的值并不会更新。

this.$emit('input', value)

bug链接

[Bug Report] Component Update Causes Input Component Input State Error · Issue #14521 · ElemeFE/element · GitHub

密码框

设置showPassword,会显示一个 el-icon-view的icon,并且input组件被设置为了passwor类型。

点击view icon 会使input元素在text 和 password两个原生属性之间切换。

handlePasswordVisible() {
  this.passwordVisible = !this.passwordVisible;
  this.$nextTick(() => {
    this.focus();
  });
},

带 icon 的输入框

可以通过 prefix-icon 和 suffix-icon 属性在 input 组件首部和尾部增加显示图标,也可以通过 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>
<!-- 后置内容 -->
<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>

文本域

设置type 为 textarea属性,使用原生textarea文本域输入框

可自适应文本高度的文本域

设置autosize 文本框可以自适应高度。输入的内容发生变化时,调用以下函数会自动计算文本的高度。

value(val) {
  this.$nextTick(this.resizeTextarea);
  if (this.validateEvent) {
    this.dispatch('ElFormItem', 'el.form.change', [val]);
  }
},
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);
},

新建一个hiddenTextarea,通过getComputedStyle来计算元素的高度

export default function calcTextareaHeight(
  targetElement,
  minRows = 1,
  maxRows = null
) {
  if (!hiddenTextarea) {
    hiddenTextarea = document.createElement('textarea');
    document.body.appendChild(hiddenTextarea);
  }

  let {
    paddingSize,
    borderSize,
    boxSizing,
    contextStyle
  } = calculateNodeStyling(targetElement);

  hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
  hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';

  let height = hiddenTextarea.scrollHeight;
  const result = {};

  if (boxSizing === 'border-box') {
    height = height + borderSize;
  } else if (boxSizing === 'content-box') {
    height = height - paddingSize;
  }

  hiddenTextarea.value = '';
  let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;

  if (minRows !== null) {
    let minHeight = singleRowHeight * minRows;
    if (boxSizing === 'border-box') {
      minHeight = minHeight + paddingSize + borderSize;
    }
    height = Math.max(minHeight, height);
    result.minHeight = `${ minHeight }px`;
  }
  if (maxRows !== null) {
    let maxHeight = singleRowHeight * maxRows;
    if (boxSizing === 'border-box') {
      maxHeight = maxHeight + paddingSize + borderSize;
    }
    height = Math.min(maxHeight, height);
  }
  result.height = `${ height }px`;
  hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
  hiddenTextarea = null;
  return result;
};

复合输入框

通过slot增加组件前后的内容

<!-- 前置元素 -->
<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>

尺寸

通过size属性来设置不同的尺寸,主要是应用了不同的class

inputSize() {
  return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
inputSize ? 'el-input--' + inputSize : '',

输入长度限制

利用原生的maxlength属性来限制元素的最大输入

el-autocomplete

基础使用

<el-autocomplete
    class="inline-input"
    v-model="state1"
    :fetch-suggestions="querySearch"
    placeholder="请输入内容"
    @select="handleSelect"
/>

fetch-suggestions 用于自定义显示匹配的内容列表,并且需要返回一个数组

在基础的用法中,首先点击输入框时会触发handlFocus事件,设置activated属性为true,并且调用

debouncedGetData函数,用于获取输入值为空时的所有列表项。

如果triggerOnFocus为false,那么只会在有输入值并且有匹配项的时候,才会显示匹配的列表。

handleFocus(event) {
  this.activated = true;
  this.$emit('focus', event);
  if (this.triggerOnFocus) {
    this.debouncedGetData(this.value);
  }
},

可以看到debouncedGetData函数,是一个防抖函数,用于获取数据

this.debouncedGetData = debounce(this.debounce, this.getData);

getData方法中会调用用户传进来的fetchSuggestions方法,并且将queryString传进去,经过用户自定义的匹配

函数后,在调用cb,将匹配的内容赋值给this.suggestions

getData(queryString) {
  console.log('queryString', queryString);
  if (this.suggestionDisabled) {
    return;
  }
  this.loading = true;
  this.fetchSuggestions(queryString, (suggestions) => {
    this.loading = false;
    if (this.suggestionDisabled) {
      return;
    }
    if (Array.isArray(suggestions)) {
      this.suggestions = suggestions;
      console.log(this.suggestions);
      this.highlightedIndex = this.highlightFirstItem ? 0 : -1;
    } else {
      console.error('[Element Error][Autocomplete]autocomplete suggestions must be an array');
    }
  });
},

此时computed中的suggestionVisible属性会设置为true

suggestionVisible() {
  const suggestions = this.suggestions;
  let isValidData = Array.isArray(suggestions) && suggestions.length > 0;
  return (isValidData || this.loading) && this.activated;
},

再触发ElAutocompleteSuggestions组件的visible事件,用于显示匹配的列表

watch: {
  suggestionVisible(val) {
    let $input = this.getInput();
    if ($input) {
      this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);
    }
  }
},
// ElAutocompleteSuggestions
created() {
  this.$on('visible', (val, inputWidth) => {
    this.dropdownWidth = inputWidth + 'px';
    this.showPopper = val;
  });
}

当用户输入建议时 会触发input事件

handleInput(value) {
  this.$emit('input', value);
  this.suggestionDisabled = false;
  if (!this.triggerOnFocus && !value) {
    this.suggestionDisabled = true;
    this.suggestions = [];
    return;
  }
  this.debouncedGetData(value);
},

自定义模板

和el-input组件一样,可以通过prepend、append、prefix、suffix四个插槽来设置组件前后的图标和内容。

可以通过默认的作用域插槽自定义显示匹配的列表内容

<li
  v-for="(item, index) in suggestions"
  :key="index"
  :class="{'highlighted': highlightedIndex === index}"
  @click="select(item)"
  :id="`${id}-item-${index}`"
  role="option"
  :aria-selected="highlightedIndex === index"
>
  <slot :item="item">
    {{ item[valueKey] }}
  </slot>
</li>

远程搜索

远程搜索会有个loading状态。

其实就是在fetch-suggestions函数中不会立即调用cb,组件内部getData方法中有个loading属性

不会立即设置为false。