1.需求分析
支持text和textarea
支持不同大小
支持密码显示,支持隐藏/显示密码
支持前缀/后缀,可填充图标/模板等
支持前置/后置用于label
支持原生属性:placeholder等
暴露输入框实例,提供自定义操作。
2.编码实现
2.1类型定义
export interface InputProps {
type?: string;
modelValue: string;
size?: 'large' | 'small';
disabled?: boolean;
clearable?: boolean;
showPassword?: boolean;
placeholder?: string;
readonly?: boolean;
autocomplete?: string;
autofocus?: boolean;
form?: string;
}
export interface InputEmits {
(e: 'update:modelValue', value: string) : void;
// input 的 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;
}
2.2模板处理逻辑
1.通过判断传入props的type显示text/textarea
2.前置/后置通过具名插槽,$slot.prefix获取是否传入值,来显示/隐藏
3.输入框主体包括前缀、后缀、输入框,前后缀通过具名,插槽$slot来显示隐藏,输入框通过动态赋予属性
属性处理逻辑
输入框主要需要判断是否显示为密码、是否禁用、是否只可读、是否自动填充、默认提示文字、是否自动聚焦,同时设置inheritAttrs: false来手动设置透传到input上
在后缀中需要添加清除图标和切换可见性图标,清除图标只有在可以清除&&输入框有内容&&聚焦时显示。切换可见性图标只有在输入框为密码类型&&输入框有值&&没有禁用时显示
<div class="yd-input__wrapper">
<!-- prefix -->
<span v-if="$slots.prefix" class="yd-input__prefix">
<slot name="prefix"></slot>
</span>
<!-- input -->
<input ref="inputRef" v-bind="attr" @change="handleChange" @blur="handleBlur" @focus="handleFocus"
@input="handleInput" v-model="innerValue"
:type="showPasswordArea ? (passwordVisible ? 'text' : 'password') : type" :class="`yd-input__inner`"
:disabled="disabled" :readonly="readonly" :autocomplete="autocomplete" :placeholder="placeholder"
:autofocus="autofocus" :form="form">
<!-- suffix -->
<span @click="keepFocus" v-if="$slots.suffix || showClear || showPasswordArea" class="yd-input__suffix">
<slot name="suffix"></slot>
<Icon @click="handleClear" @mousedown.prevent="NOOP" icon="circle-xmark" v-if="showClear"
class="yd-input__clear"></Icon>
<Icon @click="handleHiddenPassword" @mousedown.prevent="NOOP" icon="eye"
v-if="showPasswordArea && passwordVisible" class="yd-input__password"></Icon>
<Icon @click="handleShowPassword" @mousedown.prevent="NOOP" icon="eye-slash"
v-if="showPasswordArea && !passwordVisible" class="yd-input__password"></Icon>
</span>
</div>
2.3事件处理逻辑
输入框有输入事件、输入框变化事件、输入框聚焦事件、输入框失去焦点事件。各类事件都要触发对应的事件,在聚焦、失去焦点中都需要修改现在的聚焦状态。
const handleInput = () => {
emits('update:modelValue', innerValue.value)
emits('input', innerValue.value)
runValidation('input')
}
const handleChange = () => {
emits('change', innerValue.value)
runValidation('change')
}
const handleClear = () => {
console.log('clear')
innerValue.value = ""
emits('update:modelValue', '')
emits('clear')
emits('input', '')
emits('change', '')
}
const handleFocus = (e: FocusEvent) => {
isFocus.value = true
emits('focus', e)
}
const handleBlur = (e: FocusEvent) => {
isFocus.value = false
emits('blur', e)
runValidation('blur')
}
const handleShowPassword = () => {
passwordVisible.value = true
}
const handleHiddenPassword = () => {
passwordVisible.value = false
}
前后缀包裹总体有保持聚焦事件,通过获取input的原生实例,使用inputRef.value.focus()保持输入框聚焦,否则点击前后缀会失去输入框的聚焦。
清除图标有清除事件,需要注意需要绑定一个mousedown.prevent事件绑定一个NOOP(无用)函数,否则无法完成清除作用。因为点击清除图标时,首先触发了input的blur事件,先改变了可见性,所以没有触发清除操作。通过mousedown.prevent阻止blur事件的发生。
密码显示切换图标有显示切换事件,直接设置值即可,同时它也需要绑定mousedown.prevent。
<!-- suffix -->
<span @click="keepFocus" v-if="$slots.suffix || showClear || showPasswordArea" class="yd-input__suffix">
<slot name="suffix"></slot>
<Icon @click="handleClear" @mousedown.prevent="NOOP" icon="circle-xmark" v-if="showClear"
class="yd-input__clear"></Icon>
<Icon @click="handleHiddenPassword" @mousedown.prevent="NOOP" icon="eye"
v-if="showPasswordArea && passwordVisible" class="yd-input__password"></Icon>
<Icon @click="handleShowPassword" @mousedown.prevent="NOOP" icon="eye-slash"
v-if="showPasswordArea && !passwordVisible" class="yd-input__password"></Icon>
</span>
2.4样式处理逻辑
通过动态赋予类来实现样式动态展示
<div class="yd-input" :class="{
[`yd-input--${type}`]: type,
[`yd-input--${size}`]: size,
'is-disabled': disabled,
'is-prepend': $slots.prepend,
'is-append': $slots.append,
'is-prefix': $slots.prefix,
'is-suffix': $slots.suffix,
'is-focus':isFocus
}">
.yd-input {
--yd-input-text-color: var(--yd-text-color-regular);
--yd-input-border: var(--yd-border);
--yd-input-hover-border: var(--yd-border-color-hover);
--yd-input-focus-border: var(--yd-color-primary);
--yd-input-transparent-border: 0 0 0 1px transparent inset;
--yd-input-border-color: var(--yd-border-color);
--yd-input-border-radius: var(--yd-border-radius-base);
--yd-input-bg-color: var(--yd-fill-color-blank);
--yd-input-icon-color: var(--yd-text-color-placeholder);
--yd-input-placeholder-color: var(--yd-text-color-placeholder);
--yd-input-hover-border-color: var(--yd-border-color-hover);
--yd-input-clear-hover-color: var(--yd-text-color-secondary);
--yd-input-focus-border-color: var(--yd-color-primary);
}
.yd-input {
--yd-input-height: var(--yd-component-size);
position: relative;
font-size: var(--yd-font-size-base);
display: inline-flex;
width: 100%;
line-height: var(--yd-input-height);
box-sizing: border-box;
vertical-align: middle;
&.is-disabled {
cursor: not-allowed;
.yd-input__wrapper {
background-color: var(--yd-disabled-bg-color);
box-shadow: 0 0 0 1px var(--yd-disabled-border-color) inset;
}
.yd-input__inner {
color: var(--yd-disabled-text-color);
-webkit-text-fill-color: var(--yd-disabled-text-color);
cursor: not-allowed;
}
.yd-textarea__inner {
background-color: var(--yd-disabled-bg-color);
box-shadow: 0 0 0 1px var(--yd-disabled-border-color) inset;
color: var(--yd-disabled-text-color);
-webkit-text-fill-color: var(--yd-disabled-text-color);
cursor: not-allowed;
}
}
&.is-prepend {
>.yd-input__wrapper {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.is-append {
>.yd-input__wrapper {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.yd-input--large {
--yd-input-height: var(--yd-component-size-large);
font-size: 14px;
.yd-input__wrapper {
padding: 1px 15px;
.yd-input__inner {
--yd-input-inner-height: calc(var(--yd-input-height, 40px) - 2px);
}
}
}
.yd-input--small {
--yd-input-height: var(--yd-component-size-small);
font-size: 12px;
.yd-input__wrapper {
padding: 1px 7px;
.yd-input__inner {
--yd-input-inner-height: calc(var(--yd-input-height, 24px) - 2px);
}
}
}
.yd-input__prefix, .yd-input__suffix {
display: inline-flex;
white-space: nowrap;
flex-shrink: 0;
flex-wrap: nowrap;
height: 100%;
text-align: center;
color: var(--yd-input-icon-color, var(--yd-text-color-placeholder));
transition: all var(--yd-transition-duration);
}
.yd-input__prefix {
>:first-child {
margin-left: 0px !important;
}
>:last-child {
margin-right: 8px !important;
}
}
.yd-input__suffix {
>:first-child {
margin-left: 8px !important;
}
>:last-child {
margin-right: 0px !important;
}
}
.yd-input__prepend, .yd-input__append {
background-color: var(--yd-fill-color-light);
color: var(--yd-color-info);
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 100%;
border-radius: var(--yd-input-border-radius);
padding: 0 20px;
white-space: nowrap;
}
.yd-input__prepend {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
box-shadow: 1px 0 0 0 var(--yd-input-border-color) inset,0 1px 0 0 var(--yd-input-border-color) inset,0 -1px 0 0 var(--yd-input-border-color) inset;
}
.yd-input__append {
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: 0 1px 0 0 var(--yd-input-border-color) inset,0 -1px 0 0 var(--yd-input-border-color) inset,-1px 0 0 0 var(--yd-input-border-color) inset;
& >.yd-input__wrapper {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
.yd-input--textarea {
position: relative;
display: inline-block;
width: 100%;
vertical-align: bottom;
font-size: var(--yd-font-size-base);
}
.yd-textarea__wrapper {
position: relative;
display: block;
resize: vertical;
padding: 5px 11px;
line-height: 1.5;
box-sizing: border-box;
width: 100%;
font-size: inherit;
font-family: inherit;
color: var(--yd-input-text-color, var(--yd-text-color-regular));
background-color: var(--yd-input-bg-color, var(--yd-fill-color-blank));
background-image: none;
-webkit-appearance: none;
box-shadow: 0 0 0 1px var(--yd-input-border-color, var(--yd-border-color)) inset;
border-radius: var(--yd-input-border-radius, var(--yd-border-radius-base));
transition: var(--yd-transition-box-shadow);
border: none;
&:focus {
outline: none;
box-shadow: 0 0 0 1px var(--yd-input-focus-border-color) inset;
}
&::placeholder {
color: var(--yd-input-placeholder-color);
}
}
.yd-input__wrapper {
display: inline-flex;
flex-grow: 1;
align-items: center;
justify-content: center;
padding: 1px 11px;
background-color: var(--yd-input-bg-color, var(--yd-fill-color-blank));
background-image: none;
border-radius: var(--yd-input-border-radius, var(--yd-border-radius-base));
transition: var(--yd-transition-box-shadow);
box-shadow: 0 0 0 1px var(--yd-input-border-color, var(--yd-border-color)) inset;
&:hover {
box-shadow: 0 0 0 1px var(--yd-input-hover-border-color) inset;
}
&.is-focus {
box-shadow: 0 0 0 1px var(--yd-input-focus-border-color) inset;
}
.yd-input__inner {
--yd-input-inner-height: calc(var(--yd-input-height, 32px) - 2px);
width: 100%;
flex-grow: 1;
-webkit-appearance: none;
color: var(--yd-input-text-color, var(--yd-text-color-regular));
font-size: inherit;
height: var(--yd-input-inner-height);
line-height: var(--yd-input-inner-height);
padding: 0;
outline: none;
border: none;
background: none;
box-sizing: border-box;
&::placeholder {
color: var(--yd-input-placeholder-color);
}
}
.yd-icon {
height: inherit;
line-height: inherit;
display: flex;
justify-content: center;
align-items: center;
transition: all var(--yd-transition-duration);
margin-left: 8px;
}
.yd-input__clear, .yd-input__password {
color: var(--yd-input-icon-color);
font-size: 14px;
cursor: pointer;
&:hover {
color: var(--yd-input-clear-hover-color);
}
}
}