原本不打算写输入框的分析,心想一个输入框能有多复杂,还能怎么封装,后来浏览了下源码,发现仍是有不少本身不知道的知识点,因而打算仍是写,下图就是一个Element的最基本的输入框css
首先仍是先要搞懂Element封装后的input的html结构才行,下面是简化后的html结构html
<template>
<div ...>
<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,前置内容,后置内容,后置元素这几部分构成,那么它们分别表明啥呢?下图就是答案vue
这里值得注意的是先后置元素和input主体的布局,修改先后置元素内容能够发现,中间input的宽度是自适应的,以下图git
table-cell
布局,咱们知道table内表格宽度都是自适应的,某一列很宽的话,另外的列就会变窄,所以这个思想能够用到这里来,下面就是示例布局(左列宽度不定,右列自适应),注意外层容器设置
display:table
<div style="display:table" class='wrapper'>
<div style="display:table-cell" class='left'>
</div>
<div style="display:table-cell" class='right'>
</div>
</div>
复制代码
这个布局用flex也能够实现,具体就是left元素不设置宽度,right元素设置flex:1便可,下面看下输入框的cssgithub
-webkit-appearance:none,outline:none
这些用法在和各个组件内都很广泛,目的就是去掉浏览器本身渲染出的样式,统一规定样式。这里的
transition
竟然使用了贝塞尔曲线进行过渡,话说过渡时间才0.2秒,使用贝塞尔曲线能看出来么?直接
ease
应该也能够啊!
禁用很简单,经过用户传入的disabled
属性来控制,以下代码web
<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
的颜色也是能够控制的,但要注意兼容性bash
&::placeholder {
color: $--input-disabled-placeholder-color;
}
复制代码
经过查看组件里原生input的属性,了解了不少知识点app
<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事件;在触发compositionend时,就是填入实际内容后(已确认文本),因此这里若是不想触发input事件的话就得设置一个bool变量来控制
<el-input v-model="inputValue"></el-input>
中inputValue的值,而是但愿输入完成后再改变,因此须要特殊处理,咱们来看
handleComposition
的源码,注意这里只写了一个方法而不是3个,经过event.type来判断事件类型从而简化代码,能够借鉴
handleComposition(event) {
if (event.type === '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 && event.type === 'compositionstart') {
this.valueBeforeComposition = text;
}
}
},
复制代码
这里首先在data中定义了一个bool变量isOnComposition
,这个变量就是用来判断是否在打拼音的过程当中,初始为false,当开始打拼音后,触发compositionstart
事件,更新isOnComposition
,经过this.isOnComposition = !isKorean(lastCharacter)
来更新,这里的逻辑是判断输入的字符的最后一个是否是韩文,韩文经过正则表达式来判断,至于为啥要判断韩文的最后一个字符,不清楚~ 若是是中文,则isOnComposition
为true,这里比较难理解的是后面这个if,当正在打拼音的过程当中且是compositionstart
事件时,则用一个valueBeforeComposition
变量保存当前的文本,也就是保存这次打字前input中的文本内容,这个valueBeforeComposition
的做用后面介绍,接下来看if (event.type === '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内容清空,以下图
<!-- 后置内容 -->
<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 &&
!this.readonly &&
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随着内容增多则会出现滚动条
function calcTextareaHeight(){
...
let height = hiddenTextarea.scrollHeight;
const result = {};
...
result.height = `${ height }px`;
return result
}
复制代码
这里让height等于scrollHeight,也就是滚动条卷去的高度,这里就将height变大了,而后返回该height并绑定到input的style中从而动态改变textarea的height,具体代码很复杂,还要处理最大最小高度等,参考github