Administrator
Administrator
Published on 2025-04-26 / 14 Visits
0
0

Input实现

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


Comment