引言
学习Element UI的input模块笔记。Element UI的input组件主要分成两大类:input和textarea。其中最主要和常用的是input类型。
渲染的源码:
<!-- 前置元素 -->
<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"
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
ref="input"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
<!-- 前置内容 -->
<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>
<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>
解释1:组件内部通过判断是否使用插槽prepend和append来渲染前置元素和后置元素
---> 使用方式
<div>
<el-input placeholder="请输入内容" v-model="input1">
<template slot="prepend">前置元素</template>
</el-input>
</div>
<div style="margin-top: 15px;">
<el-input placeholder="请输入内容" v-model="input2">
<template slot="append">后置元素</template>
</el-input>
</div>
---> 源码
<!-- 前置元素 -->
<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>
解释2:使用带Icon的输入框有两张方法,一种是slot方式,一种是属性方式。
<!-- 前置内容 -->
<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>
解释3:使用Icon的有两种类型,一种是前置内容,一种是后置内容,其中前置内容比较简单,这里不讲,后置内容比较复杂,包含清除(clearable)、显示密码(show-password)、显示输入文本数量(show-word-limit)和文本状态等。
<!-- 后置内容 -->
<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>
<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>
组件的computed和methods
computed
computed: {
// 计算组件el-from-item的size属性
_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];
},
// 计算textarea输入框的高度
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
// 计算输入框的长度,三种,优先级从左到右,第一个是组件的el-input的size属性,第二个是组件el-form-item的属性size,第三个是全局设置的属性size。
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
// 判断输入框是否可以使用,条件有两个,一个是组件el-input的disabled属性,一个是组件el-from的disabled属性。
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
// 把输入框内容字符串化。
nativeInputValue() {
return this.value === null || this.value === undefined ? '' : String(this.value);
},
// 判断是否显示“删除”按钮
showClear() {
return this.clearable &&
!this.inputDisabled &&
!this.readonly &&
this.nativeInputValue &&
(this.focused || this.hovering);
},
// 判断是否显示“查看密码”按钮
showPwdVisible() {
return this.showPassword &&
!this.inputDisabled &&
!this.readonly &&
(!!this.nativeInputValue || this.focused);
},
// 判断是否显示输入框的文本长度提示框。
isWordLimitVisible() {
return this.showWordLimit &&
this.$attrs.maxlength &&
(this.type === 'text' || this.type === 'textarea') &&
!this.inputDisabled &&
!this.readonly &&
!this.showPassword;
},
// 计算设定的最大文本长度
upperLimit() {
return this.$attrs.maxlength;
},
// 计算文本长度
textLength() {
if (typeof this.value === 'number') {
return String(this.value).length;
}
return (this.value || '').length;
},
inputExceed() {
// show exceed style if length of initial value greater then maxlength
return this.isWordLimitVisible &&
(this.textLength > this.upperLimit);
}
}
input里面的计算属性,部分是用来判断是否显示图标,比如:取消按钮、查看密码按钮等,其中inputDisabled计算属性的this.elForm是由组件el-form注入,通过el-from组件的属性disabled可以控制el-input是否可用。
methods
methods: {
// 输入框自动聚焦
focus() {
this.getInput().focus();
},
// 输入框自动失去焦点
blur() {
this.getInput().blur();
},
// 无被使用
getMigratingConfig() {
return {
props: {
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
'click': 'click is removed.'
}
};
},
// 输入框失去焦点触发事件
handleBlur(event) {
this.focused = false;
this.$emit('blur', event);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
}
},
select() {
this.getInput().select();
},
// 计算textarea框的高度
resizeTextarea() {
if (this.$isServer) return;
const { autosize, type } = this;
if (type !== 'textarea') return;
if (!autosize) {
this.textareaCalcStyle = {
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
};
return;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
},
setNativeInputValue() {
const input = this.getInput();
if (!input) return;
if (input.value === this.nativeInputValue) return;
input.value = this.nativeInputValue;
},
// 输入框获取焦点触发事件
handleFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
// 输入法编辑器开始新的输入合成时会触发事件
handleCompositionStart() {
this.isComposing = true;
},
// 当一个字符被添加到正在编写的文本段落中时触发这个事件
handleCompositionUpdate(event) {
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
this.isComposing = !isKorean(lastCharacter);
},
// 当文本段落的组成完成或取消时, 这个事件将被触发
handleCompositionEnd(event) {
if (this.isComposing) {
this.isComposing = false;
this.handleInput(event);
}
},
// 输入框输入事件
handleInput(event) {
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
if (this.isComposing) return;
// hack for 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;
this.$emit('input', event.target.value);
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
this.$nextTick(this.setNativeInputValue);
},
// 输入框值改变触发事件
handleChange(event) {
this.$emit('change', event.target.value);
},
// 计算图标的偏移位置
calcIconOffset(place) {
let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
if (!elList.length) return;
let el = null;
for (let i = 0; i < elList.length; i++) {
if (elList[i].parentNode === this.$el) {
el = elList[i];
break;
}
}
if (!el) return;
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');
}
},
// 更新图标的偏移位置
updateIconOffset() {
this.calcIconOffset('prefix');
this.calcIconOffset('suffix');
},
// 清除图标点击事件
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
},
// 显示密码点击事件
handlePasswordVisible() {
this.passwordVisible = !this.passwordVisible;
this.focus();
},
// 获取输入框的dom
getInput() {
return this.$refs.input || this.$refs.textarea;
},
// 是否显示后置内容
getSuffixVisible() {
return this.$slots.suffix ||
this.suffixIcon ||
this.showClear ||
this.showPassword ||
this.isWordLimitVisible ||
(this.validateState && this.needStatusIcon);
}
}
methods的方法大部分比较简单,需要注意的有:
- 输入框失去焦点事件触发的函数handleBlur中的this.dispatch('ElFormItem', 'el.form.blur', [this.value]) 操作,这个主要是触发组件el-form-item的表单验证的。
- calcIconOffset方法是用于计算前置元素prepend和后置元素append的偏移量,在组件生命周期mounted、updated和watch的type的方法里边会被调用,这个方法比较复杂。
结语
el-input组件的源码相对比较简单,只要使用过vue,组件el-input,加上Element-UI的官方文档对el-input的说明,基本上都能看懂。