本文的研究思路是通过阅读Element源码,然后自动动手一步一步编写组件,完善其对应功能。
选择Input输入框研究
继研究了Button组件之后,我又看了一下Link组件的源码,跟Button组件类似,复杂度不是很高。随后挑选了Input组件作为今天的研究对象。
准备工作
新建InputShownPage页面,写测试代码:
<template>
<div>
<div class="row">
<el-input v-model="input" placeholder="请输入内容"></el-input>
</div>
</div>
</template>
<script>
import ElInput from '../../components/Input/index'
export default {
name: 'InputShownPage',
methods: {
},
components: {
ElInput
}
}
</script>
在components文件夹下新建Input组件,接下来我们实现一个最基础的Input框。
编写基本的Input
在Input组件中,写
<template>
<div class="el-input">
<template>
<input
class="el-input__inner"
v-bind="$attrs"
/>
</template>
</div>
</template>
<script>
export default {
name: 'ElInput',
};
</script>
知识点:$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)
Input默认会撑满整行,所以我们在测试页InputShownPage为其添加宽度样式:
.row .el-input {
width: 180px;
}
效果如下图:
实现组件的v-model绑定
我们知道v-model由名为value 的 prop 和名为 input 的事件组成。即
<el-input :value="input" @input="(value) => { input = value }"></el-input>
由于emit('input', $event.target.value)"即可。
<input
class="el-input__inner"
v-bind="$attrs"
@input="$emit('input', $event.target.value)"
/>
Input Events支持
Element的文档上Input的事件有blur,focus,change,input,clear。input已经支持,blur,focus,change是原生input标签就支持的。直接暴露出去即可
<input
class="el-input__inner"
v-bind="$attrs"
@input="$emit('input', $event.target.value)"
@focus="$emit('focus', $event.target.value)"
@blur="$emit('blur', $event.target.value)"
@change="$emit('change', $event.target.value)"
/>
clear是当Input支持clearable属性的时候的点击事件,那我们先让Input支持clearable属性。
这样一个基本的Input组件就完成了。
支持禁用状态
先写测试代码
<el-input
placeholder="请输入内容"
v-model="input"
:disabled="true">
</el-input>
再写组件:
- 为组件的props添加disabled: Boolean。
- 把组件根div的class改为
:class="[
'el-input',
{
'is-disabled': disabled,
}
]"
3,给input标签添加:disabled="disabled"
效果如下:
可清空
老规矩,先把测试代码写在测试页里。
<el-input placeholder="请输入密码" v-model="input" show-password></el-input>
编写Input组件:
-
增加一个clearable属性,为true的时候开启可清空功能
-
增加一个清空按钮,在mouseover和focus时候且输入框有值的时候显示,否则隐藏。
-
给清空按钮添加click相应方法,点击后清空输入框的值。具体代码如下:
<template>
<div :class="[
'el-input',
{
'is-disabled': disabled,
'el-input--suffix': clearable,
}
]"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<template>
<input
class="el-input__inner"
v-bind="$attrs"
:value="value"
:disabled="disabled"
@input="$emit('input', $event.target.value)"
@focus="handleFocus"
@blur="handleBlur"
@change="$emit('change', $event.target.value)"
/>
<!-- 后置内容 -->
<span
class="el-input__suffix"
v-if="getSuffixVisible()">
<span class="el-input__suffix-inner">
<i v-if="showClear"
class="el-input__icon el-icon-circle-close el-input__clear"
@mousedown.prevent
@click="clear"
></i>
</span>
</span>
</template>
</div>
</template>
<script>
export default {
name: 'ElInput',
props: {
value: [String, Number],
disabled: Boolean,
clearable: {
type: Boolean,
default: false
},
},
data() {
return {
hovering: false,
focused: false,
};
},
computed: {
nativeInputValue() {
return this.value === null || this.value === undefined ? '' : String(this.value);
},
showClear() {
return this.clearable &&
!this.disabled &&
(this.focused || this.hovering) &&
!!this.nativeInputValue;
},
},
methods: {
handleFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
handleBlur(event) {
this.focused = false;
this.$emit('blur', event);
},
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
},
getSuffixVisible() {
return this.showClear
}
},
mounted() {
},
};
</script>
效果如下:
密码框
老规矩,先把测试代码写在测试页里。
<el-input placeholder="请输入密码" v-model="input" show-password></el-input>
再写组件代码:
Element的密码框,比原生的密码框多了一个查看密码的功能,所以实现起来多了个逻辑,默认情况input的type是passport,点击查看按钮,type就变成text。
- 增加type属性,增加showPassword属性,绑定input的type为
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
- 增加查看密码按钮,并实现其逻辑:
// 为input加入ref
<input ref="input" ... />
// 在clear按钮同级加入查看密码按钮
<i v-if="showPwdVisible"
class="el-input__icon el-icon-view el-input__clear"
@click="handlePasswordVisible"
></i>
handlePasswordVisible() {
this.passwordVisible = !this.passwordVisible;
this.focus();
},
focus() {
this.getInput().focus();
setTimeout(() => {
this.getInput().setSelectionRange(-1,-1);
});
},
blur() {
this.getInput().blur();
},
getInput() {
return this.$refs.input;
},
为此我们还实现了focus和blur这两个方法。setSelectionRange的作用是获取焦点后,可以让光标保持在文本后。
带 icon 的输入框
这里分为前置icon和后置icon。刚才的可清除和查看密码两个按钮,样式上跟后置icon的效果一样。
这里就不贴测试代码了,跟官网上的一样,分为属性和slot两种方式。
编写组件代码:
- 添加suffixIcon,prefixIcon两个属性。
- 添加前置后置两部分的标签。
<!-- 前置内容 -->
<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>
...
<template v-if="!showClear || !showPwdVisible">
<slot name="suffix"></slot>
<i class="el-input__icon"
v-if="suffixIcon"
:class="suffixIcon">
</i>
</template>
- 为根div添加样式判断逻辑。
'el-input--prefix': $slots.prefix || prefixIcon,
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
效果如下:
文本域
显然,文本域的type不再是text,是textarea。由于textarea没有前后置内容和元素。且有独有resize、自适应高度特性。所以在编写的时候,用一个新标签做它的呈现。
- 编写标签。
<template v-if="type !== 'textarea'">
// input标签的内容
</template>
<textarea
v-else
ref="textarea"
class="el-textarea__inner"
v-bind="$attrs"
:disabled="disabled"
:style="textareaStyle"
@input="$emit('input', $event.target.value)"
@focus="handleFocus"
@blur="handleBlur"
@change="$emit('change', $event.target.value)"
>
</textarea>
效果如下:
可自适应文本高度的文本域
思路:要实现自适应高度,第一步就是watch文本value,文本变化时,触发计算文本框的高度,第二步是根据输入的参数autosize="{ minRows: 2, maxRows: 4}实现计算高度算法。
// 第一步
watch: {
value() {
this.$nextTick(this.resizeTextarea);
},
type() { // type改变的时候,也需要计算下高度
this.$nextTick(this.resizeTextarea);
}
},
// 第二步
resizeTextarea() {
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);
},
calcTextareaHeight不贴了,可在我文档底部上传的码云查看,也可以直接差Element源码,直接搬过来的。
复合型输入框
写出测试代码
<el-input placeholder="请输入内容" v-model="input">
<template slot="prepend">Http://</template>
<template slot="append">.com</template>
</el-input>
在组件中添加对应标签和样式即可。
// 给根元素添加样式
'el-input-group': $slots.prepend || $slots.append,
'el-input-group--append': $slots.append,
'el-input-group--prepend': $slots.prepend,
<!-- 前置元素 -->
<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>
效果如下:
尺寸
通过控制样式即可,给跟标签添加样式:
this.size ? 'el-input--' + this.size : '',
输入长度限制
带输入建议 自定义模板 远程搜索 这三点都是另一个Input组件el-autocomplete的特性。暂不写在这里。先研究输入长度限制特性,Element的Input组件会有显示总字数和已写字数。
// 对于text
<span v-if="isWordLimitVisible" class="el-input__count">
<span class="el-input__count-inner">
{{ textLength }}/{{ upperLimit }}
</span>
</span>
// 对于textarea
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
// 如果showWordLimit为ture,显示总字数和已写字数
isWordLimitVisible() {
return this.showWordLimit &&
this.$attrs.maxlength &&
(this.type === 'text' || this.type === 'textarea') &&
!this.disabled &&
!this.showPassword;
},
// 总字数
upperLimit() {
return this.$attrs.maxlength;
},
// 已写字数
textLength() {
if (typeof this.value === 'number') {
return String(this.value).length;
}
return (this.value || '').length;
},
效果如下:
总结
至此,Element的Input组件大部分功能我们都模仿完毕了。
本文的所有代码已上传至码云:gitee.com/DaBuChen/my…
其他组件源码研究: