前言
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组件中补充。