Element Plus 组件库实现:7. Input组件

227 阅读2分钟

前言

Input组件作为用户与软件交互的桥梁,其重要性不言而喻。一个优秀的Input组件,不仅能够提升用户体验,还能为软件的功能性提供坚实的支撑。本文将简单介绍Input组件的基本实现思路。

需求分析

一个Input组件要具备以下基本功能:

  • 支持Input/Textarea两种模式
  • 支持不同大小
  • 一键清空
  • 切换密码显示
  • 自定义前缀/后缀(prefix/suffix),如图标
  • 自定义前置/后置(prepend/append),如邮箱后缀,域名的前缀http
  • Input的一些原生属性

确定方案

  • 属性
export interface InputProps {
  type?: string
  // 实现v-model必备的
  modelValue: string
  size?: 'large' | 'small'
  disabled?: boolean
  clearable?: boolean
  // 展示密码
  showPassword?: boolean
  placeholder?: string
  readonly?: boolean
  // 自动填充
  autocomplete?: string
  autofocus?: boolean
  // 关联表单
  form?: string
}
  • 事件
export interface InputEmits {
   // 和modelValue结合支持v-model属性
  (e: 'update:modelValue', value: string): void
  // input事件就是指值有变化就算
  (e: 'input', value: string): void
  // input的change事件指修改了值, 并且失去了focus
  (e: 'change', value: string): void
  (e: 'focus', value: FocusEvent): void
  (e: 'blur', value: FocusEvent): void
  (e: 'clear'): void
}
  • 实例
// 最终要暴露出去的
export interface InputInstance {
  ref: HTMLInputElement | HTMLTextAreaElement
}

代码实现

  • 组件
<template>
<!-- 仅展示插槽相关,其他属性自行脑部 -->
    <div class="yv-input" :class="{
        'is-prepend': $slots.prepend,
        'is-prefix': $slots.prefix
        >
        <!-- input形式 -->
        <template v-if="type !== 'textarea'">
            <!-- 输入框前置内容,只对非 type="textarea" 有效 -->
            <div v-if="$slots.prepend" class="yv-input__prepend">
                <slot name="prepend"></slot>
            </div>
            <!-- 输入框 -->
            <div class="yv-input__wrapper">
                <!-- 输入框头部内容,只对非 type="textarea" 有效 -->
                <span v-if="$slots.prefix" class="yv-input__prefix">
                    <slot name="prefix"></slot>
                </span>
                       <!--原生input属性+拓展属性,仅展示部分 -->
                <input ref="inputRef" v-bind="attrs" class="yv-input__inner"
                     :type="showPassword ? (passwordVisible ? 'text' : 'password') : type" 
                    v-model="innerValue" 
                    @input="handleInput"
                    @change="handleChange" 
                    @focus="handleFocus"
                    @blur="handleBlur" />

                <!-- 输入框尾部内容,只对非 type="textarea" 有效 -->
                <span v-if="$slots.suffix || showClear || showPasswordArea" class="yv-input__suffix" @click="keepFocus">
                    <slot name="suffix"></slot>
                       <!-- 可能用到的图标 -->
                    <Icon @click="clearValue" v-if="showClear" icon="circle-xmark" class="yv-input__clear" />

                    <Icon v-if="showPasswordArea && passwordVisible" icon="eye" @click="togglePasswordVislble"
                        class="yv-input__password" />

                    <Icon v-if="showPasswordArea && !passwordVisible" icon="eye-slash" @click="togglePasswordVislble"
                        class="yv-input__password" />
                </span>
            </div>
            <!-- 输入框后置内容,只对非 type="textarea" 有效 -->
            <div v-if="$slots.append" class="yv-input__append">
                <slot name="append"></slot>
            </div>
        </template>

        <!-- textarea形式 -->
         <!--原生属性+拓展属性,仅展示部分 -->
        <template>
            <textarea ref="inputRef" v-bind="attrs" class="yv-textarea__wrapper" v-model="innerValue" @input="handleInput" @change="handleChange" @focus="handleFocus"
                @blur="handleBlur" />
        </template>
    </div>
</template>
  • 定义属性和事件
const props = withDefaults(defineProps<InputProps>(), 
    { 
        type: 'text', 
        autocomplete: 'off' 
    }
)
const emits = defineEmits<InputEmits>()
  • 禁止透传
defineOptions({
    name: 'YvInput',
    inheritAttrs: false
})
// 使用手动传入
const attrs = useAttrs()
  • 其他属性和值
// 输入框绑定的值
const innerValue = ref(props.modelValue)
watch(() => props.modelValue, (newVal) => {
    innerValue.value = newVal
})
// 是否聚焦
const isFocus = ref(false)
// 密码是否可见
const passwordVisible = ref(false)
const togglePasswordVislble = () => {
    passwordVisible.value = !passwordVisible.value
}

// 展示清除图标的条件
const showClear = computed(() => props.clearable && !props.disabled && !!innerValue.value && isFocus.value)
// 当输入框为密码框时,切换输入框类型
const showPasswordArea = computed(() => props.showPassword && !props.disabled && !!innerValue.value)
  • 其他方法
const keepFocus = async () => {
    await nextTick()
    inputRef.value.focus()
}

const handleInput = () => {
    emits('update:modelValue', innerValue.value)
    emits('input', innerValue.value)
}

const handleChange = () => {
    emits('change', innerValue.value)
}

const handleFocus = (event: FocusEvent) => {
    isFocus.value = true
}
const handleBlur = (event: FocusEvent) => {
    isFocus.value = false
    emits('blur', event)
}
const clearValue = () => {
    innerValue.value = ''
    emits('update:modelValue', '')
    emits('clear')
    emits('input', '')
    emits('change', '')
}
  • 暴露input节点
// 将inputRef确定为DOM类型
const inputRef = ref() as Ref<HTMLInputElement>
defineExpose({
    ref: inputRef
})

总结

本文简单介绍了一个Input组件的基本实现方式,思路就是基于原生的input进行属性的拓展和方法的添加,关于Input的验证功能,将在Form组件中补充。