前言
该文章是Vue复习姿势系列的第四篇,分享输入框组件的实现过程。
脚手架请参考第一篇: juejin.cn/post/701798…
组件示例地址: oversnail.github.io/over-snail-…
git地址: github.com/overSnail/o…
第一篇:Vue复习姿势系列之UI组件——按钮(Button)
第二篇:Vue复习姿势系列之UI组件——单选框(Radio)
第三篇:Vue复习姿势系列之UI组件——复选框(Checkbox)
介绍
基础表单组件,处理用户输入功能。
功能实现
1. 基础功能
- 基础输入框比较简单,调整下样式和实现
v-model功能即可。(v-model语法糖由v-bind:value和$emit("input")两个功能组成)src/packages目录下新建input文件夹,文件夹内创建input.vue和index.js。
src/styles目录下心新建input.scss,并在src/styles/index.scss中引入。
// input.vue
<template>
<div class="my-input">
<input
class="my-input-input"
type="text"
:value="currentValue"
@input="handleInput"
/>
</div>
</template>
<script>
export default {
name: 'MyInput',
data() {
return {
currentValue: this.value, // 当前输入值
}
},
props: {
value: {
type: String,
default: '',
},
},
watch: {
value: {
handler(newVal) {
this.currentValue = newVal
},
immediate: true,
},
},
methods: {
handleInput(_e) {
const value = _e.target.value
this.currentValue = value
this.$emit('input', value)
},
},
}
</script>
// input.scss
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(input) {
&-input {
height: 36px;
line-height: 36px;
padding: 7px 10px;
box-sizing: border-box;
border-radius: 4px;
border: 1px solid $--border-color;
outline: none;
font-size: $--font-size-medium;
&:focus {
border-color: $--color-primary;
box-shadow: 0 0 4px $--color-primary;
}
}
}
2. 禁用状态
原生input元素的disabled属性以及调整下样式即可实现。
// input.vue 省略部分代码
<template>
<div class="my-input">
<input
class="my-input-input"
:class="{
'my-input-input-disabled': disabled
}"
type="text"
:value="currentValue"
@input="handleInput"
:placeholder="placeholder"
:disabled="disabled"
/>
</div>
</template>
<script>
export default {
......
props: {
......
// 占位符
placeholder: {
type: String,
default: "请输入"
},
// 是否禁用
disabled: {
type: Boolean,
default: false
}
},
}
</script>
// input.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(input) {
&-input {
......
// 禁用状态样式
&-disabled {
cursor: not-allowed;
background-color: #f5f7fa;
}
}
}
3. 可清空
input元素padding-right增大,空出足够的空间放置图标位置。value不为空时,显示清空图标。
// input.vue 省略部分代码
<template>
<div class="my-input" :class="{ 'my-input-disabled': disabled }">
<input
class="my-input-input"
:class="{
'my-input-input-disabled': disabled,
'my-input-input-icon': clearable,
}"
type="text"
:value="currentValue"
@input="handleInput"
:placeholder="placeholder"
:disabled="disabled"
/>
<!-- 图标位置 -->
<span class="my-input-icon">
<i
class="iconfont icon-close"
v-if="clearable && currentValue && !disabled"
@click="handleClean"
></i>
</span>
</div>
</template>
<script>
export default {
......
props: {
......
// 是否显示清空按钮
clearable: {
type: Boolean,
default: false,
},
},
methods: {
/**
* @description 清空输入值
*/
handleClean() {
if (!this.disabled) {
this.currentValue = ''
this.$emit('input', this.currentValue)
}
},
},
}
</script>
// input.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(input) {
display: inline-block;
position: relative;
width: 220px;
&-disabled {
cursor: not-allowed!important;
}
// 输入框相关样式
&-input {
......
&-icon {
padding-right: 24px;
}
}
// 图标区相关样式
&-icon {
position: absolute;
width: 16px;
height: 16px;
right: 6px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
text-align: center;
line-height: 16px;
i {
cursor: pointer;
}
}
}
4. 带icon的输入框
实现思路与clearable类似,区别在于只是作为装饰,并且头尾两边都可以设置。
- 增加
prefix-icon属性,设置该属性后input元素的paading-left增大,空出图标位置。 - 增加
suffix-icon属性,设置该属性后input元素的padding-right增大,空出图标位置。 - 当
clearable=true时,优先显示清空图标。这样一来,input元素是否增大padding-right的判断依据有两个,为了方便,我们采用computed属性来管理。
// input.vue 省略部分代码
<template>
<div class="my-input" :class="{ 'my-input-disabled': disabled }">
<input
......
:class="{
'my-input-input-icon': needPaddingRight,
'my-input-input-icon-suffix': prefixIcon,
}"
......
/>
<!-- 前置图标区域 -->
<span class="my-input-icon my-input-icon-prefix">
<i
class="iconfont"
:class="{
[`${prefixIcon}`]: prefixIcon,
}"
/>
</span>
<!-- 后置图标区域 -->
<span class="my-input-icon">
<i
class="iconfont icon-close"
v-if="clearable && currentValue && !disabled"
@click="handleClean"
/>
<i
v-else
class="iconfont"
:class="{
[`${suffixIcon}`]: suffixIcon,
}"
/>
</span>
</div>
</template>
<script>
export default {
......
props: {
// 前置图标名称
prefixIcon: {
type: String,
default: '',
},
// 后置图标名称
suffixIcon: {
type: String,
default: '',
},
},
......
computed: {
// 是否需要设置输入框的右侧内边距
needPaddingRight() {
return this.clearable && this.suffixIcon
},
},
......
}
</script>
// input.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(input) {
// 输入框相关样式
&-input {
......
&-icon {
padding-right: 24px;
&-suffix {
padding-left: 24px;
}
}
}
// 图标区相关样式
&-icon {
......
&-prefix {
left: 6px;
}
}
}
5. 尺寸
4种尺寸,尺寸属性本身也是对CSS的操作。
// input.vue 省略部分代码
<template>
<div
......
:class="{ 'my-input-disabled': disabled, [`my-input-${size}-size`]: true }"
>
<input
class="my-input-input"
:class="{
......
[`my-input-input-${size}-size`]: true
}"
......
/>
......
</div>
</template>
<script>
// 工具函数,用于判断传入的值是否符合条件
import { oneOf } from '../../utils/assist'
export default {
......
props: {
......
size: {
validator(value) {
return oneOf(value, ['large', 'medium', 'small', 'mini'])
},
type: String,
default: 'medium',
},
},
......
}
</script>
// input.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'mixins/mixins';
@include b(input) {
......
// 输入框相关样式
&-input {
......
// 尺寸相关样式
&-large-size {
padding: 9px 10px;
}
&-medium-size {
padding: 7px 10px;
}
&-small-size {
padding: 5px 10px;
font-size: $--font-size-small;
}
&-mini-size {
padding: 3px 10px;
font-size: $--font-size-small;
}
}
......
// 尺寸相关样式
&-large-size {
height: 40px;
}
&-medium-size {
height: 36px;
}
&-small-size {
height: 32px;
}
&-mini-size {
height: 28px;
}
}
6. 带输入建议
输入建议功能类似单选框,在聚焦时或者用户输入时提供备选项。
- 新增备选项弹窗,在
focus时显示,选择备选项或者点击其他位置时关闭弹窗。 - 新增
suggestion属性,用来控制是否启动输入建议功能。 - 新增
fetch-suggestions属性,类型为Function,作为输入建议值的获取。实现方式让用户定,组件提供callback函数。 - 涉及到输入后相关动作,需要使用
debounce函数。 - 建议项被点击时,设置
value值即可。
// input.vue 省略部分代码
<template>
<div
class="my-input"
:class="{ 'my-input-disabled': disabled, [`my-input-${size}-size`]: true }"
ref="myInput"
>
......
<!-- 输入建议选项框 -->
<transition name="fade-bottom">
<div
class="my-input-suggestion"
v-show="panelVisible"
ref="mySuggestion"
v-if="suggestion && this.options.length > 0"
>
<div
class="my-input-suggestion-cell"
@click="setSuggestion(item.value)"
v-for="item in options"
:key="item.value"
>
{{ item.value }}
</div>
</div>
</transition>
</div>
</template>
<script>
// 工具函数,用于判断传入的值是否符合条件
import { oneOf, debounce } from '../../utils/assist'
export default {
data() {
return {
currentValue: this.value, // 当前输入值
panelVisible: false, // 鼠标在hover阶段
options: [], // 输入建议可选项
}
},
props: {
......
// 是否开启输入建议
suggestion: {
type: Boolean,
default: false,
},
// 输入建议回调函数
fetchSuggestions: {
type: Function,
},
},
mounted() {
// 绑定点击事件
document.addEventListener('click', this.addCloseEvent)
// 初始化进行第一次请求
this.getSuggesitions()
},
beforeDestroy() {
// 释放点击事件
document.removeEventListener('click', this.addCloseEvent)
},
methods: {
......
/**
* @description 输入事件
*/
handleInput(_e) {
const value = _e.target.value
this.currentValue = value
this.$emit('input', value)
debounce(
() => {
this.getSuggesitions()
},
333,
'fetch-suggestions'
)
},
/**
* @description input的hover事件,该函数作用是控制输入提示的显示
*/
handleFocus() {
this.panelVisible = true
this.getSuggesitions();
},
/**
* @description 控制提示框的显示/隐藏
*/
addCloseEvent(event) {
// 点击目标若不是组件内元素时,关闭选项弹窗
let target = event.path.find((d) => d === this.$refs.myInput)
if (!target && this.panelVisible) {
this.panelVisible = false
}
},
/**
* @description 获取输入提示数据
*/
getSuggesitions() {
// 调用用户过滤后的值
this.fetchSuggestions &&
this.fetchSuggestions(this.currentValue, (options) => {
console.log(options)
this.options = options
})
},
/**
* @description 设置所选中的值
*/
setSuggestion(str) {
this.currentValue = str
this.$emit('input', this.currentValue)
this.panelVisible = false
},
},
}
</script>
// index.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'common/animate';
@import 'mixins/mixins';
@include b(input) {
......
// 输入建议弹窗相关样式
&-suggestion {
width: 100%;
min-height: 56px;
max-height: 160px;
transform-origin: center top;
z-index: 2367;
position: absolute;
top: calc(100% + 4px);
left: 0;
border: solid 1px #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
@include scrollBar;
&-cell {
padding: 0 10px;
line-height: 28px;
font-size: $--font-size-medium;
cursor: pointer;
&:hover {
background-color: #f5f7fa;
}
}
}
}
// utils/assist.js 省略部分代码
/**
* @description 防抖函数
* @param {Function} func 回调函数
* @param {number} wait 防抖间隔
* @param {string} name 计时器名称,计时器挂在到window上
*/
export function debounce(func, wait, name) {
if (window[name]) clearTimeout(window[name])
window[name] = setTimeout(function() {
func()
window[name] = undefined
}, wait)
}
7. 自定义建议模板
- 使用作用域插槽来实现该功能,暴露出备选项的默认
slot,同时将item参数传递给该插槽。
// input.vue 省略部分代码
<template>
<div
class="my-input"
:class="{ 'my-input-disabled': disabled, [`my-input-${size}-size`]: true }"
ref="myInput"
>
......
<!-- 输入建议选项框 -->
<transition name="fade-bottom">
<div
class="my-input-suggestion"
v-show="panelVisible"
ref="mySuggestion"
v-if="suggestion && this.options.length > 0"
>
<div
class="my-input-suggestion-cell"
@click="setSuggestion(item.value)"
v-for="item in options"
:key="item.value"
>
<slot :item="item">
{{ item.value }}
</slot>
</div>
</div>
</transition>
</div>
</template>
......
</script>
8. 远程搜索
带输入功能的实现方式是由用户来提供数据,因此远程搜索已经是实现了的。只需要在执行用户回调之前加上loading效果,回调执行时再移除loading效果即可。
// input.vue 省略部分代码
<template>
<div
class="my-input"
:class="{ 'my-input-disabled': disabled, [`my-input-${size}-size`]: true }"
ref="myInput"
>
......
<!-- 输入建议选项框 -->
<transition name="fade-bottom">
<div
class="my-input-suggestion"
v-show="panelVisible"
ref="mySuggestion"
v-if="suggestion && this.options.length > 0"
>
......
<!-- 加载中提示 -->
<div class="my-input-suggestion-layer" v-show="loading">
<i class="iconfont icon-loading my-input-suggestion-layer-loading" />
</div>
</div>
</transition>
</div>
</template>
<script>
......
export default {
name: 'MyInput',
data() {
return {
......
loading: false, // 是否正在加载中
}
},
......
methods: {
......
/**
* @description 获取输入提示数据
*/
getSuggesitions() {
if (this.fetchSuggestions) {
this.loading = true;
this.fetchSuggestions(this.currentValue, (options) => {
this.loading = false;
this.options = options
})
}
},
......
},
}
</script>
// input.scss 省略部分代码
@charset "UTF-8";
@import 'common/var';
@import 'common/animate';
@import 'mixins/mixins';
@include b(input) {
......
// 输入建议弹窗相关样式
&-suggestion {
......
&-layer {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
background-color: #fff;
&-loading {
display: inline-block;
animation: loading 2s linear infinite;
@keyframes loading {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
}
}
}
结语
目前做了输入框常用的一些功能,虽然每个都简单,但整到一起还挺繁琐的。其中花费时间最长的是带输入建议功能,需要提供额外的弹出框来显示建议。
自定义建议模板功能使用了vue的作用域插槽。
input事件的触发频率很高,采用防抖机制做优化。