UNPKG

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
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