Input OTP
Accessible one-time password component.
import { Component } from '@angular/core';
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
import { HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSeparator, HlmInputOtpSlot } from '@spartan-ng/helm/input-otp';
@Component({
selector: 'spartan-input-otp-preview',
imports: [HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSeparator, HlmInputOtpSlot, BrnInputOtp],
template: `
<brn-input-otp hlmInputOtp maxLength="6" inputClass="disabled:cursor-not-allowed">
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="0" />
<hlm-input-otp-slot index="1" />
<hlm-input-otp-slot index="2" />
</div>
<hlm-input-otp-separator />
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="3" />
<hlm-input-otp-slot index="4" />
<hlm-input-otp-slot index="5" />
</div>
</brn-input-otp>
`,
})
export class InputOtpPreview {}
Installation
npx nx g @spartan-ng/cli:ui input-otp
ng g @spartan-ng/cli:ui input-otp
Usage
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
import {
HlmInputOtp
HlmInputOtpGroup
HlmInputOtpSeparator
HlmInputOtpSlot
} from '@spartan-ng/helm/input-otp';
<brn-input-otp hlmInputOtp maxLength="6" inputClass="disabled:cursor-not-allowed">
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="0" />
<hlm-input-otp-slot index="1" />
<hlm-input-otp-slot index="2" />
</div>
<hlm-input-otp-separator />
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="3" />
<hlm-input-otp-slot index="4" />
<hlm-input-otp-slot index="5" />
</div>
</brn-input-otp>
Brain API
BrnInputOtpSlot
Selector: brn-input-otp-slot
Inputs
Prop | Type | Default | Description |
---|---|---|---|
index* (required) | number | - | The index of the slot to render the char or a fake caret |
BrnInputOtp
Selector: brn-input-otp
Inputs
Prop | Type | Default | Description |
---|---|---|---|
hostStyles | string | position: relative; cursor: text; user-select: none; pointer-events: none; | Styles applied to the host element. |
inputStyles | string | position: absolute; inset: 0; width: 100%; height: 100%; display: flex; textAlign: left; opacity: 1; color: transparent; pointerEvents: all; background: transparent; caret-color: transparent; border: 0px solid transparent; outline: transparent solid 0px; box-shadow: none; line-height: 1; letter-spacing: -0.5em; font-family: monospace; font-variant-numeric: tabular-nums; | Styles applied to the input element to make it invisible and clickable. |
containerStyles | string | position: absolute; inset: 0; pointer-events: none; | Styles applied to the container element. |
disabled | boolean | false | Determine if the date picker is disabled. |
maxLength* (required) | number | - | The number of slots. |
inputMode | InputMode | numeric | Virtual keyboard appearance on mobile |
inputClass | ClassValue | - | - |
transformPaste | (pastedText: string, maxLength: number) => string | (text) => text | Defines how the pasted text should be transformed before saving to model/form. Allows pasting text which contains extra characters like spaces, dashes, etc. and are longer than the maxLength. "XXX-XXX": (pastedText) => pastedText.replaceAll('-', '') "XXX XXX": (pastedText) => pastedText.replaceAll(/\s+/g, '') |
value | string | - | The value controlling the input |
Outputs
Prop | Type | Default | Description |
---|---|---|---|
completed | string | - | Emitted when the input is complete, triggered through input or paste. |
valueChanged | string | - | The value controlling the input |
Helm API
HlmInputOtpFakeCaret
Selector: hlm-input-otp-fake-caret
HlmInputOtpGroup
Selector: [hlmInputOtpGroup]
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
HlmInputOtpSeparator
Selector: hlm-input-otp-separator
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | inline-flex | - |
HlmInputOtpSlot
Selector: hlm-input-otp-slot
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
index* (required) | number | - | The index of the slot to render the char or a fake caret |
HlmInputOtp
Selector: brn-input-otp[hlmInputOtp], brn-input-otp[hlm]
Inputs
Prop | Type | Default | Description |
---|---|---|---|
class | ClassValue | - | - |
Examples
Form
Sync the otp to a form by adding formControlName
to brn-input-otp
.
import { afterNextRender, Component, computed, inject, OnDestroy, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
import { HlmButton } from '@spartan-ng/helm/button';
import { HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSlot } from '@spartan-ng/helm/input-otp';
import { HlmToaster } from '@spartan-ng/helm/sonner';
import { toast } from 'ngx-sonner';
@Component({
selector: 'spartan-input-otp-form',
imports: [ReactiveFormsModule, HlmButton, HlmToaster, BrnInputOtp, HlmInputOtp, HlmInputOtpGroup, HlmInputOtpSlot],
template: `
<hlm-toaster />
<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-8">
<brn-input-otp
hlmInputOtp
[maxLength]="maxLength"
inputClass="disabled:cursor-not-allowed"
formControlName="otp"
[transformPaste]="transformPaste"
(completed)="submit()"
>
<div hlmInputOtpGroup>
<hlm-input-otp-slot index="0" />
<hlm-input-otp-slot index="1" />
<hlm-input-otp-slot index="2" />
<hlm-input-otp-slot index="3" />
<hlm-input-otp-slot index="4" />
<hlm-input-otp-slot index="5" />
</div>
</brn-input-otp>
<div class="flex flex-col gap-4">
<button type="submit" hlmBtn [disabled]="form.invalid">Submit</button>
<button type="button" hlmBtn variant="ghost" [disabled]="isResendDisabled()" (click)="resendOtp()">
Resend in {{ countdown() }}s
</button>
</div>
</form>
`,
host: {
class: 'preview flex min-h-[350px] w-full justify-center p-10 items-center',
},
})
export class InputOtpFormExample implements OnDestroy {
private readonly _formBuilder = inject(FormBuilder);
private _intervalId?: NodeJS.Timeout;
public countdown = signal(60);
public isResendDisabled = computed(() => this.countdown() > 0);
public maxLength = 6;
/** Overrides global formatDate */
public transformPaste = (pastedText: string) => pastedText.replaceAll('-', '');
public form = this._formBuilder.group({
otp: [null, [Validators.required, Validators.minLength(this.maxLength), Validators.maxLength(this.maxLength)]],
});
constructor() {
afterNextRender(() => this.startCountdown());
}
submit() {
console.log(this.form.value);
toast('OTP submitted', {
description: `Your OTP ${this.form.value.otp} has been submitted`,
});
}
resendOtp() {
// add your api request here to resend OTP
this.resetCountdown();
}
ngOnDestroy() {
this.stopCountdown();
}
private resetCountdown() {
this.countdown.set(60);
this.startCountdown();
}
private startCountdown() {
this.stopCountdown();
this._intervalId = setInterval(() => {
this.countdown.update((countdown) => Math.max(0, countdown - 1));
if (this.countdown() === 0) {
this.stopCountdown();
}
}, 1000);
}
private stopCountdown() {
if (this._intervalId) {
clearInterval(this._intervalId);
this._intervalId = undefined;
}
}
}