ng-otp-input
Version:
A fully customizable, one-time password input component for the web built with Angular.
405 lines (396 loc) • 19.5 kB
JavaScript
import * as i0 from '@angular/core';
import { forwardRef, Output, Input, Inject, Component, NgModule } from '@angular/core';
import * as i1 from '@angular/forms';
import { NgControl, FormGroup, FormControl, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DOCUMENT, NgIf, NgFor, NgStyle, NgClass } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
class KeyboardUtil {
static ifTab(event) {
return this.ifKey(event, 'Tab');
}
static ifDelete(event) {
return this.ifKey(event, 'Delete;Del');
}
static ifBackspace(event) {
return this.ifKey(event, 'Backspace');
}
static ifRightArrow(event) {
return this.ifKey(event, 'ArrowRight;Right');
}
static ifLeftArrow(event) {
return this.ifKey(event, 'ArrowLeft;Left');
}
static ifSpacebar(event) {
return this.ifKey(event, 'Spacebar; '); //don't remove the space after ; as this will check for space key
}
static ifKey(event, keys) {
let keysToCheck = keys.split(';');
return keysToCheck.some(k => k === event.key);
}
}
class ObjectUtil {
static keys(obj) {
if (!obj)
return [];
return Object.keys(obj);
}
}
class NgOtpInputComponent {
set disabled(isDisabled) {
this.setDisabledState(isDisabled);
}
get inputType() {
return this.config?.isPasswordInput
? 'password'
: this.config?.allowNumbersOnly
? 'tel'
: 'text';
}
get controlKeys() { return ObjectUtil.keys(this.otpForm?.controls); }
;
get formControl() {
return this.formCtrl ?? this.inj?.get(NgControl);
}
constructor(document, inj) {
this.document = document;
this.inj = inj;
this.config = { length: 4 };
this.onBlur = new Subject();
this.onInputChange = new Subject();
this.inputControls = new Array(this.config.length);
this.componentKey = Math.random()
.toString(36)
.substring(2) + new Date().getTime().toString(36);
this.destroy$ = new Subject();
this.activeFocusCount = 0;
this.onChange = () => { };
this.onTouched = () => { };
this._isDisabled = false;
}
ngOnInit() {
this.otpForm = new FormGroup({});
for (let index = 0; index < this.config.length; index++) {
this.otpForm.addControl(this.getControlName(index), new FormControl());
}
this.otpForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
ObjectUtil.keys(this.otpForm.controls).forEach((k) => {
var val = this.otpForm.controls[k].value;
if (val && val.length > 1) {
if (val.length >= this.config.length) {
this.setValue(val);
}
else {
this.rebuildValue();
}
}
});
});
}
setDisabledState(isDisabled) {
this._isDisabled = isDisabled; // Update local state
if (this.otpForm) {
if (isDisabled) {
this.otpForm.disable({ emitEvent: false }); // Disable form group
}
else {
this.otpForm.enable({ emitEvent: false }); // Enable form group
}
}
}
writeValue(value) {
this.currentVal = !this.hasVal(value) ? null : value;
this.setValue(this.currentVal);
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
onFocusIn() {
this.onTouched();
this.activeFocusCount++;
}
onFocusOut() {
setTimeout(() => {
this.activeFocusCount--;
if (this.activeFocusCount === 0) {
this.onTouched();
this.onBlur.next();
}
}, 0);
}
ngAfterViewInit() {
if (!this.config.disableAutoFocus) {
const containerItem = this.document.getElementById(`c_${this.componentKey}`);
if (containerItem) {
const ele = containerItem.getElementsByClassName('otp-input')[0];
if (ele && ele.focus) {
ele.focus();
}
}
}
}
getControlName(idx) {
return `ctrl_${idx}`;
}
onKeyDown($event, inputIdx) {
const prevInputId = this.getBoxId(inputIdx - 1);
const currentInputId = this.getBoxId(inputIdx);
if (KeyboardUtil.ifKey($event, 'Enter')) {
let inp = this.document.getElementById(currentInputId);
const form = inp?.closest('form');
if (form) {
$event.preventDefault();
const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
form.dispatchEvent(submitEvent);
return;
}
}
if (KeyboardUtil.ifSpacebar($event)) {
$event.preventDefault();
return false;
}
if (KeyboardUtil.ifBackspace($event)) {
if (!$event.target.value) {
this.clearInput(prevInputId, inputIdx - 1);
this.setSelected(prevInputId);
}
else {
this.clearInput(currentInputId, inputIdx);
}
this.rebuildValue();
return;
}
if (KeyboardUtil.ifDelete($event)) {
if (!$event.target.value) {
this.clearInput(prevInputId, inputIdx - 1);
this.setSelected(prevInputId);
}
else {
this.clearInput(currentInputId, inputIdx);
}
this.rebuildValue();
return;
}
}
hasVal(val) {
return val != null && val != undefined && (!val?.trim || val.trim() != '');
}
onInput($event, inputIdx) {
let newVal = this.hasVal(this.currentVal) ? `${this.currentVal}${$event.target.value}` : $event.target.value;
if (this.config.allowNumbersOnly && !this.validateNumber(newVal)) {
$event.target.value = null;
$event.stopPropagation();
$event.preventDefault();
this.clearInput(null, inputIdx);
return;
}
if (this.ifValidKeyCode(null, $event.target.value)) {
const nextInputId = this.getBoxId(inputIdx + 1);
this.setSelected(nextInputId);
this.rebuildValue();
}
else {
$event.target.value = null;
let ctrlName = this.getControlName(inputIdx);
this.otpForm.controls[ctrlName]?.setValue(null);
this.rebuildValue();
}
}
onKeyUp($event, inputIdx) {
if (KeyboardUtil.ifTab($event)) {
inputIdx -= 1;
}
const nextInputId = this.getBoxId(inputIdx + 1);
const prevInputId = this.getBoxId(inputIdx - 1);
if (KeyboardUtil.ifRightArrow($event)) {
$event.preventDefault();
this.setSelected(nextInputId);
return;
}
if (KeyboardUtil.ifLeftArrow($event)) {
$event.preventDefault();
this.setSelected(prevInputId);
return;
}
}
validateNumber(val) {
return val && /^[0-9]+$/.test(val);
}
getBoxId(idx) {
return `otp_${idx}_${this.componentKey}`;
}
clearInput(eleId, inputIdx) {
let ctrlName = this.getControlName(inputIdx);
this.otpForm.controls[ctrlName]?.setValue(null);
if (eleId) {
const ele = this.document.getElementById(eleId);
if (ele && ele instanceof HTMLInputElement) {
ele.value = null;
}
}
}
setSelected(eleId) {
this.focusTo(eleId);
const ele = this.document.getElementById(eleId);
if (ele && ele.setSelectionRange) {
setTimeout(() => {
ele.setSelectionRange(0, 1);
}, 0);
}
}
ifValidKeyCode(event, val) {
const inp = val ?? event.key;
if (this.config?.allowNumbersOnly) {
return this.validateNumber(inp);
}
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
return (isMobile ||
(/^[a-zA-Z0-9%*_\-@#$!]$/.test(inp) && inp.length == 1));
}
focusTo(eleId) {
const ele = this.document.getElementById(eleId);
if (ele) {
ele.focus();
}
}
// method to set component value
setValue(value) {
if (this.config.allowNumbersOnly && isNaN(value)) {
return;
}
this.otpForm?.reset();
if (!this.hasVal(value)) {
this.rebuildValue();
return;
}
value = value.toString().replace(/\s/g, ''); // remove whitespace
Array.from(value).forEach((c, idx) => {
if (this.otpForm.get(this.getControlName(idx))) {
this.otpForm.get(this.getControlName(idx)).setValue(c);
}
});
if (!this.config.disableAutoFocus) {
setTimeout(() => {
const containerItem = this.document.getElementById(`c_${this.componentKey}`);
var indexOfElementToFocus = value.length < this.config.length ? value.length : (this.config.length - 1);
let ele = containerItem.getElementsByClassName('otp-input')[indexOfElementToFocus];
if (ele && ele.focus) {
setTimeout(() => {
ele.focus();
}, 1);
}
}, 0);
}
this.rebuildValue();
}
rebuildValue() {
let val = null;
ObjectUtil.keys(this.otpForm.controls).forEach(k => {
let ctrlVal = this.otpForm.controls[k].value;
if (ctrlVal) {
let isLengthExceed = ctrlVal.length > 1;
let isCaseTransformEnabled = !this.config.allowNumbersOnly && this.config.letterCase && (this.config.letterCase.toLocaleLowerCase() == 'upper' || this.config.letterCase.toLocaleLowerCase() == 'lower');
ctrlVal = ctrlVal[0];
let transformedVal = isCaseTransformEnabled ? this.config.letterCase.toLocaleLowerCase() == 'upper' ? ctrlVal.toUpperCase() : ctrlVal.toLowerCase() : ctrlVal;
if (isCaseTransformEnabled && transformedVal == ctrlVal) {
isCaseTransformEnabled = false;
}
else {
ctrlVal = transformedVal;
}
if (val == null) {
val = ctrlVal;
}
else {
val += ctrlVal;
}
if (isLengthExceed || isCaseTransformEnabled) {
this.otpForm.controls[k].setValue(ctrlVal);
}
}
});
if (this.currentVal != val) {
this.currentVal = val;
this.onChange(val);
if (this.formCtrl?.setValue) {
this.formCtrl.setValue(val);
}
this.onInputChange.next(val);
}
}
handlePaste(e) {
// Get pasted data via clipboard API
let clipboardData = e.clipboardData || window['clipboardData'];
if (clipboardData) {
var pastedData = clipboardData.getData('Text');
}
e.stopPropagation();
e.preventDefault();
if (!pastedData || (this.config.allowNumbersOnly && !this.validateNumber(pastedData))) {
return;
}
this.setValue(pastedData);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgOtpInputComponent, deps: [{ token: DOCUMENT }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: NgOtpInputComponent, isStandalone: true, selector: "ng-otp-input, ngx-otp-input", inputs: { config: "config", formCtrl: "formCtrl", disabled: "disabled" }, outputs: { onBlur: "onBlur", onInputChange: "onInputChange" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef((() => NgOtpInputComponent)),
multi: true,
},
], ngImport: i0, template: "<div class=\"ng-otp-input-wrapper wrapper {{config.containerClass}}\" id=\"c_{{componentKey}}\" *ngIf=\"otpForm?.controls\"\r\n [ngStyle]=\"config.containerStyles\" tabindex=\"0\" \r\n (focusin)=\"onFocusIn()\" \r\n (focusout)=\"onFocusOut()\">\r\n <div class=\"n-o-c\">\r\n <ng-container *ngFor=\"let item of controlKeys;let i=index;let last=last\">\r\n <input (paste)=\"handlePaste($event)\" [pattern]=\"config.allowNumbersOnly ? '\\\\d*' : ''\" [type]=\"inputType\" [placeholder]=\"config?.placeholder || ''\"\r\n [ngStyle]=\"config.inputStyles\" \r\n class=\"otp-input {{config.inputClass}}\" autocomplete=\"one-time-code\" \r\n [formControl]=\"otpForm.controls[item]\" #inp [id]=\"getBoxId(i)\" \r\n (keyup)=\"onKeyUp($event,i)\" (input)=\"onInput($event,i)\" (keydown)=\"onKeyDown($event,i)\" [ngClass]=\"{'error-input': (config?.showError && formControl?.invalid && (formControl?.dirty || formControl?.touched))}\">\r\n <span *ngIf=\"config.separator && !last\">\r\n {{config.separator}}\r\n </span>\r\n </ng-container>\r\n </div> \r\n</div>", styles: [".otp-input{width:50px;height:50px;border-radius:4px;border:solid 1px #c5c5c5;text-align:center;font-size:32px}.ng-otp-input-wrapper .otp-input{margin:0 .51rem}.ng-otp-input-wrapper .otp-input:first-child{margin-left:0}.ng-otp-input-wrapper .otp-input:last-child{margin-right:0}.n-o-c{display:flex;align-items:center}.error-input{border-color:red}@media screen and (max-width: 767px){.otp-input{width:40px;font-size:24px;height:40px}}@media screen and (max-width: 420px){.otp-input{width:30px;font-size:18px;height:30px}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgOtpInputComponent, decorators: [{
type: Component,
args: [{ selector: 'ng-otp-input, ngx-otp-input', imports: [ReactiveFormsModule, NgIf, NgFor, NgStyle, NgClass], providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef((() => NgOtpInputComponent)),
multi: true,
},
], template: "<div class=\"ng-otp-input-wrapper wrapper {{config.containerClass}}\" id=\"c_{{componentKey}}\" *ngIf=\"otpForm?.controls\"\r\n [ngStyle]=\"config.containerStyles\" tabindex=\"0\" \r\n (focusin)=\"onFocusIn()\" \r\n (focusout)=\"onFocusOut()\">\r\n <div class=\"n-o-c\">\r\n <ng-container *ngFor=\"let item of controlKeys;let i=index;let last=last\">\r\n <input (paste)=\"handlePaste($event)\" [pattern]=\"config.allowNumbersOnly ? '\\\\d*' : ''\" [type]=\"inputType\" [placeholder]=\"config?.placeholder || ''\"\r\n [ngStyle]=\"config.inputStyles\" \r\n class=\"otp-input {{config.inputClass}}\" autocomplete=\"one-time-code\" \r\n [formControl]=\"otpForm.controls[item]\" #inp [id]=\"getBoxId(i)\" \r\n (keyup)=\"onKeyUp($event,i)\" (input)=\"onInput($event,i)\" (keydown)=\"onKeyDown($event,i)\" [ngClass]=\"{'error-input': (config?.showError && formControl?.invalid && (formControl?.dirty || formControl?.touched))}\">\r\n <span *ngIf=\"config.separator && !last\">\r\n {{config.separator}}\r\n </span>\r\n </ng-container>\r\n </div> \r\n</div>", styles: [".otp-input{width:50px;height:50px;border-radius:4px;border:solid 1px #c5c5c5;text-align:center;font-size:32px}.ng-otp-input-wrapper .otp-input{margin:0 .51rem}.ng-otp-input-wrapper .otp-input:first-child{margin-left:0}.ng-otp-input-wrapper .otp-input:last-child{margin-right:0}.n-o-c{display:flex;align-items:center}.error-input{border-color:red}@media screen and (max-width: 767px){.otp-input{width:40px;font-size:24px;height:40px}}@media screen and (max-width: 420px){.otp-input{width:30px;font-size:18px;height:30px}}\n"] }]
}], ctorParameters: () => [{ type: Document, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: i0.Injector }], propDecorators: { config: [{
type: Input
}], formCtrl: [{
type: Input
}], disabled: [{
type: Input
}], onBlur: [{
type: Output
}], onInputChange: [{
type: Output
}] } });
class NgOtpInputModule {
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgOtpInputModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: NgOtpInputModule, imports: [NgOtpInputComponent], exports: [NgOtpInputComponent] }); }
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgOtpInputModule, imports: [NgOtpInputComponent] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgOtpInputModule, decorators: [{
type: NgModule,
args: [{
imports: [
NgOtpInputComponent
],
exports: [NgOtpInputComponent]
}]
}] });
class Config {
}
/*
* Public API Surface of ng-otp-input
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgOtpInputComponent, Config as NgOtpInputConfig, NgOtpInputModule };
//# sourceMappingURL=ng-otp-input.mjs.map