Element-ui 之 Input 组件中 Input 部分的源码分析

一. Input 组件的功能

(1)可禁用。

(2)可清空。

(3)可以作为密码框。

(4)可以在前面或后面增加 icon

(5)可以调整尺寸。

(6)可以限制输入文字的长度。

二. Input 组件提供的属性、插槽、事件、方法

由于 Input 组件提供的属性较多,需要先将 Input 组件提供的属性进行分类,一类一类的去讲解,最后完成整个 Input 组件。首先,来将 Input 组件的属性进行分类:

2.1 属性

类别一:绑定值属性

序号 参数 说明 类型 可选值 默认值
1 value /v-model 绑定值 String/Number -- --

类别二:原生属性

序号 参数 说明 类型 可选值 默认值
1 type 类型 String text,textarea 和其他原生 input 的 type 值 text
2 maxlength 原生属性,最大输入长度 number -- --
3 minlength 原生属性,最小输入长度 number -- --
4 placeholder 输入框占位文本 string -- --
5 disabled 禁用 boolean -- false
6 autocomplete 原生属性,自动补全 string on, off off
7 name 原生属性 string -- --
8 readonly 原生属性,是否只读 boolean -- --
9 max 原生属性,设置最大值 -- -- --
10 min 原生属性,设置最小值 -- -- --
11 step 原生属性,设置输入字段的合法数字间隔 -- -- --
12 autofocus 原生属性,自动获取焦点 boolean true, false false

类别三:可清空属性

序号 参数 说明 类型 可选值 默认值
1 clearable 是否可清空 boolean -- false

类别四:显示密码图标属性

序号 参数 说明 类型 可选值 默认值
1 show-password 是否显示切换密码图标 boolean -- false

类别五:字数统计属性

序号 参数 说明 类型 可选值 默认值
1 show-word-limit 是否显示输入字数统计,只在 type=text 或 type=textarea 时有效 boolean -- false

类别六:尺寸与图标属性

序号 参数 说明 类型 可选值 默认值
1 size 输入框尺寸,只在 type != textarea 时有效 string medium / small / mini --
2 prefix-icon 输入框头部图标 string -- --
3 suffix-icon 输入框尾部图标 string -- --

类别七:屏幕阅读器相关属性

序号 参数 说明 类型 可选值 默认值
1 label 输入框关联的label文字 string -- --
2 tabindex 输入框的tabindex string -- --

2.2 插槽

序号 name 说明
1 prefix 输入框头部内容,只对 type=text 有效
2 suffix 输入框尾部内容,只对 type=text 有效
3 prepend 输入框前置内容,只对 type=text 有效
4 append 输入框后置内容,只对 type=text 有效

2.3 事件

| 序号 | 事件 | 说明 | 回调参数 |
|----|--------|-----------------------------|----------------|---------|
| 1 | blur | 在 Input 失去焦点时触发 | (event: Event) |
| 2 | focus | 在 Input 获得焦点时触发 | (event: Event) |
| 3 | change | 仅在输入框失去焦点或用户按下回车时触发 | (value: string | number) |
| 4 | input | 在 Input 值改变时触发 | (value: string | number) |
| 5 | clear | 在点击由 clearable 属性生成的清空按钮时触发 | -- |

2.4 方法

序号 方法名 说明
1 focus 使 input 获取焦点
2 blur 使 input 失去焦点
3 select 选中 input 中的文字

三. Input 组件的具体实现

3.1 注册组件并实现基本HTML结构

3.1.1 组件的注册

components 文件夹里面新建一个 input 文件夹,里面新建 input.vue 文件。设置组件的 namecomponentName 都为 ElInput

main.js 文件中引入该组件,使用 Vue.component 完成组件的注册。

js 复制代码
import Input from './components/input/input';

const components = [
  ...
  Input
]

components.forEach(component => {
  Vue.component(component.name, component);
});

3.1.2 Input 组件的基本 template 部分

基本的 template 部分是一个 div 包裹一个 input,之后在此基础上增加 input 的功能。

html 复制代码
<template>
  <div class="el-input">
    <input class="el-input__inner" />
  </div>
</template>
<script>
export default {
  name: 'ElInput',

  componentName: 'ElInput',
}
</script>

3.1.3 引入组件部分

新建一个文件,文件内部引入 input 组件。

html 复制代码
<template>
  <div>
    <el-input></el-input>
  </div>
</template>
<script>
export default {
  
}
</script>

3.2 实现每一类属性的功能

3.2.1 绑定值属性 value/v-model

实现步骤:

  1. 在引入 input 组件时绑定 v-model
html 复制代码
<template>
  <div>
    <el-input v-model="val"></el-input>
  </div>
</template>
<script>
export default {
  data() {
    return {
      val: '输入框值'
    }
  }
}
</script>
  1. 组件内部接收 value 值,并且设置计算属性 nativeInputValue,监听 value 值的改变来给 input 赋值。
html 复制代码
<template>
  <div class="el-input">
    <input
      class="el-input__inner"
      ref="input"
      @input="handleInput"
    />
  </div>
</template>
<script>
export default {
  props: {
    value: [String, Number],
  },
  computed: {
    // 监听 value 值的变化,如果 value 为 null 或者 undefined,则返回空,否则返回 value 转化为字符串后的值
    nativeInputValue() {
      return this.value === null || this.value === undefined ? '' : String(this.value);
    }
  },
  watch: {
    // 监听 value 值的变化,给 input 赋值
    nativeInputValue() {
      this.setNativeInputValue();
    }
  },
  methods: {
    // 获取 input
    getInput() {
      return this.$refs.input || this.$refs.textarea;
    },
    // 设置 input 元素的值
    setNativeInputValue() {
      // 获取到 input 元素
      const input = this.getInput();
      if (!input) return;
      // 如果 input 之前的 value 值与改变后的值相同,则 return
      if (input.value === this.nativeInputValue) return;
      // 将 input 的值赋值为最新的值
      input.value = this.nativeInputValue;
    },
    // 触发 input 事件
    handleInput(event) {
      // 解决 IE 浏览器中 Input 初始化自动执行的问题
      if (event.target.value === this.nativeInputValue) return;
      // 触发父级的 input 事件
      this.$emit('input', event.target.value);
      // 给 input 重新赋值
      this.$nextTick(this.setNativeInputValue);
    }
  },
  mounted() {
    // 设置 input 元素的值
    this.setNativeInputValue();
  }
}
</script>
  1. 处理拼音输入时 input 值的变化问题。

前两步绑定 v-model 值还存在一个小的问题,如果是输入的拼音,引入组件绑定 input 方法拿到的 value 是包含在输入法输入的拼音的那部分的,所以在还未输入到输入框时,不能设置 inputvalue 值,也不能触发父级的 input 方法。

通过设置变量 isComposing 来去判断是否是输入法输入(比如说输入拼音),如果是输入拼音,则不触发 input,如果拼音已经输入完成,则触发 input

引入 compositionstart、compositionupdate、compositionend 事件

以输入拼音为例,详细介绍这三个事件的触发时机:

(1)compositionstart:输入法编辑器开始新的输入合成时(开始输入拼音时)触发。

(2)compositionupdate:组合输入更新时(每输入一下拼音时)都会触发。

(3)compositionend:组合输入结束时(拼音输入结束,关闭中文输入法时)触发。

html 复制代码
<template>
  <div class="el-input">
    <input
      ...
      @compositionstart="handleCompositionStart"
      @compositionupdate="handleCompositionUpdate"
      @compositionend="handleCompositionEnd"
      @input="handleInput"
    />
  </div>
</template>
<script>
export default {
  data() {
    return {
      isComposing: false, // 是否处于拼音输入状态
    }
  },
  methods: {
    // 输入法编辑器开始新的输入合成时(开始输入拼音时)触发
    handleCompositionStart(event) {
      // 触发父级的 compositionstart 事件
      this.$emit('compositionstart', event);
      // 将 isComposing 设置为 true,说明处于拼音输入的状态
      this.isComposing = true;
    },
    // 组合输入更新时(每输入一下拼音时)都会触发
    handleCompositionUpdate(event) {
      // 触发父级的 compositionupdate 事件
      this.$emit('compositionupdate', event);
      // 将 isComposing 设置为 true,说明处于拼音输入的状态
      this.isComposing = true;
    },
    // 组合输入结束时(拼音输入结束,关闭中文输入法时)触发
    handleCompositionEnd(event) {
      // 触发父级的 compositionend 事件
      this.$emit('compositionend', event);
      if (this.isComposing) {
        // 将 isComposing 设置为 false,说明此时已经不是拼音输入的状态
        this.isComposing = false;
        // 触发 handleInput 方法,给 input 赋值
        this.handleInput(event);
      }
    },
    handleInput(event) {
      // 如果正在输入拼音,直接 return
      if (this.isComposing) return;
      ...
    }
  }
}
</script>

其中 handleCompositionUpdate 在 element-ui 源代码中有一个是否是韩语的判断,如果是韩语则设置为 false,不是设置为 true,这里就不处理韩语了,统一在输入法键盘输入时设置为 true

3.2.2 原生属性

inheritAttrs 属性以及 v-bind="$attrs"

默认情况下父作用域的不被认做 props 的属性绑定将会作为普通的 HTML 属性应用在子组件的根元素上。通过设置 inheritAttrsfalse,将不会被默认绑定到子组件的根元素上。通过属性 $attrs 可以获取到这些属性显性的绑定到非根元素上。

inheritAttrs 设置为 false 不会影响到 classstyle 的绑定。

实现步骤:

通过设置 inheritAttrs: false 避免不被认做 props 的属性默认被绑定在 input 组件的根元素上。然后在 input 元素上使用 v-bind="$attrs" 来绑定传入的不被认做 props 的属性。

html 复制代码
<template>
  <div class="el-input">
    <input
      ...
      v-bind="$attrs"
      ...
    />
  </div>
</template>
<script>
export default {
  inheritAttrs: false,
}
</script>

通过这样绑定后,可以不做特殊处理直接去绑定的原生属性有:maxlength、minlength、placeholder、autocomplete、name、max、min、step、autofocus

需要特殊处理的原生属性有:

(1)type:需要根据 type 去判断是 text 还是 textarea 去显示不同的控件。

(2)disabled:由于 input 可以放在 form 表单里面,所以需要单独获取 disabled 属性,然后设置计算属性去监听 inputdisabled 的变化和 form 表单的 disabled 变化,优先获取 inputdisabled 状态。

(3)readonlyreadonly 属性需要和"显示清空"、"显示密码框"、"计数功能"相互作用,所以需要单独用 props 接收 readonly 属性。

3.2.2.1 原生属性 ------ type 的处理

实现步骤:

  1. props 接收 type 属性,并且根据 type 属性去展示不同的样式 class,在 input 元素中绑定 type 属性。
html 复制代码
<template>
  <div :class="[
    type === 'textarea' ? 'el-textarea' : 'el-input',
    ]"
  >
    <template v-if="type !== 'textarea'">
      <input :type="type" />
    </template>
  </div>
</template>
<script>
export default {
  props: {
    type: {
      type: String,
      default: 'text'
    },
  }
}
</script>
  1. 监听 type 的变化,如果 type 变化了执行 setNativeInputValue 方法,重新给组件赋值。
js 复制代码
watch: {
  type() {
    this.$nextTick(() => {
      this.setNativeInputValue();
    });
  }
}

3.2.2.2 原生属性 ------ disabled 的处理

props 接收 disabled 属性,然后设置计算属性 inputDisabled 变量监听 disabled 的改变(后续还会监听 form 表单的 disabled 属性的改变)。然后将 disabled 属性和其样式 class 绑定在元素上。

html 复制代码
<template>
  <div :class="[
    {
      'is-disabled': inputDisabled,
    }
    ]"
  >
    <template v-if="type !== 'textarea'">
      <input :disabled="inputDisabled" ... />
    </template>
  </div>
</template>
<script>
export default {
  props: {
    disabled: Boolean,
  },
  computed: {
    // 监听 disabled 属性的变化
    inputDisabled() {
      return this.disabled;
    },
  }
}
</script>

3.2.2.3 原生属性 ------ readonly 的处理

接收 readonly 属性,待后续需要时会用到。

html 复制代码
<script>
export default {
  readonly: Boolean,
}
</script>

3.2.3 可清空属性

3.2.3.1 可清空图标的显示情况

可清空图标是在输入框有值,并且输入框聚焦或者鼠标移入输入框的情况下显示的。点击清空图标可以讲输入框内容清空。

3.2.3.2 可清空功能的实现步骤

  1. 设置两个变量 hovering、focused 分别监听 mouseenter/mouseleavefocus/blur 属性,为显示可清空图标做准备。
html 复制代码
<template>
  <div ...
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <template v-if="type !== 'textarea'">
      <input ...
        @focus="handleFocus"
        @blur="handleBlur"
      />
    </template>
  </div>
</template>
<script>
export default {
  data() {
    return {
      hovering: false,
      focused: false,
      ...
    }
  },
  methods: {
    handleBlur(event) {
      this.focused = false;
      this.$emit('blur', event);
    },
    handleFocus(event) {
      this.focused = true;
      this.$emit('focus', event);
    },    
  },
}
</script>
  1. 接收可清空属性,并且设置计算属性 判断显示清空图标的条件。
html 复制代码
<script>
export default {
  props: {
    clearable: {
      type: Boolean,
      default: false
    },
  },
  computed: {
    showClear() {
      // 在可清空属性设置为 true ,且 input 有值且不为禁用或只读状态,且鼠标移入输入框内部或输入框聚焦,则展示可清空图标
      return this.clearable &&
        !this.inputDisabled &&
        !this.readonly &&
        this.nativeInputValue &&
        (this.focused || this.hovering);
    },
  }
}
</script>
  1. 将清空图标放在 input 组件的后置内容中。
html 复制代码
<template>
  <div>
    <template v-if="type !== 'textarea'">
      <input .../>
      <span
        class="el-input__suffix"
        v-if="getSuffixVisible()">
        <!-- 方法 getSuffixVisible 来判断后置内容是否显示 -->
        <span class="el-input__suffix-inner">
          <i v-if="showClear"
            class="el-input__icon el-icon-circle-close el-input__clear"
          ></i>
        </span>
      </span>
    </template>
  </div>
</template>

方法 getSuffixVisible

js 复制代码
methods: {
  // 目前只判断 showClear 是否为 true,后续增加密码图标等功能时还会继续增加判断
  getSuffixVisible() {
    return this.showClear;
  }
}

将后置内容的 class 加在最外层的 div 上面,这个 class 是让 inner 部分的 padding-right 加宽一些:

html 复制代码
<div :class="[
    ...
    {
      ...
      'el-input--suffix': clearable
    }
    ]"
>
</div>
  1. 点击清空按钮执行清空功能。
    给清空按钮绑定 click 事件,执行 clear 方法:
html 复制代码
<i v-if="showClear"
  class="el-input__icon el-icon-circle-close el-input__clear"
  @click="clear"
></i>
js 复制代码
clear() {
  // 触发父级的 input 事件,并且将值传为空,由于父级用 v-model 语法糖绑定值,则可清空组件的值
  this.$emit('input', '');
  // 触发父级的 change 事件
  this.$emit('change', '');
  // 触发父级的 clear 事件
  this.$emit('clear');
},

此时点击清空后,输入框中的内容就被清空了,但是还有一个问题,输入框在清空后失去焦点了,往往用户的操作习惯是清空后继续输入,所以不能让输入框失去焦点。

所以需要在清空按钮处增加 @mousedown.prevent,因为 mousedown 事件默认行为是除了点击对象外,所有焦点对象失去焦点,只要增加 @mousedown.prevent,输入框就不会失去焦点了。

html 复制代码
<!-- @mousedown.prevent 用于阻止输入框失去焦点 -->
<i v-if="showClear"
   class="el-input__icon el-icon-circle-close el-input__clear"
   @mousedown.prevent
   @click="clear"
></i>

3.2.4 显示密码图标属性

3.2.4.1 密码图标的作用及显示情况

  1. 作用:将用户输入的内容显示成小圆点的密文形式。
  2. 密码图标的显示情况:当输入框设置了 show-password 属性,并且输入框有值或者输入框在聚焦的情况下,显示密码图标。

3.2.4.2 密码图标功能的实现步骤

  1. 显示密码图标。
    props 接收 show-password 属性,并且设置计算属性 showPwdVisible 判断密码图标的显示条件,再将密码图标放在组件的后置内容中。
js 复制代码
props: {
  showPassword: {
    type: Boolean,
    default: false
  },
}

computed: {
  showPwdVisible() {
    // 当显示密码图标属性被设置为 true 时,且输入框不属于禁用、只读状态,且输入框有值或者输入框在聚焦的情况下,显示密码图标
    return this.showPassword &&
      !this.inputDisabled &&
      !this.readonly &&
      (!!this.nativeInputValue || this.focused);
  },
}

将密码图标放在组件的后置内容中,并且修改 getSuffixVisible 方法,将密码图标显示出来。

html 复制代码
<span
  class="el-input__suffix"
  v-if="getSuffixVisible()"
>
  <span class="el-input__suffix-inner">
    ...
    <i v-if="showPwdVisible"
        class="el-input__icon el-icon-view el-input__clear"
    ></i>
  </span>
</span>

getSuffixVisible 方法增加 showPassword 的判断:

js 复制代码
getSuffixVisible() {
  return this.showClear || this.showPassword;
}

el-input--suffixclass 加上 showPassword 的判断:

html 复制代码
<div :class="[
  ...
  {
    ...
    'el-input--suffix': clearable || showPassword
  }
  ]"
>
</div>
  1. 点击密码图标进行明文和密文的切换。
    设置 passwordVisible 变量,作为明文和密文的类型判断。通过改变 input 元素的 type 属性(显示明文时 type=text,显示密码时 type=password)。点击密码图标时切换变量 passwordVisible
js 复制代码
data() {
  return {
    passwordVisible: false
  }
},

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

修改绑定的 type 属性,增加 passwordVisible 的判断,在 showPasswordtrue 的情况下,判断是否已经展示了密码,如果是明文展示的,则 typetext,否则为 password

html 复制代码
<input
  :type="showPassword ? (passwordVisible ? 'text': 'password') : type"
/>

3.2.5 字数统计属性

3.2.5.1 字数统计属性实现思路

  1. 接收传入的 show-word-limit 属性。
  2. 判断字数统计属性在何种情况下显示:
    (1)show-word-limit 为 true
    (2)传入了 max-length
    (3)是 text 类型或者 textarea 类型
    (4)不是禁用或只读状态
    (5)show-password 不为 true,也就是不是密码框
  3. 设置计算属性 upperLimit 来获取 maxlength 的值。
  4. 设置计算属性 textLength 来实时获取 value 值的长度。
  5. 设置计算属性 inputExceed 判断是否超出字数。
  6. 页面显示字数统计样式,超出最大字数的样式。

3.2.5.2 字数统计属性代码实现

js 复制代码
// 接收传入的 show-word-limit 属性
props: {
  showWordLimit: {
      type: Boolean,
      default: false
    },
}
computed: {
  // 判断字数统计属性在何种情况下显示
  isWordLimitVisible() {
    // 当showWordLimit属性为true,且有最大字数限制,且类型为 text 或 textarea,且不为禁用、只读、密码的状态时,显示字数统计
    return this.showWordLimit &&
      this.$attrs.maxlength &&
      (this.type === 'text' || this.type === 'textarea') &&
      !this.inputDisabled &&
      !this.readonly &&
      !this.showPassword;
  },
  // 设置计算属性 upperLimit 来获取 maxlength 的值
  upperLimit() {
    return this.$attrs.maxlength;
  },
  // 获取 value 值的长度
  textLength() {
    if (typeof this.value === 'number') {
      return String(this.value).length;
    }

    return (this.value || '').length;
  },
  // 判断字数是否超出限制
  inputExceed() {
    // 如果显示字数统计且字数超限,则说明字数超出限制
    return this.isWordLimitVisible &&
      (this.textLength > this.upperLimit);
  }
}
// 在方法 getSuffixVisible 上面增加 showWordLimit 的判断
getSuffixVisible() {
  return this.showClear|| 
    this.showPassword || 
    this.isWordLimitVisible;
}

页面结构部分:

html 复制代码
<!-- 字数统计样式 -->
<span class="el-input__suffix-inner">
  <span v-if="isWordLimitVisible" class="el-input__count">
    <span class="el-input__count-inner">
      {{ textLength }}/{{ upperLimit }}
    </span>
  </span>
</span>
<!-- 超出最大字数的样式,在最外层增加样式 -->
:class="[
  {'is-exceed': inputExceed,}
]"

3.2.6 尺寸与图标属性

3.2.6.1 尺寸属性

实现思路:

  • props 接收 size 属性,并且设置计算属性 inputSize 监听 size 的改变。(后续结合 form 组件,在计算属性里面还需要监听 formsize)。
  • 在最外层根据不同的 size 增加不同的样式 class

代码实现:

js 复制代码
// props 接收 size 属性
props: {
  size: String,
}

// 计算属性 inputSize 监听 size 的改变
computed: {
  inputSize() {
    return this.size;
  },
}
html 复制代码
<!-- 根据不同的 size 增加不同的样式 class -->
<!-- 不同的尺寸的高度和字号不同 -->
inputSize ? 'el-input--' + inputSize : '',

3.2.6.2 图标属性

实现思路:

  • props 接收 suffixIconprefixIcon 属性。
  • 增加 suffixprefixclass 样式。
  • HTML 中增加前置图标和后置图标元素。

代码实现:

js 复制代码
// props 接收 suffixIcon 和 prefixIcon 属性
props: {
   suffixIcon: String,
   prefixIcon: String,
}
html 复制代码
<!-- 增加 suffix 和 prefix 的 class 样式 -->
'el-input--prefix': prefixIcon,
'el-input--suffix': suffixIcon || clearable || showPassword

<!-- 在 HTML 中增加前置图标元素 -->
<span class="el-input__prefix" v-if="prefixIcon">
  <i class="el-input__icon"
    v-if="prefixIcon"
    :class="prefixIcon">
  </i>
</span>

<!-- 在 HTML 中增加后置图标元素 -->
<span class="el-input__suffix-inner">
  <template v-if="suffixIcon">
    <i class="el-input__icon"
      v-if="suffixIcon"
      :class="suffixIcon">
    </i>
  </template>
</span>
js 复制代码
getSuffixVisible() {
    return this.suffixIcon ||
      this.showClear|| 
      this.showPassword || 
      this.isWordLimitVisible;
}

3.2.7 屏幕阅读器相关属性

增加 aria-labeltabIndex 属性。
aria-label 属性用于没有给输入框设计对应的 label 文本位置时,aria-label 为读屏软件提供描述信息。

代码实现:

js 复制代码
// 接收 label 和 tabIndex 属性
props: {
  label: String,
  tabindex: String
}
html 复制代码
<!-- 绑定在 input 元素上 -->
<input
  :aria-label="label"
  :tabindex="tabindex"
/>

3.3 实现插槽功能

3.3.1 实现 prefix 和 suffix 插槽

(1)prefixsuffix 位置描述
prefixsuffix 是输入框头部和尾部的内容,展示在输入框的内部,和传入的头部图标尾部图标展示的位置一致。

(2)代码实现

  • 分别在 classel-input__prefixel-input__suffix 元素的内部,采用具名插槽的形式,将传入的元素插入。
  • v-if 显示判断增加 $slots.prefix$slots.suffix
  • 最外层的 class 增加 $slots.prefix$slots.suffix 的判断。
html 复制代码
<!-- 前置内容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
  <slot name="prefix"></slot>
  ...
</span>
  
<!-- 后置内容 -->
<span class="el-input__suffix-inner">
  <template v-if="$slots.suffix || suffixIcon">
    <slot name="suffix"></slot>
    ...
  </template>
</span>
  • 在方法 getSuffixVisible 增加 this.$slots.suffix 的显示判断。
js 复制代码
getSuffixVisible() {
  return this.$slots.suffix ||
    this.suffixIcon ||
    this.showClear|| 
    this.showPassword || 
    this.isWordLimitVisible;
}

3.3.2 实现 prepend 和 append 插槽

(1)prependappend 位置描述

prependappend 是输入框前置和输入框后置的内容,展示在输入框的外部。

(2)将 prependappend 元素放置在组件中

  • 在组件的HTML结构中增加前置元素和后置元素。
  • 增加前置元素和后置元素的 class
html 复制代码
<!-- 在 type!=textarea 的 template 元素中增加前置元素和后置元素 -->
<!-- 前置元素 -->
<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>

在最外层增加前置元素和后置元素的 class
'el-input-group': $slots.prepend || $slots.append,
'el-input-group--append': $slots.append,
'el-input-group--prepend': $slots.prepend,

(3)重新计算 prefixsuffix 的位置

由于 prefixsuffix 的位置是参照输入框绝对定位的元素,所以在增加了 prependappend 元素后,如果输入框还存在 prefixsuffix 元素,需要将其的横向位置分别向右或者向左进行移动,避免图标位置与 prefixsuffix 的位置重叠。

计算图标横向位移的方法:

js 复制代码
updateIconOffset() {
  // 提供 calcIconOffset 方法接收 prefix 和 suffix 两个参数
  this.calcIconOffset('prefix');
  this.calcIconOffset('suffix');
},

calcIconOffset 方法(用于计算图表横向位移):

js 复制代码
calcIconOffset(place) {
  // 获取到 class 为 el-input__prefix 或 el-input__suffix
  let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
  if (!elList.length) return;
  // 设置变量 el
  let el = null;
  for (let i = 0; i < elList.length; i++) {
    // 如果获取到的 el-input__prefix 或 el-input__suffix 的父级元素就是该组件的根元素
    if (elList[i].parentNode === this.$el) {
      // 则将 el-input__prefix 或 el-input__suffix 赋值给 el 变量
      el = elList[i];
      break;
    }
  }
  if (!el) return;
  const pendantMap = {
    suffix: 'append',
    prefix: 'prepend'
  };
  
  const pendant = pendantMap[place];
  if (this.$slots[pendant]) {
    // 如果存在前置元素或后置元素,则获取前置元素或后置元素的 offsetWidth,将 el 向右或向左移动对应的宽度
    el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
  } else {
    // 如果不存在则 el 移除 style 属性
    el.removeAttribute('style');
  }
},

mountedupdated 钩子函数中调用 updateIconOffset,在载入后和更新后都需要重新计算图标的位置。

js 复制代码
mounted() {
  ...
  this.updateIconOffset();
},
updated() {
  this.$nextTick(this.updateIconOffset);
}

3.4 补齐事件和方法

3.4.1 补齐事件

目前已经实现的事件有 blurfocusinputclear,未实现的有 change

input 元素上面增加 change 事件,执行 handleChange 方法,方法内部采用 emit 触发父组件的 change 事件。

js 复制代码
// handleChange 方法
handleChange(event) {
  this.$emit('change', event.target.value);
},

3.4.2 补齐方法

目前对外提供的可以调用到的方法有 focus,还需要提供 blurselect

blur 方法:

js 复制代码
blur() {
  this.getInput().blur();
},

select 方法:

js 复制代码
select() {
  this.getInput().select();
},

四. 整体代码详解

包含 Input 与 Form 组件相互作用的部分。

html 复制代码
<template>
  <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--suffix': clearable || showPassword,
      'el-input--prefix': $slots.prefix || prefixIcon,
      'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
    }
    ]"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
    <template v-if="type !== 'textarea'">
      <!-- 前置元素 -->
      <div class="el-input-group__prepend" v-if="$slots.prepend">
        <slot name="prepend"></slot>
      </div>
      <input
        class="el-input__inner"
        v-bind="$attrs"
        :type="showPassword ? (passwordVisible ? 'text': 'password') : type"
        :disabled="inputDisabled"
        ref="input"
        @compositionstart="handleCompositionStart"
        @compositionupdate="handleCompositionUpdate"
        @compositionend="handleCompositionEnd"
        @input="handleInput"
        @focus="handleFocus"
        @blur="handleBlur"
        @change="handleChange"
        :aria-label="label"
        :tabindex="tabindex"
      />
      <!-- 前置内容 -->
      <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="$slots.suffix || suffixIcon">
            <slot name="suffix"></slot>
            <i class="el-input__icon"
              v-if="suffixIcon"
              :class="suffixIcon">
            </i>
          </template>
          <!-- @mousedown.prevent 用于阻止输入框失去焦点 -->
          <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>
    <textarea
      v-else
      :tabindex="tabindex"
      class="el-textarea__inner"
      @compositionstart="handleCompositionStart"
      @compositionupdate="handleCompositionUpdate"
      @compositionend="handleCompositionEnd"
      @input="handleInput"
      ref="textarea"
      v-bind="$attrs"
      :disabled="inputDisabled"
      :readonly="readonly"
      :style="textareaStyle"
      @focus="handleFocus"
      @blur="handleBlur"
      @change="handleChange"
      :aria-label="label"
    >
    </textarea>
    <span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
  </div>
</template>
<script>
import calcTextareaHeight from './calcTextareaHeight';
import emitter from '../../utils/mixins/emitter';

export default {
  name: 'ElInput',

  componentName: 'ElInput',
  
  mixins: [emitter],
  
  inheritAttrs: false,
  
  // inject 去接收 Form 组件和 Form-Item 组件传过来的数据
  inject: {
    elForm: {
      default: ''
    },
    elFormItem: {
      default: ''
    }
  },
  
  data() {
    return {
      textareaCalcStyle: {},
      hovering: false,
      focused: false,
      isComposing: false, // 是否处于拼音输入状态
      passwordVisible: false
    }
  },
  
  props: {
    value: [String, Number],
    size: String,
    resize: String,
    disabled: Boolean,
    readonly: Boolean,
    type: {
      type: String,
      default: 'text'
    },
    autosize: {
      type: [Boolean, Object],
      default: false
    },
    // 输入时是否触发表单的校验
    validateEvent: {
      type: Boolean,
      default: true
    },
    suffixIcon: String,
    prefixIcon: String,
    clearable: {
      type: Boolean,
      default: false
    },
    showPassword: {
      type: Boolean,
      default: false
    },
    showWordLimit: {
      type: Boolean,
      default: false
    },
    label: String,
    tabindex: String
  },
  
  computed: {
    // 获取 Form-Item 组件的 elFormItemSize 变量
    _elFormItemSize() {
      return (this.elFormItem || {}).elFormItemSize;
    },
    // 接收 Form-Item 组件传过来的表单项验证状态 validateState
    validateState() {
      return this.elFormItem ? this.elFormItem.validateState : '';
    },
    // 接收 Form 组件传过来的是否显示校验结果反馈图标 statusIcon 属性
    needStatusIcon() {
      return this.elForm ? this.elForm.statusIcon : false;
    },
    // 根据 validateState 判断显示的图标
    validateIcon() {
      return {
        validating: 'el-icon-loading',
        success: 'el-icon-circle-check',
        error: 'el-icon-circle-close'
      }[this.validateState];
    },
    textareaStyle() {
      return Object.assign({}, this.textareaCalcStyle, { resize: this.resize });
    },
    // Input 的 size
    inputSize() {
      // 优先级:组件本身的 size > Form-Item 的 size
      return this.size || this._elFormItemSize;
    },
    // Input 的 disabled
    inputDisabled() {
      // 优先级:组件本身的 disabled > Form 的 disabled
      return this.disabled || (this.elForm || {}).disabled;
    },
    // 监听 value 值的变化,如果 value 为 null 或者 undefined,则返回空,否则返回 value 转化为字符串后的值
    nativeInputValue() {
      return this.value === null || this.value === undefined ? '' : String(this.value);
    },
    // 显示可清空图标
    showClear() {
      // 在可清空属性设置为 true ,且 input 有值且不为禁用或只读状态,且鼠标移入输入框内部或输入框聚焦,则展示可清空图标
      return this.clearable &&
        !this.inputDisabled &&
        !this.readonly &&
        this.nativeInputValue &&
        (this.focused || this.hovering);
    },
    // 判断密码图表在哪种情况下显示
    showPwdVisible() {
      // 当显示密码图标属性被设置为 true 时,且输入框不属于禁用、只读状态,且输入框有值或者输入框在聚焦的情况下,显示密码图标
      return this.showPassword &&
        !this.inputDisabled &&
        !this.readonly &&
        (!!this.nativeInputValue || this.focused);
    },
    // 判断字数统计属性在何种情况下显示
    isWordLimitVisible() {
      // 当showWordLimit属性为true,且有最大字数限制,且类型为 text 或 textarea,且不为禁用、只读、密码的状态时,显示字数统计
      return this.showWordLimit &&
        this.$attrs.maxlength &&
        (this.type === 'text' || this.type === 'textarea') &&
        !this.inputDisabled &&
        !this.readonly &&
        !this.showPassword;
    },
    // 设置计算属性 upperLimit 来获取 maxlength 的值
    upperLimit() {
      return this.$attrs.maxlength;
    },
    // 获取 value 值的长度
    textLength() {
      if (typeof this.value === 'number') {
        return String(this.value).length;
      }

      return (this.value || '').length;
    },
    // 判断字数是否超出限制
    inputExceed() {
      // 如果显示字数统计且字数超限,则说明字数超出限制
      return this.isWordLimitVisible &&
        (this.textLength > this.upperLimit);
    }
  },
  
  watch: {
    value(val) {
      this.$nextTick(this.resizeTextarea);
      // 如果输入时触发表单的校验,则在 value 值的改变时即向上派发 el.form.change 事件
      if (this.validateEvent) {
        this.dispatch('ElFormItem', 'el.form.change', [val]);
      }
    },
    // 监听 value 值的变化,给 input 赋值
    nativeInputValue() {
      this.setNativeInputValue();
    },
    type() {
      this.$nextTick(() => {
        this.setNativeInputValue();
        this.resizeTextarea();
      });
    }
  },
  
  methods: {
    focus() {
      this.getInput().focus();
    },
    blur() {
      this.getInput().blur();
    },
    select() {
      this.getInput().select();
    },
    resizeTextarea() {
      // 解构出来 autosize 和 type 属性
      const { autosize, type } = this;
      // 如果不是 textarea 则不继续向下执行
      if (type !== 'textarea') return;
      // 如果没有 autosize 则直接计算出最小高度
      if (!autosize) {
        this.textareaCalcStyle = {
          minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
        };
        return;
      }
      // 如果有 autosize 需要自适应高度,则根据传入的 minRows 和 maxRows 计算出最小高度和自适应的高度
      const minRows = autosize.minRows;
      const maxRows = autosize.maxRows;
      // 将计算后的结果赋值给 textareaCalcStyle 变量
      this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
    },
    // 获取 input
    getInput() {
      return this.$refs.input || this.$refs.textarea;
    },
    // 设置 input 元素的值
    setNativeInputValue() {
      // 获取到 input 元素
      const input = this.getInput();
      if (!input) return;
      // 如果 input 之前的 value 值与改变后的值相同,则 return
      if (input.value === this.nativeInputValue) return;
      // 将 input 的值赋值为最新的值
      input.value = this.nativeInputValue;
    },
    handleBlur(event) {
      this.focused = false;
      this.$emit('blur', event);
      // 如果输入时触发表单的校验,则在输入框 blur 时即向上派发 el.form.blur 事件
      if (this.validateEvent) {
        this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
      }
    },
    handleFocus(event) {
      this.focused = true;
      this.$emit('focus', event);
    },
    // 输入法编辑器开始新的输入合成时(开始输入拼音时)触发
    handleCompositionStart(event) {
      // 触发父级的 compositionstart 事件
      this.$emit('compositionstart', event);
      // 将 isComposing 设置为 true,说明处于拼音输入的状态
      this.isComposing = true;
    },
    // 组合输入更新时(每输入一下拼音时)都会触发
    handleCompositionUpdate(event) {
      // 触发父级的 compositionupdate 事件
      this.$emit('compositionupdate', event);
      // 将 isComposing 设置为 true,说明处于拼音输入的状态
      this.isComposing = true;
    },
    // 组合输入结束时(拼音输入结束,关闭中文输入法时)触发
    handleCompositionEnd(event) {
      // 触发父级的 compositionend 事件
      this.$emit('compositionend', event);
      if (this.isComposing) {
        // 将 isComposing 设置为 false,说明此时已经不是拼音输入的状态
        this.isComposing = false;
        // 触发 handleInput 方法,给 input 赋值
        this.handleInput(event);
      }
    },
    // 触发 input 事件
    handleInput(event) {
      // 如果正在输入拼音,直接 return
      if (this.isComposing) return;
      // 解决 IE 浏览器中 Input 初始化自动执行的问题
      if (event.target.value === this.nativeInputValue) return;
      // 触发父级的 input 事件
      this.$emit('input', event.target.value);
      // 给 input 重新赋值
      this.$nextTick(this.setNativeInputValue);
    },
    handleChange(event) {
      this.$emit('change', event.target.value);
    },
    calcIconOffset(place) {
      // 获取到 class 为 el-input__prefix 或 el-input__suffix
      let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
      if (!elList.length) return;
      // 设置变量 el
      let el = null;
      for (let i = 0; i < elList.length; i++) {
        // 如果获取到的 el-input__prefix 或 el-input__suffix 的父级元素就是该组件的根元素
        if (elList[i].parentNode === this.$el) {
          // 则将 el-input__prefix 或 el-input__suffix 赋值给 el 变量
          el = elList[i];
          break;
        }
      }
      if (!el) return;
      const pendantMap = {
        suffix: 'append',
        prefix: 'prepend'
      };
      
      const pendant = pendantMap[place];
      if (this.$slots[pendant]) {
        // 如果存在前置元素或后置元素,则获取前置元素或后置元素的 offsetWidth,将 el 向右或向左移动对应的宽度
        el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
      } else {
        // 如果不存在则 el 移除 style 属性
        el.removeAttribute('style');
      }
    },
    updateIconOffset() {
      this.calcIconOffset('prefix');
      this.calcIconOffset('suffix');
    },
    // 清空操作
    clear() {
      // 触发父级的 input 事件,并且将值传为空,由于父级用 v-model 语法糖绑定值,则可清空组件的值
      this.$emit('input', '');
      // 触发父级的 change 事件
      this.$emit('change', '');
      // 触发父级的 clear 事件
      this.$emit('clear');
    },
    handlePasswordVisible() {
      this.passwordVisible = !this.passwordVisible;
      this.$nextTick(() => {
        this.focus();
      });
    },
    // 判断后置内容区域是否显示
    getSuffixVisible() {
      return this.$slots.suffix ||
        this.suffixIcon ||
        this.showClear|| 
        this.showPassword || 
        this.isWordLimitVisible ||
        (this.validateState && this.needStatusIcon);;
    }
  },
  
  mounted() {
    // 设置 input 元素的值
    this.setNativeInputValue();
    this.resizeTextarea();
    this.updateIconOffset();
  },
  
  updated() {
    this.$nextTick(this.updateIconOffset);
  }
}
</script>
相关推荐
神夜大侠33 分钟前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱35 分钟前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
杨荧4 小时前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
NoloveisGod5 小时前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing5 小时前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
理想不理想v6 小时前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试
EasyNTS6 小时前
无插件H5播放器EasyPlayer.js网页web无插件播放器vue和react详细介绍
前端·javascript·vue.js
guokanglun6 小时前
Vue.js动态组件使用
前端·javascript·vue.js
糊涂涂是个小盆友8 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
开心工作室_kaic12 小时前
ssm111基于MVC的舞蹈网站的设计与实现+vue(论文+源码)_kaic
前端·vue.js·mvc