OTP Input
A one-time-code / PIN field — Kendo's OTP Input, PrimeNG's InputOtp: length single-character cells in one labelled role="group", each cell named "Character n of m", with a single Tab stop on the caret cell. Typing advances, Backspace steps back, Delete closes the gap and a paste — or the SMS autofill through autocomplete="one-time-code" — fills every cell. The value is always the contiguous characters entered, so value.length === length means complete.
Works standalone via [(value)], with Signal Forms via [formField], and with reactive/template forms via formControl/ngModel.
Getting started
Type, paste or let the browser autofill the code. Arrows (RTL-mirrored) and Home/End move between cells, Backspace clears and steps back; (completed) fires once every cell holds a character.
Value: · Waiting for the code
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeOtpInput } from '@oge-ui/inputs';
import type { OgeOtpCompletedEvent } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeOtpInput],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One labelled role="group"; one Tab stop (the caret cell). Typing
fills and advances, Backspace clears and steps back, arrows move, and a
paste — or the SMS autofill (autocomplete="one-time-code") — fills
every cell. (completed) fires once all cells hold a character. -->
<oge-otp-input
label="Verification code"
hint="We sent a 6-digit code to your phone."
[(value)]="code"
(completed)="onCompleted($event)"
/>
<p>Value: <code>{{ code() }}</code> · {{ status() }}</p>
`,
})
export class Demo {
protected readonly code = signal('');
protected readonly status = signal('Waiting for the code');
protected onCompleted(event: OgeOtpCompletedEvent): void {
this.status.set('Verifying ' + event.value);
}
}Character types and groups
type accepts 'numeric' (script and full-width digits fold to ASCII), 'alphanumeric' or 'alphabetic'; rejected characters never land. letterCase upper- or lower-cases letters and groupSize draws a decorative separator.
Value:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeOtpInput } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeOtpInput],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- type: 'numeric' (digits; Arabic-Indic and full-width digits are
folded to ASCII) | 'alphanumeric' | 'alphabetic'. letterCase upper-cases
letters; groupSize draws a decorative separator between groups. -->
<oge-otp-input
label="Licence key"
type="alphanumeric"
letterCase="upper"
[length]="8"
[groupSize]="4"
[(value)]="key"
/>
`,
})
export class Demo {
protected readonly key = signal('');
}Masked PIN
masked hides the characters like a password field; placeholder marks the empty cells and size picks the cell scale.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeOtpInput } from '@oge-ui/inputs';
@Component({
selector: 'demo-root',
imports: [OgeOtpInput],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- masked hides the characters like a password field; placeholder
marks the empty cells; size picks the cell scale. -->
<oge-otp-input
label="PIN"
[length]="4"
[masked]="true"
placeholder="•"
size="lg"
[(value)]="pin"
/>
`,
})
export class Demo {
protected readonly pin = signal('');
}Inside a form
A FormValueControl and a ControlValueAccessor: formControl, ngModel and [formField] all bind it. Because the value is the contiguous characters entered, a length validator means "complete".
Valid: false
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeOtpInput } from '@oge-ui/inputs';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
@Component({
selector: 'demo-root',
imports: [OgeOtpInput, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- FormValueControl + ControlValueAccessor. The value is always the
contiguous characters entered, so a length validator means "complete". -->
<oge-otp-input label="Code" [length]="6" [formControl]="otp" />
<p>Valid: <code>{{ otp.valid }}</code></p>
`,
})
export class Demo {
protected readonly otp = new FormControl('', [
Validators.required,
Validators.minLength(6),
]);
}