网站搜索

html - Angular OTP 输入:多个字段中值重复问题


我知道有一个现成的用于 OTP 输入字段的 npm 包,如这个 Stack Overflow 帖子所提到的,但我需要从头构建这个组件,以便更好地进行自定义和学习。

我正在开发一个 Angular 8 OTP 输入组件,用户在其中输入 6 位数的代码。OTP 输入字段是使用 *ngFor 动态生成的,并且我使用 [value] 绑定和事件监听器来更新组件状态。然而,我遇到了两个问题:

  1. 值重复:
  • 当我在第一个输入框(索引 0)中输入一个数字时,相同的数字会出现在第二个输入框(索引 1)中。
  1. 退格键行为:
  • 当我点击第二个输入框并按删除键时,第一个输入框的值被删除,而不是第二个。

我的组件代码 HTML(otp-input.component.html):

<div class="otp-container">
<input
*ngFor="let digit of otpArray; let i = index"
type="text"
class="otp-input"
maxlength="1"
[value]="otpArray[i]"
(input)="onInput($event, i)"
(keydown)="onKeyDown($event, i)"
#otpInput
/>
</div>

TypeScript (otp-input.component.ts):

import { Component, EventEmitter, Output, ViewChildren, ElementRef, QueryList } from 
'@angular/core';

@Component({
selector: 'app-otp-input',
templateUrl: './otp-input.component.html',
styleUrls: ['./otp-input.component.css']
})
export class OtpInputComponent {
otpLength = 6;
otpArray: string[] = new Array(this.otpLength).fill('');

@Output() otpCompleted = new EventEmitter<string>();
@ViewChildren('otpInput') otpInputs!: QueryList<ElementRef>;
 
onInput(event: Event, index: number): void {
const inputElement = event.target as HTMLInputElement;
const value = inputElement.value;

// Ensure the correct value is assigned to the correct index
this.otpArray[index] = value;

console.log(`User entered value "${this.otpArray[index]}" at index ${index}`);

const inputEvent = event as InputEvent;
if (inputEvent.inputType === 'deleteContentBackward') {
  console.log('User pressed delete on input ' + index);
  return;
}

 if (value && index < this.otpLength - 1) {
  this.otpInputs.toArray()[index + 1].nativeElement.focus();
}

this.checkOtpCompletion();
}

onKeyDown(event: KeyboardEvent, index: number): void {
  if (event.key === 'Backspace') {
   if (this.otpArray[index]) {
    this.otpArray[index] = ''; // Clear current input
  } else if (index > 0) {
    console.log('Backspace pressed, moving to previous index:', index);
    this.otpInputs.toArray()[index - 1].nativeElement.focus();
  }
}

}

checkOtpCompletion(): void {
const otpValue: string = this.otpArray.join('');
if (otpValue.length === this.otpLength) {
  this.otpCompleted.emit(otpValue);
}

} } 预期行为:

  1. 每个数字应只输入在选定的输入字段中,不影响其他字段。
  2. 按下退格键应先清除当前字段,然后将焦点移到前一个输入框。

我尝试过的:

  1. 将 [value] 替换为 [(ngModel)
  2. 已检查意外的更改检测更新。
  3. 我添加了控制台日志,并且验证了更新是按预期顺序发生的。
  4. 在 onInput 函数中手动处理状态更新

问题

  1. 为什么在输入时,下一个输入框的值会被重复?
  2. 我怎样才能防止退格键在删除当前字段之前删除上一个字段的值?

答案#1

问题可能在于,当未指定 trackBy 时,*ngFor 会在变化检测周期中销毁元素并重新创建它们,这可能是这种奇怪行为的原因。另一种理论是,name 和 id 帮助确定更新哪个元素,如果未指定,可能会遇到一些奇怪的错误,例如输入被错误更新。

ngFor :

<div class="otp-container">
<input
  *ngFor="let digit of otpArray; let i = index; trackBy:trackByIndex"
  type="text"
  class="otp-input"
  maxlength="1"
  [id]="'otp-' + i"
  [name]="'otp-' + i"
  [value]="otpArray[i]"
  (input)="onInput($event, i)"
  (keydown)="onKeyDown($event, i)"
  #otpInput
/>
</div>

TS :

trackByIndex = (index: number, obj: object): string => {   return index; };

@for:

  <div class="otp-container">
  @for(digit of otpArray; let i = $index;track i) {
    <input
      type="text"
      class="otp-input"
      maxlength="1"
      [id]="'otp-' + i"
      [name]="'otp-' + i"
      [value]="otpArray[i]"
      (input)="onInput($event, i)"
      (keydown)="onKeyDown($event, i)"
      #otpInput
    />
  }
</div>

完整代码:

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import {
  EventEmitter,
  Output,
  ViewChildren,
  ElementRef,
  QueryList,
} from '@angular/core';

@Component({
  selector: 'app-otp-input',
  template: `
  <div class="otp-container">
  @for(digit of otpArray; let i = $index;track i) {
    <input
      type="text"
      class="otp-input"
      maxlength="1"
      [id]="'otp-' + i"
      [name]="'otp-' + i"
      [value]="otpArray[i]"
      (input)="onInput($event, i)"
      (keydown)="onKeyDown($event, i)"
      #otpInput
    />
  }
</div>
  `,
})
export class OtpInputComponent {
  otpLength = 6;
  otpArray: string[] = new Array(this.otpLength).fill('');

  @Output() otpCompleted = new EventEmitter<string>();
  @ViewChildren('otpInput') otpInputs!: QueryList<ElementRef>;

  onInput(event: Event, index: number): void {
    const inputElement = event.target as HTMLInputElement;
    const value = inputElement.value;

    // Ensure the correct value is assigned to the correct index
    this.otpArray[index] = value;

    console.log(
      `User entered value "${this.otpArray[index]}" at index ${index}`
    );

    const inputEvent = event as InputEvent;
    if (inputEvent.inputType === 'deleteContentBackward') {
      console.log('User pressed delete on input ' + index);
      return;
    }

    if (value && index < this.otpLength - 1) {
      this.otpInputs.toArray()[index + 1].nativeElement.focus();
    }

    this.checkOtpCompletion();
  }
  checkOtpCompletion(): void {
    const otpValue: string = this.otpArray.join('');
    if (otpValue.length === this.otpLength) {
      this.otpCompleted.emit(otpValue);
    }
  }

  onKeyDown(event: KeyboardEvent, index: number): void {
    if (event.key === 'Backspace') {
      if (this.otpArray[index]) {
        this.otpArray[index] = ''; // Clear current input
      } else if (index > 0) {
        console.log('Backspace pressed, moving to previous index:', index);
        this.otpInputs.toArray()[index - 1].nativeElement.focus();
      }
    }
  }
}

@Component({
  selector: 'app-root',
  template: `
    <app-otp-input/>
  `,
  imports: [OtpInputComponent],
})
export class App {
  name = 'Angular';
}

bootstrapApplication(App);

Stackblitz 演示