Vue复习姿势系列之UI组件——输入框(Input)

1,398 阅读2分钟

前言

该文章是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. 基础功能

  1. 基础输入框比较简单,调整下样式和实现v-model功能即可。(v-model语法糖由v-bind:value$emit("input")两个功能组成) src/packages目录下新建input文件夹,文件夹内创建input.vueindex.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;
    }
  }
}

image.png

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;
    }
  }
}

image.png

3. 可清空

  1. input元素padding-right增大,空出足够的空间放置图标位置。
  2. 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;
    }
  }
}

image.png

4. 带icon的输入框

实现思路与clearable类似,区别在于只是作为装饰,并且头尾两边都可以设置。

  1. 增加prefix-icon属性,设置该属性后input元素的paading-left增大,空出图标位置。
  2. 增加suffix-icon属性,设置该属性后input元素的padding-right增大,空出图标位置。
  3. 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;
    }
  }
}

image.png

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;
  }
}

image.png

6. 带输入建议

输入建议功能类似单选框,在聚焦时或者用户输入时提供备选项。

  1. 新增备选项弹窗,在focus时显示,选择备选项或者点击其他位置时关闭弹窗。
  2. 新增suggestion属性,用来控制是否启动输入建议功能。
  3. 新增fetch-suggestions属性,类型为Function,作为输入建议值的获取。实现方式让用户定,组件提供callback函数。
  4. 涉及到输入后相关动作,需要使用debounce函数。
  5. 建议项被点击时,设置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)
}

image.png

7. 自定义建议模板

  1. 使用作用域插槽来实现该功能,暴露出备选项的默认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>

image.png

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);
          }
        }
      }
    }
  }
}

CPT2110032042-296x239.gif

结语

目前做了输入框常用的一些功能,虽然每个都简单,但整到一起还挺繁琐的。其中花费时间最长的是带输入建议功能,需要提供额外的弹出框来显示建议。
自定义建议模板功能使用了vue的作用域插槽。
input事件的触发频率很高,采用防抖机制做优化。

涉及知识点及参考文章

1. vue作用域插槽,你真的懂了吗
2. 7分钟理解JS的节流、防抖及使用场景