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

1,196 阅读5分钟

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

简介

前文 中介绍了组件的整体结构以及单行文本输入功能的实现。本文将继续深入分析其组件生命周期、表单验证等内容,耐心读完,相信会对您有所帮助。

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

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

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

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

生命周期

在生命周期钩子createdmountedupdated中,添加监听事件,初始化组件状态。

created() {
  // 监听当前实例上的自定义 inputSelect 事件,用于快速选中输入控件的所有内容
  this.$on('inputSelect', this.select);
}, 
mounted() {
  this.setNativeInputValue(); // 设置原生输入控件的value值
  this.resizeTextarea(); // 设置文本域的大小
  this.updateIconOffset(); // 输入框头部/尾部(图标)元素偏移
}, 
// 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
updated() {
  // `updated` 不会保证所有的子组件也都被重新渲染完毕。
  //  使用 `vm.$nextTick` 确保整个视图都被重新渲染之后才会运行的代码
  this.$nextTick(this.updateIconOffset);
} 

组件对属性 valuenativeInputValuetype添加了侦听器,用于状态更新以及关联表单验证事件。

watch: {
  value(val) {
    this.$nextTick(this.resizeTextarea);
    // 开启输入时是否触发表单的校验
    if (this.validateEvent) {
      // 触发组件`FormItem`的自定义`el.form.change`事件,告知表单字段内容发生改变。
      this.dispatch('ElFormItem', 'el.form.change', [val]);
    }
  },
  // 原生输入控制value值处理
  nativeInputValue() {
    this.setNativeInputValue();
  }, 
  type() {
    // 组件渲染为 input 或者 textarea 
    // 类型切换会导致 DOM 也会发生改变 ,所以使用 `vm.$nextTick`。
    this.$nextTick(() => {
      this.setNativeInputValue();
      this.resizeTextarea();
      this.updateIconOffset();
    });
  }
},

下面逐一介绍下代这些方法的功能和作用。

select()

方法 select 用于选中输入控件的所有内容。

// methods
// 通过模板引用获取input/textarea实例
getInput() {
  return this.$refs.input || this.$refs.textarea;
}, 
// 选中输入控件的所有内容
select() {
  this.getInput().select();
},

setNativeInputValue()

方法setNativeInputValue用于设置原生控件的 value 属性,该属性时一个包含了文本框当前文字的DOMString。原生控件的value值默认是空字符串 ("").

计算属性nativeInputValue 用于将输入内容格式化成字符串。

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

在方法setNativeInputValue中使用nativeInputValue更新属性value值。

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

resizeTextarea()

方法resizeTextarea用来设置文本域的大小,这个讲解 <textarea>详细介绍。

updateIconOffset()

方法updateIconOffset 根据前置/后置内容元素的 offsetWidth,在水平方向移动头部/尾部内容元素。

updateIconOffset() {
  this.calcIconOffset('prefix'); //计算头部图标偏移
  this.calcIconOffset('suffix'); //计算尾部图标偏移
},
calcIconOffset(place) {
  // 根据 el-input__prefix/el-input__suffix 选中元素节点
  let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []); 
  let el = null;
  // 找到当前实例的头部/尾部元素节点
  for (let i = 0; i < elList.length; i++) {
    if (elList[i].parentNode === this.$el) {
      el = elList[i];
      break;
    }
  }
  // 映射关系 头部对应前置, 尾部对应后置
  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');
  }
}, 

v-model

指令v-model常用于在表单输入元素(<input><textarea> 、 <select>)创建双向绑定数据绑定。它会根据控件类型自动选取正确的方法来更新元素。

它会根据所使用的元素类型自动使用对应的 DOM 属性和事件组合:

  • 文本类型的 <input> 和 <textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"> 和 <input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件。

v-model 会忽略任何表单元素上初始的 valuechecked 或 selected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用data 选项来声明该初始值。

v-model 本质是个语法糖,以组件el-input为例

<el-input v-model="searchText" />

上面的代码其实等价于下面这段 (编译器会对 v-model 进行展开):

<input 
  :value="searchText" 
  @input="newValue => searchText = newValue" 
/>

所以在组件 input 的 props 选项里声明 value 时必须的,这也是文档中为什么会说属性 value/v-model 都是绑定值

当组件el-input属性 value 最新值需要更新到父组件属性searchText时,就会使用$emit触发实例 input 事件,实现双向绑定。

// @input="handleInput"  
handleInput(event) { 
  // ...
  
  // 触发当前实例上的input事件
  this.$emit('input', event.target.value); 
},

所以官方文档这句话 Input 为受控组件,它总会显示 Vue 绑定值 也就不难理解了。

单行文本输入 input

后置内容

后置内容不止用于展示图标,还提供了内容清空、密码显隐、输入文字统计以及表单验证状态图标等内容。

<!-- 后置内容 -->
<span class="el-input__suffix" v-if="getSuffixVisible()">
  <span class="el-input__suffix-inner">
    <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
      // 图标
    </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>

后置内容元素渲染由很多数据状态控制。

getSuffixVisible() {
  return this.$slots.suffix ||       // 传入插槽对象
    this.suffixIcon ||               // 设置尾部图标
    this.showClear ||                // 可清空
    this.showPassword ||             // 密码框
    this.isWordLimitVisible ||       // 输入长度限制
    // 输入时触发表单的校验 并显示校验结果图标
    (this.validateState && this.needStatusIcon);
}

可清空

使用clearable属性即可得到一个可清空的输入框。 计算属性showClear根据组件状态、输入内容等判断功能是否开启。

showClear() {
  return this.clearable &&
    !this.inputDisabled &&       // 非禁用
    !this.readonly &&            // 非只读
    this.nativeInputValue &&     // 值不为空
    (this.focused || this.hovering);  // 获得元素焦点或者鼠标悬停
},

图标绑定 click 事件 ,调用方法 clear ,更新v-model值,触发组件实例的changeclear等自定义事件。

clear() {
  this.$emit('input', '');  // 用于 v-model 更新
  this.$emit('change', '');
  this.$emit('clear');
},

密码框

使用show-password属性即可得到一个可切换显示隐藏的密码框。

showPwdVisible() {
  return (
    this.showPassword &&
    !this.inputDisabled &&    // 非禁用
    !this.readonly &&         // 非只读
    (!!this.nativeInputValue || this.focused) // 值不为空 或 获得元素焦点
  );
},

图标绑定 click 事件 ,调用方法 handlePasswordVisible ,更新内部属性passwordVisible值,因为密码显隐是通过渲染不同类型 (textpassword) 的input控件实现,此时DOM会重新渲染,所以使用$nextTick,调用方法focus重新获取元素的焦点。

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

// methods
handlePasswordVisible() {
  this.passwordVisible = !this.passwordVisible;
  this.$nextTick(() => {
    this.focus();
  });
},
// 获取焦点
focus() {
  this.getInput().focus();
},

输入长度限制

通过设置 show-word-limit 属性来展示字数统计。只能对类型为 text 或 textarea 的输入框生效, 使用原生maxlength属性限制最大输入长度 。

isWordLimitVisible() {
  return (
    this.showWordLimit &&
    this.$attrs.maxlength &&  // 原生`maxlength`属性 透传 attribute
    (this.type === "text" || this.type === "textarea") && //类型为 `text` 或 `textarea`
    !this.inputDisabled &&    // 非禁用
    !this.readonly &&         // 非禁用
    !this.showPassword        // 非密码框
  );
},

使用了计算属性 textLengthupperLimit 显示了输入进度。

image.png

计算属性upperLimit 返回最大输入长度,使用$attrs获取原生属性 maxlength值。

// 最大输入长度
upperLimit() {
  return this.$attrs.maxlength; // 透传 attributes 
},

计算属性textLength返回当前输入内容的长度

textLength() {
  if (typeof this.value === "number") {
    return String(this.value).length;
  } 
  return (this.value || "").length;
},

计算属性 inputExceed 判断是否输入超限,用于生成组件根元素的样式is-exceed

inputExceed() { 
  return this.isWordLimitVisible && this.textLength > this.upperLimit;
},

表单验证结果反馈图标

当组件在表单中使用,表单form设置属性status-icon为输入框添加了表示校验结果的反馈图标。

computed: {
  // 表单域下组件的校验状态 校验中/成功/失败
  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];
  }, 
} 

组件实现渲染效果如下: image.png

📚参考&关联阅读

"表单输入绑定",vuejs
"Input/text",MDN
"String",MDN

关注专栏

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

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