Element源码分析系列5-Input(输⼊框)
简介
本来不打算写输⼊框的分析,⼼想⼀个输⼊框能有多复杂,还能怎么封装,后来浏览了下源码,发现还是有很多⾃⼰不知道的知识点,于是打算还是写,下图就是⼀个Element的最基本的输⼊框
结果⼀看源码,我的⿁⿁,源码竟然300多⾏!咋会这么复杂,看过官⽹的⽂档后,发现确实应该这么复杂,因为这个输⼊框不仅仅是只有⼀个input这么简单,还附带了很多的其他内容,上图仅是⼀个最基本的形式⽽已,下⾯我们依次分析,官⽹源码
本来打算贴出全部源码,但是发现这样篇幅太长,因此我们只分析重点,分析部分源码
输⼊框源码html结构
⾸先还是先要搞懂Element封装后的input的html结构才⾏,下⾯是简化后的html结构
<template>
<div ...>
input标签placeholder属性
<template v-if="type !== 'textarea'">
<!-- 前置元素 -->
<div class="el-input-group__prepend" v-if="$slots.prepend">
<slot name="prepend"></slot>
</div>
<!--主体input-->
<input ...>
<!-- 前置内容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
...
</span>
<!-- 后置内容 -->
<span
...
</span>
<!-- 后置元素 -->
<div class="el-input-group__append" v-if="$slots.append">
...
</div>
</template>
<textarea v-else>
</textarea>
</div>
</template>
复制代码
是不是看着很头⼤?其实很简单,最外层⼀个div作为wrapper包裹⾥⾯的元素,然后⾥⾯是template标签(template实际不会渲染出来)的v-if,最下⾯是textarea的v-else,说明type这个选项控制输⼊框组件是显⽰input还是textarea,对于v-else就⼀个textarea,没啥可说的,关键在于前⾯的v-if,仔细看这个结构,是由前置元素,主体input,前置内容,后置内容,后置元素这⼏部分构成,那么它们分别代表啥呢?下图就是答案
图中中间的是input输⼊框,前后2个都是辅助性的内容,这2个就是前后置元素,⽽输⼊框内的搜索和⽇期Icon就是前后置内容,因此要封装这么个完整的input,代码量确实⽐较多
这⾥值得注意的是前后置元素和input主体的布局,修改前后置元素内容可以发现,中间input的宽度是⾃适应的,如下图
中间input⾃动变窄,那么这哥布局是咋回事呢,这哥布局类似于 左列宽度不定,右列⾃适应,左列不定的意思是宽度由内容撑开来,查看css代码得知,这是 table-cell布局,我们知道table内表格宽度都
是⾃适应的,某⼀列很宽的话,另外的列就会变窄,因此这个思想可以⽤到这⾥来,下⾯就是⽰例布局(左列宽度不定,右列⾃适应),注意外层容器设置 display:table
<div class='wrapper'>
<div class='left'>
</div>
<div class='right'>
</div>
</div>
复制代码
这个布局⽤flex也可以实现,具体就是left元素不设置宽度,right元素设置flex:1即可,下⾯看下输⼊框的css
输⼊框其实是有左右padding的,为了更美观,这⾥不是⽤text-indent来控制光标位置
可以看出 -webkit-appearance:none,outline:none这些⽤法在和各个组件内都很普遍,⽬的就是去掉浏览器⾃⼰渲染出的样式,统⼀规定样式。这⾥的 transition居然使⽤了贝塞尔曲线进⾏过渡,话说过渡时间才0.2秒,使⽤贝塞尔曲线能看出来么?直接 ease应该也可以啊!
禁⽤状态的实现
禁⽤很简单,通过⽤户传⼊的disabled属性来控制,如下代码
<el-input
placeholder="请输⼊内容"
v-model="input1"
:disabled="true">
</el-input>
复制代码
源码⾥通过<input :disabled="inputDisabled" ...>来控制input的功能禁⽤,这个inputDisabled是个计算
属性
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
复制代码
这⾥因为要判断如果input被包含在表单内,如果表单禁⽤,那么⾃然⾃⼰也就被禁⽤了。输⼊框样式上的禁⽤是由最外层的div的class控制的
<div :class=[{'is-disabled': inputDisabled}...]>...</div>
复制代码
这⾥没有放在⾥⾯的input上进⾏控制,原因是放在最外层可以统⼀控制⾥⾯的textarea和input,减少代码冗余,通过⼦选择器选择到input和textarea进⾏控制,这⾥placeholder的颜⾊也是可以控制的,但要注意兼容性
&::placeholder {
color: $--input-disabled-placeholder-color;
}
复制代码
input元素的属性
通过查看组件⾥原⽣input的属性,了解了很多知识点
<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:
type="type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete"
:value="currentValue"
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
复制代码
哇,居然这么多属性和⽅法~~这就是⼀个成熟组件需要实现的东西,先看tabindex,就是控制tab键按下后的访问顺序,由⽤户传⼊tabindex如果设置为负数则⽆法通过tab键访问,设置为0则是在最后访问。然后v-if="type !== 'textarea'"控制了这个input的渲染与否,⽤户传⼊type属性进⾏控制,然后是input的类el-input__inner,前⾯介绍过,然后是v-bind="$attrs"这句话,这句话是⼲嘛的?翻开官⽹得知
读起来很拗⼝,下⾯⽤个例⼦说明
<el-input maxlength="5" minlength="2">
</el-input>
复制代码
这⾥我们给<el-input>组件添加了2个原⽣属性,注意这2个原⽣属性并没有在prop⾥⾯,这2个属性是控制input的最⼤输⼊和最⼩输⼊长度的,那么这2个属性现在仅仅放在了⽗元素<el-input>上,如何将其传递给素<el-input>内的原⽣input⼦元素呢?不传递则这2个属性不起作⽤,因为⼦input上没有这2个属性。答案就是通过v-bind="$attrs"来实现,它将⽗元素所有⾮prop的特性都绑定在了⼦元素input上,否则你还得在props⾥声明maxlength,minlength,代码量增⼤。这就是$attrs的优势所在
往下看:readonly="readonly" :autocomplete="autoComplete",这2个属性都是原⽣的属性,由⽤户传⼊,控制输⼊框只读和是否⾃动补全,然后是输⼊框的value:value="currentValue"这⾥的currentValue是在data⾥⾯
currentValue: this.value === undefined || this.value === null
''
: this.value,
复制代码
如果⽤户没有在<el-input>上写v-model(v-model原理参考官⽹),那么就没有传⼊value,所以currentValue就是空字符串,否则就是传⼊的值,接着ref="input"⼀句,ref⽤来给元素或⼦组件注册引⽤信息。引⽤信息将会注册在⽗组件的 $refs 对象上,这是为了⽅便后续代码直接拿到原⽣input的dom
然后是这3句话
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
复制代码
这可不能⼩瞧,这3个⽅法是原⽣的⽅法,这⾥简单介绍下,官⽅定义如下compositionstart 事件触发于⼀段⽂字的输⼊之前(类似于keydown 事件,但是该事件仅在若⼲可见字符的输⼊之前,⽽这些可见字符的输⼊可能需要⼀连串的键盘操作、语⾳识别或者点击输⼊法的备选词) 简单来说就是切换中⽂输⼊法时在打拼⾳时(此时input内还没有填⼊真正的内容),会⾸先触发compositionstart,然后每打⼀个拼⾳字母,触发compositionupdate,最后将输⼊好的中⽂填⼊input中时触发compositionend。触发compositionstart时,⽂本框会填⼊ “虚拟⽂本”(待确认⽂本),同时触发input事件;在触发compo
sitionend时,就是填⼊实际内容后(已确认⽂本),所以这⾥如果不想触发input事件的话就得设置⼀个bool变量来控制
上图中点击空格后才会填⼊实际的⽂本,输⼊英⽂或数字则没有这3个事件的触发
那么问题来了,为啥Element要设置这3个事件的处理函数呢?原因很简单,我们肯定不希望在输⼊拼⾳的过程中就直接触发input事件改变<el-input v-model="inputValue"></el-input>中inputValue的值,⽽是希望输⼊完成后再改变,所以需要特殊处理,我们来看handleComposition的源码,注意这⾥只写了⼀个⽅法⽽不是3个,通过pe来判断事件类型从⽽简化代码,可以借鉴
handleComposition(event) {
if (pe === 'compositionend') {
this.isOnComposition = false;
this.currentValue = this.valueBeforeComposition;
this.valueBeforeComposition = null;
this.handleInput(event);
} else {
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
this.isOnComposition = !isKorean(lastCharacter);
if (this.isOnComposition && pe === 'compositionstart') {
this.valueBeforeComposition = text;
}
}
},
复制代码
这⾥⾸先在data中定义了⼀个bool变量isOnComposition,这个变量就是⽤来判断是否在打拼⾳的过程
中,初始为false,当开始打拼⾳后,触发compositionstart事件,更新isOnComposition,通过this.isOnComposition = !isKorean(lastCharacter)来更新,这⾥的逻辑是判断输⼊的字符的最后⼀个是不是韩⽂,韩⽂通过正则表达式来判断,⾄于为啥要判断韩⽂的最后⼀个字符,不清楚~ 如果是中⽂,
则isOnComposition为true,这⾥⽐较难理解的是后⾯这个if,当正在打拼⾳的过程中且是compositionstart事件时,则⽤⼀
个valueBeforeComposition变量保存当前的⽂本,也就是保存此次打字前input中的⽂本内容,这个valueBeforeComposition的作⽤后⾯介绍,接下来看if (pe === 'compositionend')中的内容,当打完拼⾳后,触发compositionend,此时设置isOnComposition为false表明打字完成,然后注意这⾥会⼿动触发⼀个this.handleInput(event)(handleInput就是input上绑定的v-on:input),这是因为最后输⼊完成
时,compositionend会在input事件后触发,此时isOnComposition还是true,⽆法触发下⾯handleInput中的emit将新的input的value传递给⽗组件,所以这⾥需要⼿动调⽤⼀次handleInput,这⾥请仔细理解!
handleInput(event) {
const value = event.target.value;
this.setCurrentValue(value);
if (this.isOnComposition) return;
this.$emit('input', value);
},
复制代码
handleInput中当isOnComposition为true时表明正在打拼⾳输⼊,则不触发emit事件,这是合理且正常的
可清空的实现
<el-input>中如果添加了clearable属性则输⼊⽂字后会出现⼀个叉的图标,点击后input内容清空,如下图
先看html结构,下⾯是后置内容的html代码
<!-- 后置内容 -->
<span
class="el-input__suffix"
v-if="$slots.suffix || suffixIcon || showClear || validateState && needStatusIcon">
<span class="el-input__suffix-inner">
<template v-if="!showClear">
<slot name="suffix"></slot>
<i class="el-input__icon"
v-if="suffixIcon"
:class="suffixIcon">
</i>
</template>
<i v-else
class="el-input__icon el-icon-circle-close el-input__clear"
@click="clear"
></i>
</span>
<i class="el-input__icon"
v-if="validateState"
:class="['el-input__validateIcon', validateIcon]">
</i>
</span>
复制代码
中间这段<i>就是清空按钮,它是⼀个i标签,有⼀个click事件,前⾯通过showClear来判断是否需要显⽰清空按钮,逻辑如下
showClear() {
return this.clearable &&
!this.disabled &&
!adonly &&
this.currentValue !== '' &&
(this.focused || this.hovering);
}
复制代码
这个计算属性第⼀步得看⽤户是否添加了显⽰清空按钮的属性,如果没有则不显⽰,如果有则继续判断,在⾮禁⽤且⾮只读状态下才且当前input的value不是空且该input获得焦点或者⿏标移动上去才显⽰,条件略多啊
然后看clear清空这个⽅法
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
this.setCurrentValue('');
this.focus();
}
复制代码
居然有5句话,但都不能少,第⼀个emit是通知⽗组件⾃⼰的value值变成了空,从⽽更新<el-input v-model="v">中的v这个data为空,第⼆句emit触发了⽗组件的change事件,这样在<el-input v-model="v" @change="inputChange">中的inputChange中就能监听到该事件了,第3个emit触发⽗组件的@clear⽅法,让⽗组件知道⾃⼰已经清空了,第四句话更新⾃⼰的currentValue为空,第五局让input获得焦点便于输⼊内容
textarea⾼度⾃适应的实现

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。