Element组件源码研究-Input输入框

5,751 阅读3分钟

本文的研究思路是通过阅读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>

由于attrs只包含属性不包含事件,所以在组件里添加@input="attrs只包含属性不包含事件,所以在组件里添加@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>

再写组件:

  1. 为组件的props添加disabled: Boolean。
  2. 把组件根div的class改为
:class="[
        'el-input',
        {
            'is-disabled': disabled,
        }
        ]"

3,给input标签添加:disabled="disabled"

效果如下:

可清空

老规矩,先把测试代码写在测试页里。

 <el-input placeholder="请输入密码" v-model="input" show-password></el-input>

编写Input组件:

  1. 增加一个clearable属性,为true的时候开启可清空功能

  2. 增加一个清空按钮,在mouseover和focus时候且输入框有值的时候显示,否则隐藏。

  3. 给清空按钮添加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。

  1. 增加type属性,增加showPassword属性,绑定input的type为
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
  1. 增加查看密码按钮,并实现其逻辑:
// 为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两种方式。

编写组件代码:

  1. 添加suffixIcon,prefixIcon两个属性。
  2. 添加前置后置两部分的标签。
<!-- 前置内容 -->
<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>
  1. 为根div添加样式判断逻辑。
'el-input--prefix': $slots.prefix || prefixIcon,
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword

效果如下:

文本域

显然,文本域的type不再是text,是textarea。由于textarea没有前后置内容和元素。且有独有resize、自适应高度特性。所以在编写的时候,用一个新标签做它的呈现。

  1. 编写标签。
 <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…

其他组件源码研究:

Element组件源码研究-Button

Element组件源码研究-Input输入框

Element组件源码研究-Layout,Link,Radio

Element组件源码研究-Checkbox多选框

Element组件源码研究-InputNumber 计数器

Element组件源码研究-Loading组件

Element组件源码研究-Message组件