Element 2 组件源码剖析之Input输入框(上)

2,716 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第17天,点击查看活动详情

简介

输入框组件 Input 通过鼠标或键盘输入表单域内容,提供复合型型输入框,带搜索的输入框,还可以进行大小选择。 本文将分析其源码实现,耐心读完,相信会对您有所帮助。🔗 组件文档 Input 🔗 gitee源码

为了更好的理解本文,请先阅读以下文章

  1. Input输入框(上) 单行输入实现
  2. Input输入框(中) 组件生命周期/事件
  3. Input输入框(下) 文本域 textarea

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!

模板HTML

组件的 props 声明,各属性功能描述详见官方文档Input#attributes 。

组件根元素是一个类名为el-textareael-input的 div 元素,元素内容包含了三部分:

  1. 封装原生 input 控件实现单行文本输入框。
  2. 封装原生 textarea 控件多行文本输入文本域。
// packages\input\src\input.vue
<template>
  <div :class="[type === 'textarea' ? 'el-textarea' : 'el-input',]" >
    <!-- 单行文本输入框 -->
    <template v-if="type !== 'textarea'">
      <!-- 输入框前置内容 -->
      <div class="el-input-group__prepend"></div> 
      <!-- 表单输入控件 -->
      <input>
      <!-- 输入框头部内容 -->
      <span class="el-input__prefix"></span>
      <!-- 输入框尾部内容 -->
      <span class="el-input__suffix"></span>
      <!-- 输入框后置内容 -->
      <div class="el-input-group__append"></div> 
    </template>
    <!-- 多行文本输入的文本域 -->
    <textarea></textarea>
  </div>
</template>

组件根元素

  • 属性type值确定使用 el-textareael-input
  • 组件尺寸inputSize生成样式 el-input--medium/small/mini
  • 禁用状态inputDisabled 生成样式 is-disabled
  • 开启输入长度限制,输入超限时inputExceed生成样式 is-disabled
  • 复合型输入框样式el-input-groupel-input-group--append/prepend根据前置/后置插槽内容生成。
  • 头部/尾部样式el-input--prefixel-input--suffix根据插槽内容或功能属性值生成。
  • 添加了鼠标移入移出事件,使用内部属性 hover 记录元素悬停状态。
<div :class="[
  type === 'textarea' ? 'el-textarea' : 'el-input',
  inputSize ? 'el-input--' + inputSize : '',
  {
    'is-disabled': inputDisabled,
    'is-exceed': inputExceed,
    'el-input-group': $slots.prepend || $slots.append,
    'el-input-group--append': $slots.append,
    'el-input-group--prepend': $slots.prepend,
    'el-input--prefix': $slots.prefix || prefixIcon,
    'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
  }
  ]"
  @mouseenter="hovering = true"
  @mouseleave="hovering = false"
>
  // ...
</div>

单行文本输入

单行文本输入框通过封装原生 input 控件实现,支持控件的原生属性。组件通过组合以下多种元素实现文本框 text或密码框password等复合型输入框功能:

  1. 输入框前置内容,提供具名插槽prepend,内容一般为标签或按钮。
  2. 原生 input 表单输入控件,添加了自定义事件。
  3. 输入框头部内容,可以通过 prefix-icon 或具名插槽prefix增加显示图标。
  4. 输入框尾部内容,可以通过 suffix-icon 或具名插槽suffix增加显示图标。该元素也用于展示输入框清空Icon、密码显隐切换Icon以及展示输入字数统计。
  5. 输入框后置内容,提供具名插槽append,内容一般为标签或按钮。
<template v-if="type !== 'textarea'">
  <!-- 输入框前置内容 -->
  <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" // 透传 Attributes 例如 placeholder
    :type="showPassword ? (passwordVisible ? 'text': 'password') : type" // text/password  也支持其他原生input的type值
    :disabled="inputDisabled" // 是否禁用
    :readonly="readonly"  // 是否只读
    :autocomplete="autoComplete || autocomplete" // 自动补全
    ref="input"
    @compositionstart="handleCompositionStart" // 输入法编辑器 (IME) 事件
    @compositionupdate="handleCompositionUpdate"
    @compositionend="handleCompositionEnd"
    @input="handleInput" // 输入内容
    @focus="handleFocus" // 获取焦点
    @blur="handleBlur" // 失去焦点
    @change="handleChange" // 输入值变化
    :aria-label="label" // ARIA 无障碍属性
  >
  <!-- 输入框头部内容 -->
  <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>
      // ...
    </span>
    // ...
  </span>
  <!-- 输入框后置内容 -->
  <div class="el-input-group__append" v-if="$slots.append">
    <slot name="append"></slot>
  </div>
</template>

组件渲染效果如下:

image.png

输入框头部/尾部都提供了具名插槽用于增加显示图标,当然也可以传入文本等其他内容,但不建议这么做。

当设置头部/尾部内容时, input 输入框通过属性 padding 提供了 30px 的宽度区域用于内容展示。头部/尾部元素使用绝对布局,将内容偏移覆盖至 padding 区域。

.el-input__prefix {
  position: absolute;
  height: 100%;
  left: 5px;
  top: 0;  
}

.el-input__suffix {
  position: absolute;
  height: 100%;
  right: 5px;
  top: 0; 
}
.el-input--prefix .el-input__inner {
    padding-left: 30px; 
}
.el-input--suffix .el-input__inner { 
    padding-right: 30px; 
}

image.png

以下示例将自定义文本传入插槽。

<el-input placeholder="请输入内容" v-model="input">
  <template slot="prefix">
    <span style="display: flex; align-items: center; height: 100%">头部内容</span>
  </template>
  <template slot="suffix">
    <span style="display: flex; align-items: center; height: 100%">尾部内容</span>
  </template>
</el-input>

示例渲染出现内容覆盖 image.png

input 元素类型

组件默认使用 text 类型的input控件。当设置 showPassword可得到一个可切换显示隐藏的密码框,内部属性passwordVisible记录显隐状态,根据不同的状态使用 passwordtext 类型。

:type="showPassword ? (passwordVisible ? 'text': 'password') : type"

属性 type 值也可设置为其他原生input的type值

<el-input v-model="input" placeholder="请输入内容" type="color"></el-input>
<el-input v-model="input" placeholder="请输入内容" type="date"></el-input>
<el-input v-model="input" placeholder="请输入内容" type="datetime-local"></el-input> 
<el-input v-model="input" placeholder="请输入内容" type="file"></el-input> 
<el-input v-model="input" placeholder="请输入内容" type="month"></el-input> 
<el-input v-model="input" placeholder="请输入内容" type="time"></el-input> 
<el-input v-model="input" placeholder="请输入内容" type="week"></el-input>

使用其他原生类型时组件渲染效果,但是一般不建议这么使用!一般组件类库都会提供对应的更加丰富的功能组件,使用库组件页面样式风格更加统一,提升用户交互体验。

image.png

输入法编辑器(IME)事件

输入法在中文、日文和韩文等少数语言中使用。以中文拼音输入法为例,输入的过程大致可以分为组字(composition)提交(commit) 两阶段。比如想打“你好”两个字,会在输入框输入“nihao”的拼音,当输入第一个字母“n”时,组字过程就开始了。此时本地的 IME(input method editor) 软件(比如微软/搜狗拼音输入法)会为我们提供组字框候选列表的 UI 组件。

关于输入法事件更多介绍,请阅读 Web 键盘输入法应用开发指南 —— 输入法事件

compositionstartcompositionupdatecompositionend是一组事件。

  1. 首先是使用拼音输入法开始输入汉字时 compositionstart 被触发,组字框和候选列表相应出现;

  2. 此后,每按一个新键,就会触发 compositionupdate,此时组字框和候选列表的内容也发生变化;

  3. 当选择了候选列表中的某个字或词,或者敲击空格(中文输入法),compositionend 事件会触发,表明输入被提交。

  4. 当取消输入时,比如使用鼠标单击页面空白处,就会终止当前输入,也会触发compositionend 事件。

属性isComposing值为 true表示用户正在输入。输入结束后(选择字词或者取消),调用方法handleInput,触发组件 input 事件。

// @compositionstart="handleCompositionStart"
// @compositionupdate="handleCompositionUpdate"
// @compositionend="handleCompositionEnd"

handleCompositionStart() {
  this.isComposing = true; // 正在输入
},
handleCompositionUpdate(event) {
  const text = event.target.value;
  const lastCharacter = text[text.length - 1] || '';
  // 韩文字符编码 判断最后一个字符是否特殊的功能键 Process Key 
  this.isComposing = !isKorean(lastCharacter); 
},
handleCompositionEnd(event) {
  // 输入结束后,触发 input 事件
  if (this.isComposing) {
    this.isComposing = false;
    this.handleInput(event);
  }
},

input/change 事件

  • <input><select><textarea>  元素的 value 被修改时,会触发 input 事件。
  • 当用户更改<input><select><textarea> 元素的值并提交这个更改时,change 事件在这些元素上触发。基于表单元素的类型和用户对标签的操作的不同,change 事件触发的时机也不同。

对于文本输入元素,比如 <input type="text">,每当元素的 value 改变,input 事件都会被触,change 事件在控件失去焦点后才会触发。

// @input="handleInput" 
// @change="handleChange"

handleInput(event) {
  // 输入法下用户正在输入
  if (this.isComposing) return;

  // IE 11下 DatePicker组件 hack 写法,详见issues
  // 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;
  
  // 触发触发当前实例上的input事件
  this.$emit('input', event.target.value);
 
  // Input 为受控组件 更新组件的绑定值
  this.$nextTick(this.setNativeInputValue);
},
handleChange(event) {
  // 触发触发当前实例上的change事件
  this.$emit('change', event.target.value);
},

对于需要使用输入法的语言, v-model 不会在输入法组合文字过程中得到更新,因为compositionend事件没触发是不会执行handleInput逻辑。

if (this.isComposing) return;

focus/blur 事件

元素获取或失去焦点时,调用事件监听方法,更新内部属性focused记录元素焦点状态,同时触发实例自定义 focusblur 事件。

// @focus="handleFocus"
// @blur="handleBlur"

handleFocus(event) {
  this.focused = true;
  // 触发触发当前实例上的focus事件
  this.$emit('focus', event);
},
handleBlur(event) {
  this.focused = false;
  // 触发触发当前实例上的blur事件
  this.$emit('blur', event);
  // 开启输入时是否触发表单的校验  
  if (this.validateEvent) {
    // 触发组件`FormItem`的自定义`el.form.blur`事件。
    this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
  } 
},

provide/inject 依赖注入

在 Form 组件中,每一个表单域由一个 Form-Item 组件构成,表单域中可以放置各种类型的表单控件,包括 Input、Select、Checkbox、Radio、Switch、DatePicker、TimePicker等。

表单form和表单域form-item使用provide 选项指定给后代组件的状态,避免了 prop 逐级透传

// packages\form\src\form.vue
provide() {
  return {
    elForm: this
  };
},

// packages\form\src\form-item.vue
provide() {
  return {
    elFormItem: this
  };
}, 
inject: ['elForm'],
computed: { 
  // ...
  _formSize() {
    return this.elForm.size;
  },
  elFormItemSize() {
    return this.size || this._formSize;
  },
  sizeClass() {
    return this.elFormItemSize || (this.$ELEMENT || {}).size;
  }
}, 

组件 input 使用inject选项注入上层组件提供的数据,如尺寸、校验、禁用,用于组件内部状态的控制计算。

// packages\input\src\input.vue
inject: {
  elForm: {
    default: ''
  },
  elFormItem: {
    default: ''
  }
},
computed: {
  // 表单域下组件的尺寸
  _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];
  },
  // 组件尺寸大小
  inputSize() {
    return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
  },
  // 组件禁用状态
  inputDisabled() {
    return this.disabled || (this.elForm || {}).disabled;
  },
}


// this.$ELEMENT 来源于组件库的全局注册
const install = function(Vue, opts = {}) { 
  Vue.prototype.$ELEMENT = {
    size: opts.size || '',
    zIndex: opts.zIndex || 2000
  }; 
  // ...
};

由于篇幅原因,内容就到这里了。下文将详细介绍组件的生命周期。

📚参考&关联阅读

"表单输入绑定",vuejs
"input_event",MDN
"change_event",MDN

关注专栏

如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!

此文章已收录到专栏中 👇,可以直接关注。