打造你自己的UI组件库(一)——一个功能强大的 Vue 输入框组件

444 阅读4分钟

打造你自己的UI组件库(一)——一个功能强大的 Vue 输入框组件

在这篇博客中,我们将拆解并深入剖析一个功能强大的 Vue 输入框组件代码。这一组件实现了丰富的功能,如自动聚焦、清除按钮、错误提示、附加图标点击事件等。


组件功能概览

这是一个通用输入框组件,支持以下特性:

  • 自定义占位符(placeholder
  • 自动聚焦和自动全选(isAutoFocusisAutoSelect
  • 前置图标、清除图标、附加图标
  • 错误提示及错误样式动画
  • 自定义选择范围(selectedRange
  • 输入事件、清除事件等多种事件监听
  • 可配置的只读模式(isReadonly

接下来,我们将分模块讲解这个组件的实现。


Props:灵活的属性定义

Props 定义说明

组件支持多种参数,以满足不同使用场景。以下是主要的 props 配置:

const props = defineProps({
   value: {
    type: String,
    default: '',
  },
  placeholder: {
    type: String,
    default: '',
  },
  type: {
    type: String,
    default: 'text',
  },
  isError: Boolean,
  errorMsg: {
    type: String,
    default: '',
  },
  isAutoFocus: {
    type: Boolean,
    default: false,
  },
  selectedRange: {
    type: Array,
    validator(value: unknown): boolean {
      return Array.isArray(value) && value.length === 2 && value.every(val => typeof val === 'number')
    },
    default() {
      return undefined
    },
  },
  autocomplete: {
    type: String,
    default: '',
  },
  isAutoSelect: {
    type: Boolean,
    default: false,
  },
  isReadonly: {
    type: Boolean,
    default: false,
  },
  frontIcon: {
    type: [String, Object] as PropType<IconType>,
    default: '',
  },
  clearIcon: {
    type: [String, Object] as PropType<IconType>,
    default: '',
  },
  ...
});

特性解析

  1. 基础属性

    • valueplaceholder 是标准的输入框配置项,用于绑定值和设置占位文本。
    • type 定义输入框类型,如 textpassword 等。
  2. 状态控制

    • isErrorerrorMsg 用于显示错误样式和错误信息。
    • isAutoFocusisAutoSelect 控制自动聚焦和自动全选功能。
  3. 验证机制

    • selectedRange 是一个数组,表示输入框的选择范围([start, end])。我们通过 validator 确保其为合法的 [number, number] 数组。

核心逻辑实现

自动聚焦与全选功能

组件在 onMounted 生命周期中通过 DOM 操作实现自动聚焦和自动全选:

onMounted(() => {
  if (inputElement.value) {
    if (props.isAutoFocus) {
      inputElement.value.focus();
    }
    if (props.isAutoSelect) {
      updateSelectedRange([0, 9999]); // 全选文本
    }
    if (props.selectedRange) {
      updateSelectedRange(props.selectedRange as [number, number]);
    }
  }
});

我们定义了一个 updateSelectedRange 函数,用于动态设置文本选中范围:

const updateSelectedRange = (range: [number, number] = [props.value.length, props.value.length]) => {
  if (!inputElement.value) return;
  inputElement.value.select();
  inputElement.value.setSelectionRange(range[0], range[1]);
};

输入框的清除功能

通过监听 clearIcon 的点击事件,清空输入框内容并触发 clear 事件:

const handleClearInput = () => {
  inputValue.value = '';
  emits('clear');
};

clearIcon 的显示逻辑由计算属性控制:

const showClearIcon = computed(() => inputValue.value?.length > 0);

在模板中绑定点击事件实现清除功能:

<span
  v-if="clearIcon"
  v-show="showClearIcon"
  class="input-clear-icon"
  @click="handleClearInput"
>
  <img :src="clearIcon" />
</span>

图标点击事件

组件支持附加图标(appendIcon)点击事件,并允许动态选择文本范围:

const handleClickAppendIcon = () => {
  props.handleAppendIconFunc();
  if (!props.disableSelectRangeOnAppendIcon) {
    setTimeout(() => {
      updateSelectedRange([inputValue.value.length, inputValue.value.length]);
    }, 0);
  }
};

模板中的点击绑定实现如下:

<span
  v-if="appendIcon"
  class="input-append-icon"
  @click="handleClickAppendIcon"
>
  <img :src="appendIcon" />
</span>

事件管理:输入、焦点和键盘事件

组件通过 defineEmits 管理各种事件,包括 clearfocusblurenterchangeinputChange

const emits = defineEmits<{
  (e: 'clear'): void;
  (e: 'focus'): void;
  (e: 'blur', v: string): void;
  (e: 'enter', v: string): void;
  (e: 'change', v: string): void;
  (e: 'inputChange', v: string): void;
}>();

在模板中使用事件绑定:

<input
  ref="inputElement"
  v-model="inputValue"
  class="input-field"
  :type="type"
  :placeholder="placeholder"
  @change="emits('change', inputValue)"
  @focus="emits('focus')"
  @blur="emits('blur', inputValue)"
  @input="emits('inputChange', inputValue)"
  @keydown.enter.stop="emits('enter', inputValue)"
/>

样式实现

语义化 CSS 类名。如下:

css:
.input-container {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.input-field {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 8px;
  transition: all 0.3s ease;
}

.input-field:focus {
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.input-clear-icon,
.input-append-icon {
  position: absolute;
  right: 12px;
  cursor: pointer;
}

.input-clear-icon {
  color: #6c757d;
}

.input-error {
  border-color: #dc3545;
}

.input-error:focus {
  box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
}
tailwind版:
const inputClasses = computed(() => ({
  'h-8 px-3 py-2': props.size !== 'large',
  'h-10 px-4 py-2': props.size === 'large',
  'bg-light-input dark:bg-dark-input': true,
  'border-red-800': props.isError,
  // ...
}))
动画实现
@keyframes wiggle {
  0%,
  100% {
    transform: translateX(0);
  }
  10%,
  30%,
  50%,
  70%,
  90% {
    transform: translateX(-4px);
  }
  20%,
  40%,
  60%,
  80% {
    transform: translateX(4px);
  }
}
.animate__wiggle {
  animation: wiggle 1s;
}

Template结构:

  <div
    class="relative"
    :class="{
      'empty': !value,
      'animate__wiggle': isError ,
      'space-y-2': isError && errorMsg
    }"
  >
    <BaseInputIcon
      v-if="frontIcon"
      :icon="frontIcon"
      :style="frontIconStyle"
      class="left-2"
    />

    <BaseInputIcon
      v-if="clearIcon && showClearIcon"
      :icon="clearIcon"
      class="right-2 cursor-pointer"
      @click="handleClearInput"
    />

    <BaseInputIcon
      v-if="appendIcon"
      :icon="appendIcon"
      class="right-2 cursor-pointer"
      @click="handleClickAppendIcon"
    />

    <input
      ref="inputElement"
      v-model="inputValue"
      :class="inputClasses"
      :type="type"
      :placeholder="placeholder"
      :autocomplete="autocomplete"
      :readonly="isReadonly"
      :data-focus="dataFocus || null"
      :data-blur="dataBlur || null"
      :tabindex="tabIndex"
      @change="emits('change', inputValue)"
      @focus="emits('focus')"
      @blur="emits('blur', inputValue)"
      @input="emits('inputChange', inputValue)"
      @keydown.enter.stop="emits('enter', inputValue); emits('validate', inputValue)"
    />

    <p
      v-if="isError && errorMsg"
      class="text-sm text-red-800 dark:text-red-700"
    >
      {{ errorMsg }}
    </p>
  </div>

效果展示

在这里插入图片描述

总结

这个输入框组件展示了如何构建一个企业级的 Vue 组件:

  • 类型安全
  • 功能完整
  • 样式灵活
  • 交互友好
  • 易于扩展

还有很多过程被我自动省略了: tailwind的引入配置、类型的设置、参数传递的过程、图标的配置导入,之后如果浏览量和需求上来的话,会做补充。可以作为开发类似组件的参考实现,其他实现或技术相关问题欢迎前往作者公众号交流~

完整源代码较长,可关注后联系作者获取(无套路,直接给)