html - Angular OTP 輸入:多個欄位中值重複的問題
我知道有一個現有的 npm 套件可以用於 OTP 輸入欄位,如這篇 Stack Overflow 文章所提到的,但我需要從零開始構建這個元件,以便更好地自訂以及學習。
我正在開發一個 Angular 8 的 OTP 輸入元件,使用者在其中輸入 6 位數的代碼。OTP 輸入欄位是使用 *ngFor 動態生成的,我使用 [value] 綁定和事件監聽器來更新元件狀態。然而,我遇到了兩個問題:
- 數值重複:
- 當我在第一個輸入框(索引 0)中輸入一個數字時,相同的數字會出現在第二個輸入框(索引 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);
}
} } 預期行為:
- 每個數字應僅輸入在選定的輸入欄位中,不影響其他欄位。
- 按下退格鍵應該先清除當前欄位,然後將焦點移到前一個輸入欄位。
我嘗試過的:
- 將 [value] 替換為 [(ngModel)]
- 已檢查意外變更偵測更新。
- 我添加了控制台日誌,並確認更新按預期的順序發生。
- 在 onInput 函數中手動處理狀態更新
問題
- 為什麼在輸入時,下一個輸入欄位的值會被重複?
- 我如何防止退格鍵在當前欄位之前刪除前一個欄位的值?
答案#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);
