UNPKG

otp-angular

Version:

**Otp Angular** is a lightweight, highly customizable, and dependency-free OTP (One-Time Password) input component built for Angular 20+ applications. It offers seamless integration, flexible configuration, and a polished user experience for OTP or verifi

287 lines (279 loc) 20.7 kB
import * as i0 from '@angular/core'; import { inject, HostListener, Directive, ElementRef, DOCUMENT, Input, model, signal, output, computed, linkedSignal, effect, untracked, afterNextRender, Component } from '@angular/core'; import * as i1 from '@angular/forms'; import { NgControl, FormsModule } from '@angular/forms'; class AlphaNumeric { control = inject(NgControl); onInputChange(event) { const input = event.target; const cleanValue = input.value.replace(/[^a-zA-Z0-9]/g, ''); if (input.value !== cleanValue) { input.value = cleanValue; this.control.control?.setValue(cleanValue); // Keep form state in sync } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AlphaNumeric, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: AlphaNumeric, isStandalone: true, selector: "[AlphaNumeric]", host: { listeners: { "input": "onInputChange($event)" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: AlphaNumeric, decorators: [{ type: Directive, args: [{ selector: '[AlphaNumeric]' }] }], propDecorators: { onInputChange: [{ type: HostListener, args: ['input', ['$event']] }] } }); class DefaultOptions { length = 4; numbersOnly = false; autoFocus = false; isPassword = false; showError = false; showCaps = true; containerClass = ''; containerStyles = {}; inputClass = ''; inputStyles = {}; placeholder = ''; separator = ''; resend = 0; resendLabel = 'RESEND VERIFICATION CODE'; resendContainerClass = ''; resendLabelClass = ''; resendTimerClass = ''; constructor(data) { if (!data) return; Object.keys(data).forEach((key) => { typeof (data[key] !== undefined) && typeof (data[key] !== null) && (this[key] = data[key]); }); } } class Disabled { otpDisabled = false; el = inject(ElementRef); document = inject(DOCUMENT); constructor() { this.document.addEventListener("mousemove", (e) => { this.otpDisabledInit(this.otpDisabled); }); } otpDisabledInit(otpDisabled) { if (this.el.nativeElement.nodeName !== 'INPUT') { if (otpDisabled) { this.el.nativeElement?.classList.add('disabled'); this.el.nativeElement?.setAttribute('disabled', true); this.el.nativeElement.style.pointerEvents = 'none'; this.el.nativeElement.setAttribute('disabled', true); } else { this.el.nativeElement?.classList.remove('disabled'); this.el.nativeElement.style.pointerEvents = 'auto'; this.el.nativeElement?.removeAttribute('disabled'); this.el.nativeElement.removeAttribute('disabled'); } } else { if (otpDisabled) { this.el.nativeElement.setAttribute('readonly', true); this.el.nativeElement.setAttribute('disabled', true); } else { this.el.nativeElement.removeAttribute('readonly'); this.el.nativeElement.removeAttribute('disabled'); } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: Disabled, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: Disabled, isStandalone: true, selector: "[otpDisabled]", inputs: { otpDisabled: "otpDisabled" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: Disabled, decorators: [{ type: Directive, args: [{ selector: '[otpDisabled]' }] }], ctorParameters: () => [], propDecorators: { otpDisabled: [{ type: Input, args: ['otpDisabled'] }] } }); class OtpAngular { document = inject(DOCUMENT); config = model(); disabled = signal(false); onInputChange = output(); defaultConfig = computed(() => { return { ...new DefaultOptions(), ...this.config() }; }); myValue = signal([]); inputStyleArray = computed(() => { const _config = this.config(); if (Array.isArray(_config?.inputStyles)) { return _config?.inputStyles; } else { return [{ ..._config?.inputStyles }]; } }); // Resend countdown = linkedSignal(() => { return Number(this.defaultConfig()?.resend); }); isResendAllowed = signal(false); onResendAvailable = output(); timer; constructor() { effect(() => { const _countdown = this.countdown(); if (_countdown <= 0) { clearInterval(this.timer); this.isResendAllowed.set(true); } }); effect(() => { const _isResendAllowed = this.isResendAllowed(); if (!_isResendAllowed) { untracked(() => { this.timer = setInterval(() => { this.countdown.update(value => value - 1); }, 1000); }); } }); afterNextRender(() => { const _config = this.defaultConfig(); if (_config.autoFocus) { const _firstID = this.document.getElementById('otp-input-1'); _firstID.focus(); } }); } setValue(value) { this.myValue.set(value.toString().split('')); const _config = this.defaultConfig(); this.onInputChange.emit(_config.numbersOnly ? Number(value) : value.toString().trim()); } keydown(e, index) { const _config = this.defaultConfig(); const prev = this.document.getElementById(`otp-input-${index}`); const current = this.document.getElementById(`otp-input-${index + 1}`); const next = this.document.getElementById(`otp-input-${index + 2}`); if (e.key === 'Backspace') { if (current.value.trim() === '') { index !== 0 && prev.focus(), setTimeout(() => prev.select(), 0); } else { current.value = ''; } return; } if (e.key === 'ArrowLeft' && index !== 0) { prev.focus(); setTimeout(() => prev.select(), 0); return; } if (e.key === 'ArrowRight' && index !== _config.length - 1) { next.focus(); setTimeout(() => next.select(), 0); return; } const input = e.target; if (e.key.length === 1) { const isAlphanumeric = /^[a-zA-Z0-9]*$/.test(e.key); isAlphanumeric && (input.value = ''); } } onInput(e, index) { const _config = this.defaultConfig(); const input = e.target; if (input.value.length > 1) { if (input.value.length >= _config.length) { const digits = input.value.toString().split('').map(Number); for (let i = 0; i < _config.length; i++) { const element = digits[i]; const srcElement = this.document.getElementById(`otp-input-${i + 1}`); srcElement.value = element.toString(); } } else { const maxLength = input.value.length + index; const digits = input.value.toString().padStart(Number(maxLength), '0').split('').map(Number); a: for (let i = index; i < _config.length; i++) { const element = digits[i]; if (element !== undefined) { const srcElement = this.document.getElementById(`otp-input-${i + 1}`); srcElement.value = element.toString(); } else { break a; } } } } const current = this.document.getElementById(`otp-input-${index + 1}`); const next = this.document.getElementById(`otp-input-${index + 2}`); if (input.value.trim() === '') return; index === _config.length - 1 ? current.blur() : next.focus(); if (_config.showCaps && e.target) { input.value = input.value.toUpperCase(); } this.emit(); } blur(e, index) { const _config = this.defaultConfig(); if (_config.showError) { const input = e.target; if (input.value.trim() === '') { input.classList.add('error'); } else { input.classList.remove('error'); } } } emit() { const _config = this.defaultConfig(); let output = ''; for (let index = 0; index < _config.length; index++) { const input = this.document.getElementById(`otp-input-${index + 1}`); output += input.value; } const _output = _config.numbersOnly ? Number(output) : output.trim(); if (_output.toString().trim() === '') { this.onInputChange.emit(null); } else { this.onInputChange.emit(_output); } } // Resend resend() { this.onResendAvailable.emit(true); } reset() { const _config = this.defaultConfig(); console.log("_config: ", _config); if (_config && _config.resend) { this.countdown.set(_config.resend); this.isResendAllowed.set(false); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: OtpAngular, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: OtpAngular, isStandalone: true, selector: "otp-angular", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { config: "configChange", onInputChange: "onInputChange", onResendAvailable: "onResendAvailable" }, ngImport: i0, template: "@let _config = defaultConfig();\n@let _myValue = myValue();\n@let _disabled = disabled();\n\n@if(_config) {\n@let _containerClass = _config.containerClass;\n@let containerClass = typeof _containerClass === 'string' ? _containerClass.split(' ')[0] : _containerClass.join(' ');\n@let _inputClass = _config.inputClass;\n@let inputClass = typeof _inputClass === 'string' ? _inputClass.split(' ')[0] : _inputClass;\n@let _containerStyles = _config.containerStyles;\n\n@let _inputStyles = inputStyleArray();\n\n@let _resend = _config.resend;\n@let _resendLabel = _config.resendLabel;\n@let _resendContainerClass = _config.resendContainerClass;\n@let _resendLabelClass = _config.resendLabelClass;\n@let _resendTimerClass = _config.resendTimerClass; \n\n<div class=\"__otpContainer {{containerClass}}\" [style]=\"_containerStyles\">\n @for (item of [].constructor(_config.length); track $index) {\n <div class=\"otp-input-wrapper\">\n <input [id]=\"`otp-input-${$index+1}`\" [max]=\"1\" AlphaNumeric autocomplete=\"off\" name=\"dummy\"\n [placeholder]=\"_config.placeholder\"\n [otpDisabled]=\"_disabled\" [class]=\"inputClass[$index]\" [style]=\"_inputStyles[$index]\"\n [attr.inputmode]=\"_config.numbersOnly ? 'numeric' : null\" [attr.pattern]=\"_config.numbersOnly ? '[0-9]*' : null\"\n [type]=\"_config.isPassword ? `password` : `text`\"\n (keydown)=\"keydown($event, $index)\" (blur)=\"blur($event, $index)\" (input)=\"onInput($event, $index)\"\n [(ngModel)]=\"_myValue[$index]\" />\n @if (_config.separator && $index < _config.length - 1) {\n <span class=\"otp-separator\">{{_config.separator.toString().split('')[0]}}</span>\n }\n </div>\n }\n @if(_resend && _resend > 0) {\n <div class=\"otp-resend {{_resendContainerClass}}\">\n @if (isResendAllowed()) {\n <div aria-label=\"resend\" type=\"button\" class=\"resendTxt {{_resendLabelClass}}\" (click)=\"resend()\">{{_resendLabel}}</div>\n } @else {\n <div class=\"resendTxt {{_resendTimerClass}}\" >{{_resendLabel}} IN {{countdown()}}s</div>\n }\n </div>\n }\n</div>\n}", styles: [".__otpContainer{display:flex;justify-content:center;gap:10px;margin:20px 0;flex-wrap:wrap}.__otpContainer .otp-input-wrapper{display:flex;align-items:center;gap:6px}.__otpContainer .otp-input-wrapper input{width:40px;height:50px;text-align:center;font-size:20px;border:2px solid #ccc;border-radius:8px;transition:border-color .2s ease,box-shadow .2s ease;outline:none}.__otpContainer .otp-input-wrapper input.error{border-color:#e74c3c;background-color:#ffe6e6;color:#e74c3c;animation:shake .2s ease-in-out 0s 2}.__otpContainer .otp-input-wrapper input::placeholder{color:#bbb;font-size:18px}.__otpContainer .otp-input-wrapper input:focus{border-color:#f60;box-shadow:0 0 5px #ff660080}.__otpContainer .otp-input-wrapper .otp-separator{font-size:24px;font-weight:700;color:#666;margin:0 2px;display:inline-block;vertical-align:middle}@media screen and (max-width: 400px){.__otpContainer .otp-input-wrapper input{width:36px;height:45px;font-size:18px}}.__otpContainer .otp-resend{width:100%;display:flex;justify-content:center;margin-top:16px;font-size:14px}.__otpContainer .otp-resend .resendTxt{text-transform:uppercase;cursor:pointer;color:#f60;font-weight:500;transition:color .2s ease,opacity .2s ease}.__otpContainer .otp-resend .resendTxt:hover{color:#cc5200;text-decoration:underline}.__otpContainer .otp-resend .resendTxt:active{opacity:.7}.__otpContainer .otp-resend .resendTxt:not([aria-label]){cursor:default;color:#999;font-weight:400;text-decoration:none}.__otpContainer .otp-resend .resendTxt:not([aria-label]):hover,.__otpContainer .otp-resend .resendTxt:not([aria-label]):active{color:#999;text-decoration:none;opacity:1}@media screen and (max-width: 480px){.__otpContainer .otp-resend{font-size:13px}.__otpContainer .otp-resend .resendTxt{text-align:center;padding:0 10px}}@media screen and (max-width: 360px){.__otpContainer .otp-resend{font-size:12px}.__otpContainer .otp-resend .resendTxt{padding:0 8px}}@keyframes shake{0%{transform:translate(0)}25%{transform:translate(-5px)}50%{transform:translate(5px)}75%{transform:translate(-5px)}to{transform:translate(0)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { 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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: AlphaNumeric, selector: "[AlphaNumeric]" }, { kind: "directive", type: Disabled, selector: "[otpDisabled]", inputs: ["otpDisabled"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: OtpAngular, decorators: [{ type: Component, args: [{ selector: 'otp-angular', imports: [FormsModule, AlphaNumeric, Disabled], template: "@let _config = defaultConfig();\n@let _myValue = myValue();\n@let _disabled = disabled();\n\n@if(_config) {\n@let _containerClass = _config.containerClass;\n@let containerClass = typeof _containerClass === 'string' ? _containerClass.split(' ')[0] : _containerClass.join(' ');\n@let _inputClass = _config.inputClass;\n@let inputClass = typeof _inputClass === 'string' ? _inputClass.split(' ')[0] : _inputClass;\n@let _containerStyles = _config.containerStyles;\n\n@let _inputStyles = inputStyleArray();\n\n@let _resend = _config.resend;\n@let _resendLabel = _config.resendLabel;\n@let _resendContainerClass = _config.resendContainerClass;\n@let _resendLabelClass = _config.resendLabelClass;\n@let _resendTimerClass = _config.resendTimerClass; \n\n<div class=\"__otpContainer {{containerClass}}\" [style]=\"_containerStyles\">\n @for (item of [].constructor(_config.length); track $index) {\n <div class=\"otp-input-wrapper\">\n <input [id]=\"`otp-input-${$index+1}`\" [max]=\"1\" AlphaNumeric autocomplete=\"off\" name=\"dummy\"\n [placeholder]=\"_config.placeholder\"\n [otpDisabled]=\"_disabled\" [class]=\"inputClass[$index]\" [style]=\"_inputStyles[$index]\"\n [attr.inputmode]=\"_config.numbersOnly ? 'numeric' : null\" [attr.pattern]=\"_config.numbersOnly ? '[0-9]*' : null\"\n [type]=\"_config.isPassword ? `password` : `text`\"\n (keydown)=\"keydown($event, $index)\" (blur)=\"blur($event, $index)\" (input)=\"onInput($event, $index)\"\n [(ngModel)]=\"_myValue[$index]\" />\n @if (_config.separator && $index < _config.length - 1) {\n <span class=\"otp-separator\">{{_config.separator.toString().split('')[0]}}</span>\n }\n </div>\n }\n @if(_resend && _resend > 0) {\n <div class=\"otp-resend {{_resendContainerClass}}\">\n @if (isResendAllowed()) {\n <div aria-label=\"resend\" type=\"button\" class=\"resendTxt {{_resendLabelClass}}\" (click)=\"resend()\">{{_resendLabel}}</div>\n } @else {\n <div class=\"resendTxt {{_resendTimerClass}}\" >{{_resendLabel}} IN {{countdown()}}s</div>\n }\n </div>\n }\n</div>\n}", styles: [".__otpContainer{display:flex;justify-content:center;gap:10px;margin:20px 0;flex-wrap:wrap}.__otpContainer .otp-input-wrapper{display:flex;align-items:center;gap:6px}.__otpContainer .otp-input-wrapper input{width:40px;height:50px;text-align:center;font-size:20px;border:2px solid #ccc;border-radius:8px;transition:border-color .2s ease,box-shadow .2s ease;outline:none}.__otpContainer .otp-input-wrapper input.error{border-color:#e74c3c;background-color:#ffe6e6;color:#e74c3c;animation:shake .2s ease-in-out 0s 2}.__otpContainer .otp-input-wrapper input::placeholder{color:#bbb;font-size:18px}.__otpContainer .otp-input-wrapper input:focus{border-color:#f60;box-shadow:0 0 5px #ff660080}.__otpContainer .otp-input-wrapper .otp-separator{font-size:24px;font-weight:700;color:#666;margin:0 2px;display:inline-block;vertical-align:middle}@media screen and (max-width: 400px){.__otpContainer .otp-input-wrapper input{width:36px;height:45px;font-size:18px}}.__otpContainer .otp-resend{width:100%;display:flex;justify-content:center;margin-top:16px;font-size:14px}.__otpContainer .otp-resend .resendTxt{text-transform:uppercase;cursor:pointer;color:#f60;font-weight:500;transition:color .2s ease,opacity .2s ease}.__otpContainer .otp-resend .resendTxt:hover{color:#cc5200;text-decoration:underline}.__otpContainer .otp-resend .resendTxt:active{opacity:.7}.__otpContainer .otp-resend .resendTxt:not([aria-label]){cursor:default;color:#999;font-weight:400;text-decoration:none}.__otpContainer .otp-resend .resendTxt:not([aria-label]):hover,.__otpContainer .otp-resend .resendTxt:not([aria-label]):active{color:#999;text-decoration:none;opacity:1}@media screen and (max-width: 480px){.__otpContainer .otp-resend{font-size:13px}.__otpContainer .otp-resend .resendTxt{text-align:center;padding:0 10px}}@media screen and (max-width: 360px){.__otpContainer .otp-resend{font-size:12px}.__otpContainer .otp-resend .resendTxt{padding:0 8px}}@keyframes shake{0%{transform:translate(0)}25%{transform:translate(-5px)}50%{transform:translate(5px)}75%{transform:translate(-5px)}to{transform:translate(0)}}\n"] }] }], ctorParameters: () => [] }); /* * Public API Surface of otp-angular */ /** * Generated bundle index. Do not edit. */ export { OtpAngular }; //# sourceMappingURL=otp-angular.mjs.map