携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
简介
前文 中介绍了组件的整体结构以及单行文本输入功能的实现。本文将继续深入分析其组件生命周期、表单验证等内容,耐心读完,相信会对您有所帮助。
为了更好的理解本文,请先阅读以下文章
更多组件分析详见 👉 📚 Element UI 源码剖析组件总览 。
本专栏的 gitbook
版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!
生命周期
在生命周期钩子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];
},
}
组件实现渲染效果如下:
📚参考&关联阅读
"表单输入绑定",vuejs
"Input/text",MDN
"String",MDN
关注专栏
如果本文对您有所帮助请关注➕、 点赞👍、 收藏⭐!您的认可就是对我的最大支持!
此文章已收录到专栏中 👇,可以直接关注。