一. 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
文件。设置组件的 name
和 componentName
都为 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
实现步骤:
- 在引入
input
组件时绑定v-model
。
html
<template>
<div>
<el-input v-model="val"></el-input>
</div>
</template>
<script>
export default {
data() {
return {
val: '输入框值'
}
}
}
</script>
- 组件内部接收
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>
- 处理拼音输入时
input
值的变化问题。
前两步绑定 v-model
值还存在一个小的问题,如果是输入的拼音,引入组件绑定 input
方法拿到的 value
是包含在输入法输入的拼音的那部分的,所以在还未输入到输入框时,不能设置 input
的 value
值,也不能触发父级的 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 属性应用在子组件的根元素上。通过设置 inheritAttrs
为 false
,将不会被默认绑定到子组件的根元素上。通过属性 $attrs
可以获取到这些属性显性的绑定到非根元素上。
inheritAttrs
设置为 false
不会影响到 class
和 style
的绑定。
实现步骤:
通过设置 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
属性,然后设置计算属性去监听 input
的 disabled
的变化和 form
表单的 disabled
变化,优先获取 input
的 disabled
状态。
(3)readonly
:readonly
属性需要和"显示清空"、"显示密码框"、"计数功能"相互作用,所以需要单独用 props
接收 readonly
属性。
3.2.2.1 原生属性 ------ type 的处理
实现步骤:
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>
- 监听
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 可清空功能的实现步骤
- 设置两个变量
hovering、focused
分别监听mouseenter/mouseleave
,focus/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>
- 接收可清空属性,并且设置计算属性 判断显示清空图标的条件。
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>
- 将清空图标放在
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>
- 点击清空按钮执行清空功能。
给清空按钮绑定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 密码图标的作用及显示情况
- 作用:将用户输入的内容显示成小圆点的密文形式。
- 密码图标的显示情况:当输入框设置了
show-password
属性,并且输入框有值或者输入框在聚焦的情况下,显示密码图标。
3.2.4.2 密码图标功能的实现步骤
- 显示密码图标。
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--suffix
的 class
加上 showPassword
的判断:
html
<div :class="[
...
{
...
'el-input--suffix': clearable || showPassword
}
]"
>
</div>
- 点击密码图标进行明文和密文的切换。
设置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
的判断,在 showPassword
为 true
的情况下,判断是否已经展示了密码,如果是明文展示的,则 type
为 text
,否则为 password
。
html
<input
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
/>
3.2.5 字数统计属性
3.2.5.1 字数统计属性实现思路
- 接收传入的 show-word-limit 属性。
- 判断字数统计属性在何种情况下显示:
(1)show-word-limit 为 true
(2)传入了 max-length
(3)是 text 类型或者 textarea 类型
(4)不是禁用或只读状态
(5)show-password 不为 true,也就是不是密码框 - 设置计算属性 upperLimit 来获取 maxlength 的值。
- 设置计算属性 textLength 来实时获取 value 值的长度。
- 设置计算属性 inputExceed 判断是否超出字数。
- 页面显示字数统计样式,超出最大字数的样式。
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
组件,在计算属性里面还需要监听form
的size
)。- 在最外层根据不同的
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
接收suffixIcon
和prefixIcon
属性。- 增加
suffix
和prefix
的class
样式。 - 在
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-label
和 tabIndex
属性。
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)prefix
和 suffix
位置描述
prefix
和 suffix
是输入框头部和尾部的内容,展示在输入框的内部,和传入的头部图标尾部图标展示的位置一致。
(2)代码实现
- 分别在
class
为el-input__prefix
和el-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)prepend
和 append
位置描述
prepend
和 append
是输入框前置和输入框后置的内容,展示在输入框的外部。
(2)将 prepend
和 append
元素放置在组件中
- 在组件的
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)重新计算 prefix
和 suffix
的位置
由于 prefix
和 suffix
的位置是参照输入框绝对定位的元素,所以在增加了 prepend
和 append
元素后,如果输入框还存在 prefix
和 suffix
元素,需要将其的横向位置分别向右或者向左进行移动,避免图标位置与 prefix
和 suffix
的位置重叠。
计算图标横向位移的方法:
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');
}
},
在 mounted
、updated
钩子函数中调用 updateIconOffset
,在载入后和更新后都需要重新计算图标的位置。
js
mounted() {
...
this.updateIconOffset();
},
updated() {
this.$nextTick(this.updateIconOffset);
}
3.4 补齐事件和方法
3.4.1 补齐事件
目前已经实现的事件有 blur
,focus
,input
和 clear
,未实现的有 change
。
在 input
元素上面增加 change
事件,执行 handleChange
方法,方法内部采用 emit
触发父组件的 change
事件。
js
// handleChange 方法
handleChange(event) {
this.$emit('change', event.target.value);
},
3.4.2 补齐方法
目前对外提供的可以调用到的方法有 focus
,还需要提供 blur
和 select
。
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>