element-ui组件解读 (一) input组件

6,269 阅读5分钟

察见渊鱼者不详,智料隐匿者有殃。——《列子·说符》

简介


在日常开发PC管理端界面的时候,会用到element-ui或者iviewui框架,比较常用的input组件是怎么封装的呢,以element-ui框架的input组件为例,分析一下它是怎么封装实现的,也可以为以后自己封装框架提供思路。

因为代码还是挺多的,主要分析几个点:

  • 支持前置内容后置内容后置元素
  • 支持所有原声的type,并且可以切换password模式
  • 支持readonlydisabledautocompletemaxlengthminlength等等
  • 支持常规inputfocusblurchange,支持新事件compositionstartcompositionupdatecompositionend
  • element-ui是怎么做到类似与vue的双向绑定的
  • typetextare模式时,做到calcTextareaHeight

下面就一步一步分下,主要的点会着重分析,比较简单或者比较不重要的点会快速带过。

源码参考element-ui input

前置、后置

  <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单独处理,前后置又分为两类:

  • 一类直接通过prop传入prefix-iconsuffix-icon传入前后置icon-class显示不同的内容
  • 另一类是通过vue中的内容分发机制slot显示不同的内容,更灵活

在后置内容中也会处理clearablePwdVisibleshow-word-limit显示不同的内容。

原声type、其它原声属性

input为例,代码如下:

  // 其它地方省略
  <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中在检测输入长度是否超过设置的长度是有使用到如下:

  // 忽略部分代码
  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