简介
输入框组件 Input
通过鼠标或键盘输入表单域内容,提供复合型型输入框,带搜索的输入框,还可以进行大小选择。 本文将分析其源码实现,耐心读完,相信会对您有所帮助。🔗 组件文档 Input 🔗 gitee源码
更多组件剖析详见 👉 📚 Element 2 源码剖析组件总览 。
模板HTML
组件的 props
声明,各属性功能描述详见官方文档Input#attributes 。
组件根元素是一个类名为el-textarea
或el-input
的 div 元素,元素内容包含了三部分:
封装原生 textarea
控件多行文本输入文本域。
复制 // packages\input\src\input.vue
< template >
< div :class = "[type === 'textarea' ? 'el-textarea' : 'el-input',]" >
<!-- 单行文本输入框 -->
< template v-if = "type !== 'textarea'" >
<!-- 输入框前置内容 -->
< div class = "el-input-group__prepend" ></ div >
<!-- 表单输入控件 -->
< input >
<!-- 输入框头部内容 -->
< span class = "el-input__prefix" ></ span >
<!-- 输入框尾部内容 -->
< span class = "el-input__suffix" ></ span >
<!-- 输入框后置内容 -->
< div class = "el-input-group__append" ></ div >
</ template >
<!-- 多行文本输入的文本域 -->
< textarea ></ textarea >
</ div >
</ template >
组件根元素
属性type
值确定使用 el-textarea
或 el-input
。
组件尺寸inputSize
生成样式 el-input--medium/small/mini
。
禁用状态inputDisabled
生成样式 is-disabled
。
开启输入长度限制,输入超限时inputExceed
生成样式 is-disabled
。
复合型输入框样式el-input-group
、el-input-group--append/prepend
根据前置/后置插槽内容生成。
头部/尾部样式el-input--prefix
、el-input--suffix
根据插槽内容或功能属性值生成。
添加了鼠标移入移出事件,使用内部属性 hover
记录元素悬停状态。
复制 < 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--prefix': $slots.prefix || prefixIcon,
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
}
]"
@mouseenter = "hovering = true"
@mouseleave = "hovering = false"
>
// ...
</ div >
单行文本输入
单行文本输入框通过封装原生 input
控件实现,支持控件的原生属性。组件通过组合以下多种元素实现文本框 text
或密码框password
等复合型输入框功能:
输入框前置内容,提供具名插槽prepend
,内容一般为标签或按钮。
原生 input 表单输入控件,添加了自定义事件。
输入框头部内容,可以通过 prefix-icon
或具名插槽prefix
增加显示图标。
输入框尾部内容,可以通过 suffix-icon
或具名插槽suffix
增加显示图标。该元素也用于展示输入框清空Icon、密码显隐切换Icon以及展示输入字数统计。
输入框后置内容,提供具名插槽append
,内容一般为标签或按钮。
复制 < template v-if = "type !== 'textarea'" >
<!-- 输入框前置内容 -->
< div class = "el-input-group__prepend" v-if = "$slots.prepend" >
< slot name = "prepend" ></ slot >
</ div >
<!-- 表单输入控件 -->
< input
:tabindex = "tabindex" // 元素是否可以聚焦 键盘导航
v-if = "type !== 'textarea'"
class = "el-input__inner"
v-bind = "$attrs" // 透传 Attributes 例如 placeholder
:type = "showPassword ? (passwordVisible ? 'text': 'password') : type" // text /password 也支持其他原生input的type值
:disabled = "inputDisabled" // 是否禁用
:readonly = "readonly" // 是否只读
:autocomplete = "autoComplete || autocomplete" // 自动补全
ref = "input"
@compositionstart = "handleCompositionStart" // 输入法编辑器 (IME) 事件
@compositionupdate = "handleCompositionUpdate"
@compositionend = "handleCompositionEnd"
@input = "handleInput" // 输入内容
@focus = "handleFocus" // 获取焦点
@blur = "handleBlur" // 失去焦点
@change = "handleChange" // 输入值变化
:aria-label = "label" // ARIA 无障碍属性
>
<!-- 输入框头部内容 -->
< 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 = "!showClear || !showPwdVisible || !isWordLimitVisible" >
< slot name = "suffix" ></ slot >
< i class = "el-input__icon" v-if = "suffixIcon" :class = "suffixIcon" ></ i >
</ template >
// ...
</ span >
// ...
</ span >
<!-- 输入框后置内容 -->
< div class = "el-input-group__append" v-if = "$slots.append" >
< slot name = "append" ></ slot >
</ div >
</ template >
组件渲染效果如下:
输入框头部/尾部都提供了具名插槽用于增加显示图标,当然也可以传入文本等其他内容,但不建议这么做。
当设置头部/尾部内容时, input 输入框通过属性 padding
提供了 30px
的宽度区域用于内容展示。头部/尾部元素使用绝对布局,将内容偏移覆盖至 padding 区域。
复制 .el-input__prefix {
position : absolute ;
height : 100 % ;
left : 5 px ;
top : 0 ;
}
.el-input__suffix {
position : absolute ;
height : 100 % ;
right : 5 px ;
top : 0 ;
}
.el-input--prefix .el-input__inner {
padding-left : 30 px ;
}
.el-input--suffix .el-input__inner {
padding-right : 30 px ;
}
以下示例将自定义文本传入插槽。
复制 < el-input placeholder = "请输入内容" v-model = "input" >
< template slot = "prefix" >
< span style = "display: flex; align-items: center; height: 100%" >头部内容</ span >
</ template >
< template slot = "suffix" >
< span style = "display: flex; align-items: center; height: 100%" >尾部内容</ span >
</ template >
</ el-input >
input 元素类型
组件默认使用 text
类型的input控件。当设置 showPassword
可得到一个可切换显示隐藏的密码框,内部属性passwordVisible
记录显隐状态,根据不同的状态使用 password
或 text
类型。
复制 :type = "showPassword ? (passwordVisible ? 'text': 'password') : type"
属性 type
值也可设置为其他原生input的type值 。
复制 < el-input v-model = "input" placeholder = "请输入内容" type = "color" ></ el-input >
< el-input v-model = "input" placeholder = "请输入内容" type = "date" ></ el-input >
< el-input v-model = "input" placeholder = "请输入内容" type = "datetime-local" ></ el-input >
< el-input v-model = "input" placeholder = "请输入内容" type = "file" ></ el-input >
< el-input v-model = "input" placeholder = "请输入内容" type = "month" ></ el-input >
< el-input v-model = "input" placeholder = "请输入内容" type = "time" ></ el-input >
< el-input v-model = "input" placeholder = "请输入内容" type = "week" ></ el-input >
使用其他原生类型时组件渲染效果,但是一般不建议这么使用!一般组件类库都会提供对应的更加丰富的功能组件,使用库组件页面样式风格更加统一,提升用户交互体验。
输入法编辑器(IME)事件
输入法在中文、日文和韩文等少数语言中使用。以中文拼音输入法为例,输入的过程大致可以分为组字(composition) 和 提交(commit) 两阶段。比如想打“你好”两个字,会在输入框输入“nihao”的拼音,当输入第一个字母“n”时,组字过程就开始了。此时本地的 IME(input method editor) 软件(比如微软/搜狗拼音输入法)会为我们提供组字框 和候选列表 的 UI 组件。
关于输入法事件更多介绍,请阅读 Web 键盘输入法应用开发指南 —— 输入法事件 。
compositionstart
、compositionupdate
和compositionend
是一组事件。
首先是使用拼音输入法开始输入汉字时 compositionstart
被触发,组字框和候选列表相应出现;
此后,每按一个新键,就会触发 compositionupdate
,此时组字框和候选列表的内容也发生变化;
当选择了候选列表中的某个字或词,或者敲击空格(中文输入法),compositionend
事件会触发,表明输入被提交。
当取消输入时,比如使用鼠标单击页面空白处,就会终止当前输入,也会触发compositionend
事件。
属性isComposing
值为 true
表示用户正在输入。输入结束后(选择字词或者取消),调用方法handleInput
,触发组件 input
事件。
复制 // @compositionstart="handleCompositionStart"
// @compositionupdate="handleCompositionUpdate"
// @compositionend="handleCompositionEnd"
handleCompositionStart () {
this .isComposing = true ; // 正在输入
} ,
handleCompositionUpdate (event) {
const text = event . target .value;
const lastCharacter = text[ text . length - 1 ] || '' ;
// 韩文字符编码 判断最后一个字符是否特殊的功能键 Process Key
this .isComposing = ! isKorean (lastCharacter);
} ,
handleCompositionEnd (event) {
// 输入结束后,触发 input 事件
if ( this .isComposing) {
this .isComposing = false ;
this .handleInput (event);
}
} ,
input/change 事件
当 <input>
、<select>
、<textarea>
元素的 value
被修改时,会触发 input
事件。
当用户更改<input>
、<select>
、<textarea>
元素的值并提交这个更改时,change
事件在这些元素上触发。基于表单元素的类型和用户对标签的操作的不同,change
事件触发的时机也不同。
对于文本输入元素,比如 <input type="text">
,每当元素的 value
改变,input
事件都会被触,change
事件在控件失去焦点后才会触发。
复制 // @input="handleInput"
// @change="handleChange"
handleInput (event) {
// 输入法下用户正在输入
if ( this .isComposing) return ;
// IE 11下 DatePicker组件 hack 写法,详见issues
// https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if ( event . target .value === this .nativeInputValue) return ;
// 触发触发当前实例上的input事件
this .$emit ( 'input' , event . target .value);
// Input 为受控组件 更新组件的绑定值
this .$nextTick ( this .setNativeInputValue);
} ,
handleChange (event) {
// 触发触发当前实例上的change事件
this .$emit ( 'change' , event . target .value);
} ,
对于需要使用输入法的语言, v-model
不会在输入法组合文字过程中得到更新,因为compositionend
事件没触发是不会执行handleInput
逻辑。
复制 if ( this .isComposing) return ;
focus/blur 事件
元素获取或失去焦点时,调用事件监听方法,更新内部属性focused
记录元素焦点状态,同时触发实例自定义 focus
或 blur
事件。
复制 // @focus="handleFocus"
// @blur="handleBlur"
handleFocus (event) {
this .focused = true ;
// 触发触发当前实例上的focus事件
this .$emit ( 'focus' , event);
} ,
handleBlur (event) {
this .focused = false ;
// 触发触发当前实例上的blur事件
this .$emit ( 'blur' , event);
// 开启输入时是否触发表单的校验
if ( this .validateEvent) {
// 触发组件`FormItem`的自定义`el.form.blur`事件。
this .dispatch ( 'ElFormItem' , 'el.form.blur' , [ this .value]);
}
} ,
provide/inject 依赖注入
在 Form 组件中,每一个表单域由一个 Form-Item 组件构成,表单域中可以放置各种类型的表单控件,包括 Input、Select、Checkbox、Radio、Switch、DatePicker、TimePicker等。
表单form
和表单域form-item
使用provide
选项指定给后代组件的状态,避免了 prop 逐级透传 。
复制 // packages\form\src\form.vue
provide () {
return {
elForm : this
};
} ,
// packages\form\src\form-item.vue
provide () {
return {
elFormItem : this
};
} ,
inject : [ 'elForm' ] ,
computed : {
// ...
_formSize () {
return this . elForm .size;
} ,
elFormItemSize () {
return this .size || this ._formSize;
} ,
sizeClass () {
return this .elFormItemSize || ( this .$ELEMENT || {}).size;
}
} ,
组件 input
使用inject
选项注入上层组件提供的数据,如尺寸、校验、禁用,用于组件内部状态的控制计算。
复制 // packages\input\src\input.vue
inject : {
elForm : {
default : ''
} ,
elFormItem : {
default : ''
}
} ,
computed : {
// 表单域下组件的尺寸
_elFormItemSize () {
return ( this .elFormItem || {}).elFormItemSize;
} ,
// 表单域下组件的校验状态 校验中/成功/失败
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];
} ,
// 组件尺寸大小
inputSize () {
return this .size || this ._elFormItemSize || ( this .$ELEMENT || {}).size;
} ,
// 组件禁用状态
inputDisabled () {
return this .disabled || ( this .elForm || {}).disabled;
} ,
}
// this.$ELEMENT 来源于组件库的全局注册
const install = function (Vue , opts = {}) {
Vue . prototype .$ELEMENT = {
size : opts .size || '' ,
zIndex : opts .zIndex || 2000
};
// ...
};
生命周期
在生命周期钩子created
、mounted
、updated
中,添加监听事件,初始化组件状态。
复制 created () {
// 监听当前实例上的自定义 inputSelect 事件,用于快速选中输入控件的所有内容
this .$on ( 'inputSelect' , this .select);
} ,
mounted () {
this .setNativeInputValue (); // 设置原生输入控件的value值
this .resizeTextarea (); // 设置文本域的大小
this .updateIconOffset (); // 输入框头部/尾部(图标)元素偏移
} ,
// 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
updated () {
// `updated` 不会保证所有的子组件也都被重新渲染完毕。
// 使用 `vm.$nextTick` 确保整个视图都被重新渲染之后才会运行的代码
this .$nextTick ( this .updateIconOffset);
}
组件对属性 value
、 nativeInputValue
、 type
添加了侦听器,用于状态更新以及关联表单验证事件。
复制 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
会忽略任何表单元素上初始的 value
、checked
或 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
值,触发组件实例的change
、clear
等自定义事件。
复制 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
值,因为密码显隐是通过渲染不同类型 (text
或password
) 的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 // 非密码框
);
} ,
使用了计算属性 textLength
、 upperLimit
显示了输入进度。
计算属性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];
} ,
}
文本域 textarea
组件通过封装原生 textarea
控件实现多行文本输入文本域功能。组件做了封装统一处理,所以textarea
控件的属性/事件跟input
控件是相似的。
复制 < div :class = "[type === 'textarea' ? '' : 'el-input']" >
<!-- 单行文本输入框 -->
< template v-if = "type !== 'textarea'" >
<!-- 表单输入控件 -->
< input >
</ template >
<!-- 多行文本输入的文本域 -->
< textarea
v-else
:tabindex = "tabindex" // 元素是否可以聚焦 键盘导航
class = "el-textarea__inner"
ref = "textarea"
v-bind = "$attrs" // 透传 Attributes
:disabled = "inputDisabled" // 是否禁用
:readonly = "readonly" // 是否只读
:autocomplete = "autoComplete || autocomplete" // 自动补全
:style = "textareaStyle" // 自定义样式
@compositionstart = "handleCompositionStart" // 输入法编辑器 (IME) 事件
@compositionupdate = "handleCompositionUpdate"
@compositionend = "handleCompositionEnd"
@input = "handleInput" // 输入内容
@focus = "handleFocus" // 获取焦点
@blur = "handleBlur" // 失去焦点
@change = "handleChange" // 输入值变化
:aria-label = "label" // ARIA 无障碍属性
>
</ textarea >
<!-- 展示字数统计 -->
< span v-if = "isWordLimitVisible && type === 'textarea'" class = "el-input__count" >
{{ textLength }}/{{ upperLimit }}
</ span >
</ div >
控件缩放
控件textarea
控件通过绑定计算属性textareaStyle
设置内联样式,实现控件的高度自适应、文本区大小可调整。
计算属性textareaStyle
中使用了内部属性textareaCalcStyle
和prop属性 resize
。
属性textareaCalcStyle
值为文本域的高度属性(height
、 min-height
)样式,在方法resizeTextarea
中由文本域内容和配置项计算生成。
属性 resize
控制文本区是否可调整大小。
both
允许用户在水平和垂直方向上调整元素的大小。
horizontal
允许用户在水平方向上调整元素的大小。
vertical
允许用户在垂直方向上调整元素的大小。
复制 data () {
return {
textareaCalcStyle : {} ,
};
} ,
computed : {
textareaStyle () {
return merge ({} , this .textareaCalcStyle , { resize : this .resize });
} ,
} ,
methods : {
resizeTextarea () {
// 计算 textareaCalcStyle
} ,
} ,
可自适应文本高度
方法 resizeTextarea
用来改变文本域的高度大小的。实例挂载时会调用该方法,当实例类型、输入框的值改变时也会多次调用该方法。
通过设置 autosize
属性可以使得文本域的高度能够根据文本内容自动进行调整,并且 autosize
还可以设定为一个对象,指定最小行数和最大行数。
复制 mounted () {
this .resizeTextarea ();
} ,
watch : {
value (val) {
this .$nextTick ( this .resizeTextarea);
} ,
type () {
this .$nextTick (() => {
this .resizeTextarea ();
});
}
} ,
methods : {
resizeTextarea () {
// 若服务端渲染,方法中断返回
if ( this .$isServer) return ;
const { autosize , type } = this ;
// 此方法只用于 `textarea` 控件
if (type !== 'textarea' ) return ;
// 属性 autosize 未开启自适应内容高度 只计算控件最小高度
if ( ! autosize) {
this .textareaCalcStyle = {
minHeight : calcTextareaHeight ( this . $refs .textarea).minHeight
};
return ;
}
// autosize 也可传入对象,如 { minRows: 2, maxRows: 6 }
const minRows = autosize .minRows; // 最少行数
const maxRows = autosize .maxRows; // 最大行数
// 控件自适应高度样式
this .textareaCalcStyle = calcTextareaHeight ( this . $refs .textarea , minRows , maxRows);
} ,
} ,
calcTextareaHeight.js
类库导出方法calcTextareaHeight
,用于动态计算文本域的高度样式。
复制 // packages\input\src\calcTextareaHeight.js
let hiddenTextarea; // 一个临时的文本域元素
// 用于隐藏创建的临时的文本域元素 hiddenTextarea
const HIDDEN_STYLE = `
height:0 !important;
// ...
` ;
// 指定实例中文本域元素样式属性列表,获取属性值后用于创建临时的文本域元素
const CONTEXT_STYLE = [
'width' ,
// ...
];
// 获取指定元素节点的样式
function calculateNodeStyling (targetElement) {
// ...
}
// 计算文本域的高度
export default function calcTextareaHeight (
targetElement ,
minRows = 1 ,
maxRows = null
) {
// ...
};
calculateNodeStyling()
方法 calculateNodeStyling
用于获取实例文本域元素节点的样式,并计算box-sizing
相关属性值。
contextStyle
复制当前实例元素的样式用于创建隐藏文本域。通过 window.getComputedStyle()
获取元素计算后/渲染后的所有 CSS 属性的值,然后获取数组CONTEXT_STYLE
中指定属性值生成样式对象并转化字符串。
boxSizing
获取元素box-sizing
属性值。
复制 // 指定实例中文本域元素样式属性列表,获取属性值后用于隐藏文本域创建
const CONTEXT_STYLE = [
'line-height' ,
'padding-top' ,
'padding-bottom' ,
'font-weight' ,
'font-size' ,
'width' ,
// ...
];
// 获取指定元素节点的样式
function calculateNodeStyling (targetElement) {
// 获取元素计算后/渲染后的所有 CSS 属性的值
const style = window .getComputedStyle (targetElement);
// box-sizing 属性定义了如何计算一个元素的总宽度和总高度。
const boxSizing = style .getPropertyValue ( 'box-sizing' );
// 只是计算高度 获取上下内边距和
const paddingSize = (
parseFloat ( style .getPropertyValue ( 'padding-bottom' )) +
parseFloat ( style .getPropertyValue ( 'padding-top' ))
);
// 只是计算高度 获取上下边框宽度和
const borderSize = (
parseFloat ( style .getPropertyValue ( 'border-bottom-width' )) +
parseFloat ( style .getPropertyValue ( 'border-top-width' ))
);
// 获取元素指定的属性值并生成对象
const contextStyle = CONTEXT_STYLE
.map (name => ` ${ name } : ${ style .getPropertyValue (name) } ` )
.join ( ';' );
return { contextStyle , paddingSize , borderSize , boxSizing };
}
calcTextareaHeight()
方法 calcTextareaHeight
通过创建一个跟实例元素一样的临时文本域,用于计算出自适应高度。
创建临时文本域元素 hiddenTextarea
,获取实例元素的内容和样式值、内容赋值给临时元素,使其作为实例元素的复制镜像。通过样式 HIDDEN_STYLE
用于隐藏临时元素使其不可见。
获取元素内容高度scrollHeight
,根据不同box-sizing
属性计算出元素总高度。
计算出单行文本行高度singleRowHeight
。
如果设置minRows
,计算出元素属性min-height
值。
如果设置maxRows
,计算出元素最大高度,实际 height 不能超过最大高度。
返回计算结果,格式 { height:20px }
或{ height:20px; minHeight:20px; }
。
复制 // 用于隐藏创建的临时的文本域元素 hiddenTextarea
const HIDDEN_STYLE = `
height:0 !important;
visibility:hidden !important;
overflow:hidden !important;
position:absolute !important;
z-index:-1000 !important;
top:0 !important;
right:0 !important
` ;
// 计算文本域内容高度
export default function calcTextareaHeight (
targetElement , // 实例文本域元素
minRows = 1 , // 最小行数
maxRows = null // 最大行数
) {
// 创建一个临时文本域,下面所有的计算都是在其上模拟的
if ( ! hiddenTextarea) {
hiddenTextarea = document .createElement ( 'textarea' );
document . body .appendChild (hiddenTextarea);
}
// 获取当前实例元素样式信息
let {
paddingSize ,
borderSize ,
boxSizing ,
contextStyle
} = calculateNodeStyling (targetElement);
// contextStyle 让临时文本域元素样式跟实例元素保持一致
// HIDDEN_STYLE 用于隐藏临时文本域元素
hiddenTextarea .setAttribute ( 'style' , ` ${ contextStyle } ; ${ HIDDEN_STYLE } ` );
// 设置临时文本域内容
hiddenTextarea .value = targetElement .value || targetElement .placeholder || '' ;
// 临时文本域内容高度,包括由于溢出导致的视图中不可见内容。
let height = hiddenTextarea .scrollHeight;
const result = {};
// 计算文本内容高度 因为 scrollHeight 包括元素的 padding,但不包括元素的 border 和 margin。
if (boxSizing === 'border-box' ) {
height = height + borderSize; // border-box 加上上下边框宽度和
} else if (boxSizing === 'content-box' ) {
height = height - paddingSize; // content-box 减去上下内边距和
}
hiddenTextarea .value = '' ; // 清空内容计算单行高度,
let singleRowHeight = hiddenTextarea .scrollHeight - paddingSize; // 单行文本高度
if (minRows !== null ) {
let minHeight = singleRowHeight * minRows; // 最小行数高度和
if (boxSizing === 'border-box' ) {
minHeight = minHeight + paddingSize + borderSize; // border + padding + 内容的高度
}
height = Math .max (minHeight , height);
result .minHeight = ` ${ minHeight } px` ; // 设置样式 { minHeight:20px; }
}
if (maxRows !== null ) {
let maxHeight = singleRowHeight * maxRows; // 最大行数高度和
if (boxSizing === 'border-box' ) {
maxHeight = maxHeight + paddingSize + borderSize; // border + padding + 内容的高度
}
height = Math .min (maxHeight , height); // 选择最小高度
}
result .height = ` ${ height } px` ; // 设置样式 { height:20px; }
// 计算结束,清除临时文本域元素
hiddenTextarea .parentNode && hiddenTextarea . parentNode .removeChild (hiddenTextarea);
hiddenTextarea = null ;
// 返回 { height:20px } 或 { height:20px; minHeight:20px; }
return result;
};
属性scrollHeight
是一个元素内容高度,包括由于溢出导致的视图中不可见内容。包括元素的 padding,但不包括元素的 border 和 margin。
属性 box-sizing
定义了如何计算一个元素的总宽度和总高度。
content-box
默认值,标准盒子模型。width
与 height
只包括内容的宽和高,不包括边框(border),内边距(padding),外边距(margin)。
border-box
width
与 height
属性包括内容,内边距和边框,但不包括外边距。
width
= border + padding + 内容的宽度
height
= border + padding + 内容的高度
样式实现
组件样式源码 packages\theme-chalk\src\input.scss
使用混合指令嵌套生成组件样式。
复制 // packages\theme-chalk\src\input.scss
// 生成 .el-textarea
@include b (textarea) {
// ...
// 生成 .el-textarea__inner
@include e (inner) {
// ...
// 生成 .el-textarea__inner::placeholder
& ::placeholder { /* ... */ }
// 生成 .el-textarea__inner:hover
& :hover { /* ... */ }
// 生成 .el-textarea__inner:focus
& :focus { /* ... */ }
}
// 生成 .el-textarea .el-input__count
& .el-input__count { /* ... */ }
@include when ( disabled ) {
// 生成 .el-textarea.is-disabled .el-textarea__inner
.el-textarea__inner {
// ...
// 生成 .el-textarea.is-disabled .el-textarea__inner::placeholder
& ::placeholder { /* ... */ }
}
}
@include when (exceed) {
// 生成 .el-textarea.is-exceed .el-textarea__inner
.el-textarea__inner { /* ... */ }
// 生成 .el-textarea.is-exceed .el-input__count
.el-input__count { /* ... */ }
}
}
// 生成 .el-input
@include b (input) {
// ...
// @include scroll-bar;
// 生成 .el-input .el-input__clear
& .el-input__clear {
// ...
// 生成 .el-input .el-input__clear:hover
& :hover { /* ... */ }
}
// 生成 .el-input .el-input__count
& .el-input__count {
// ...
// 生成 .el-input .el-input__count .el-input__count-inner
.el-input__count-inner { /* ... */ }
}
// 生成 .el-input__inner
@include e (inner) {
// ...
// 生成 .el-input__inner::-ms-reveal
& ::-ms-reveal { /* ... */ }
// 生成 .el-input__inner::placeholder
& ::placeholder { /* ... */ }
// 生成 .el-input__inner:hover
& :hover { /* ... */ }
// 生成 .el-input__inner:focus
& :focus { /* ... */ }
}
// 生成 .el-input__suffix
@include e ( suffix ) { /* ... */ }
// 生成 .el-input__suffix-inner
@include e (suffix - inner) { /* ... */ }
// 生成 .el-input__prefix
@include e ( prefix ) { /* ... */ }
// 生成 .el-input__icon
@include e ( icon ) {
// ...
// 生成 .el-input__icon:after
& :after { /* ... */ }
}
// 生成 .el-input__validateIcon
@include e (validateIcon) { /* ... */ }
@include when ( active ) {
// 生成.el-input.is-active .el-input__inner
.el-input__inner { /* ... */ }
}
@include when ( disabled ) {
// 生成 .el-input.is-disabled .el-input__inner
.el-input__inner {
// ...
// 生成 .el-input.is-disabled::placeholder
& ::placeholder { /* ... */ }
}
// 生成 .el-input.is-disabled .el-input__icon
.el-input__icon { /* ... */ }
}
@include when (exceed) {
// 生成 .el-input.is-exceed .el-input__inner
.el-input__inner { /* ... */ }
.el-input__suffix {
// 生成 .el-input.is-exceed .el-input__suffix .el-input__count
.el-input__count { /* ... */ }
}
}
@include m ( suffix ) {
// 生成 .el-input--suffix .el-input__inner
.el-input__inner { /* ... */ }
}
@include m ( prefix ) {
// 生成 .el-input--prefix .el-input__inner
.el-input__inner { /* ... */ }
}
// 生成 .el-input--medium
@include m ( medium ) {
// ...
// 生成 .el-input--medium .el-input__inner
@include e (inner) { /* ... */ }
// 生成 .el-input--medium .el-input__icon
.el-input__icon { /* ... */ }
}
// small/mini ...
}
// 生成 .el-input-group
@include b (input - group) {
// ...
// 生成 .el-input-group > .el-input__inner
> .el-input__inner { /* ... */ }
// 生成 .el-input-group__append, .el-input-group__prepend
@include e ((append, prepend)) {
// ...
// 生成 .el-input-group__append:focus,.el-input-group__prepend:focus
& :focus { /* ... */ }
/* 生成
.el-input-group__append .el-button,
.el-input-group__append .el-select,
.el-input-group__prepend .el-button,
.el-input-group__prepend .el-select */
.el-select , .el-button { /* ... */ }
/* 生成
.el-input-group__append button.el-button,
.el-input-group__append div.el-select .el-input__inner,
.el-input-group__append div.el-select:hover .el-input__inner,
.el-input-group__prepend button.el-button,
.el-input-group__prepend div.el-select .el-input__inner,
.el-input-group__prepend div.el-select:hover .el-input__inner */
button .el-button ,
div .el-select .el-input__inner ,
div .el-select:hover .el-input__inner {
// ...
}
/* 生成
.el-input-group__append .el-button,
.el-input-group__append .el-select,
.el-input-group__prepend .el-button,
.el-input-group__prepend .el-select */
.el-button , .el-input { /* ... */ }
}
// 生成 .el-input-group__prepend
@include e (prepend) { /* ... */ }
// 生成 .el-input-group__append
@include e (append) { /* ... */ }
// 生成
@include m (prepend) {
// 生成 .el-input-group--prepend .el-input__inner
.el-input__inner { /* ... */ }
// 生成 .el-input-group--prepend .el-select .el-input.is-focus .el-input__inner
.el-select .el-input.is-focus .el-input__inner { /* ... */ }
}
@include m (append) {
// 生成 .el-input-group--append .el-input__inner
.el-input__inner { /* ... */ }
// 生成 .el-input-group--append .el-select .el-input.is-focus .el-input__inner
.el-select .el-input.is-focus .el-input__inner { /* ... */ }
}
}
/** disalbe default clear on IE */
.el-input__inner::-ms-clear { /* ... */ }
混合指令scroll-bar
定义如下:
复制
/* Scrollbar
-------------------------- */
@mixin scroll-bar {
$--scrollbar-thumb-background : #b4bccc ;
$--scrollbar-track-background : #fff ;
& ::-webkit-scrollbar {
z-index : 11 ;
width : 6 px ;
& : horizontal {
height: 6 px ;
}
& -thumb {
border-radius : 5 px ;
width : 6 px ;
background : $--scrollbar-thumb-background ;
}
& -corner {
background : $--scrollbar-track-background ;
}
& -track {
background : $--scrollbar-track-background ;
& -piece {
background : $--scrollbar-track-background ;
width : 6 px ;
}
}
}
}
📚参考&关联阅读
"getComputedStyle",MDN
"CSS/box-sizing",MDN
"Element/scrollHeight",MDN
"表单输入绑定",vuejs
"input_event",MDN
"change_event",MDN
"Input/text",MDN
"String",MDN