網站搜尋

html - Angular OTP 輸入:多個欄位中值重複的問題


我知道有一個現有的 npm 套件可以用於 OTP 輸入欄位,如這篇 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

問題可能是 *ngFor 在變更檢測週期中會摧毀元素並重新創建它們,當未指定 trackBy 時,這可能是造成這種奇怪行為的原因。另一種理論是名稱和 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 展示