ng-lock
Version:
Angular decorator for lock a function and user interface while a task running.
496 lines (482 loc) • 19.3 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, Injectable, Input, Directive, NgModule, makeEnvironmentProviders } from '@angular/core';
import { BehaviorSubject, tap } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { HttpContextToken, HttpContext, HTTP_INTERCEPTORS } from '@angular/common/http';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
const NG_UNLOCK_CALLBACK = 'ngUnlockCallback';
const NG_IS_LOCK_CALLBACK = 'ngIsLockCallback';
const NG_LOCK_SIGNAL = 'ngLockSignal';
const NG_LOCK_SUBJECT = 'ngLockSubject';
const NG_LOCK_OPTION = 'ngLockOption';
const NG_LOCK_LOCKED_CLASS = 'ng-lock-locked';
/**
* Unlock a locked function by ngLock() decorator
* @param {NgLockFunction} methodToUnlock The function to unlock
* @param {string} reason The reason for the log
* @return {void}
* @throws Error
*/
function ngUnlock(methodToUnlock, reason) {
const callback = ngCallbacks(methodToUnlock, NG_UNLOCK_CALLBACK);
callback(reason);
}
/**
* Unlock all locked functions by ngLock() decorator
* @param {any} component The component instance (this)
* @return {void}
*/
function ngUnlockAll(component) {
Object.getOwnPropertyNames(Object.getPrototypeOf(component)).forEach(key => {
const prop = component[key];
if (typeof prop === 'function' && typeof prop['ngLock'] === 'object' && typeof prop['ngLock'][NG_UNLOCK_CALLBACK] === 'function') {
if (prop['ngLock'][NG_IS_LOCK_CALLBACK]()) {
prop['ngLock'][NG_UNLOCK_CALLBACK]('ngUnlockAll');
}
}
});
}
/**
* Return true if the provided function is locked
* @param {NgLockFunction} methodToCheck The method to check
* @return {boolean}
* @throws Error
*/
function ngIsLock(methodToCheck) {
const callback = ngCallbacks(methodToCheck, NG_IS_LOCK_CALLBACK);
return callback();
}
/**
* Return a Signal for the given function on the lock status (locked/unlocked)
* @param {NgLockFunction} method The function
* @return {Signal<boolean>}
*/
function ngLockSignal(method) {
const callback = ngCallbacks(method, NG_LOCK_SIGNAL);
return callback();
}
/**
* Return the option for the given function
* @param {NgLockFunction} method The function
* @return {NgLockAllOption}
*/
function ngLockOption(method) {
const callback = ngCallbacks(method, NG_LOCK_OPTION);
return callback();
}
/**
* Return an Observable for the given function on the lock status (locked/unlocked)
* @param {NgLockFunction} method - The function
* @return {Observable<boolean>}
*/
function ngLockObservable(method) {
const callback = ngCallbacks(method, NG_LOCK_SUBJECT);
return callback();
}
/**
* Return the provided NG_CALLBACKS
* @param {NgLockFunction} method The function to return the unlock callback
* @param {NG_CALLBACKS} callback The NG_CALLBACKS
* @return {NgLockFunction} Return the NG_CALLBACKS
* @throws Error
*/
function ngCallbacks(method, callback) {
if (!(method instanceof Function)) {
throw new Error('"method" param must be a function.');
}
if (callback !== NG_UNLOCK_CALLBACK && callback !== NG_IS_LOCK_CALLBACK && callback !== NG_LOCK_SIGNAL && callback !== NG_LOCK_SUBJECT && callback !== NG_LOCK_OPTION) {
throw new Error(`"callback" param "${callback}" must be a NG_CALLBACKS.`);
}
if (typeof method['ngLock'] !== 'object') {
throw new Error(`"method" param (function ${method.name}) must be a @ngLock() decorated function.`);
}
if (typeof method['ngLock'][callback] !== 'function') {
throw new Error(`"method" param (function ${method.name}) must be a @ngLock() decorated function with "${callback}".`);
}
return method['ngLock'][callback];
}
/**
* Add class and attribute on HTML element
* @param {Element} elementToLock
* @param {NgLockOption} options
*/
const ngLockHtmlElement = (elementToLock, options) => {
if (!elementToLock) {
return;
}
if (options?.lockClass)
elementToLock.classList.add(options.lockClass);
elementToLock.setAttribute('disabled', 'disabled');
elementToLock.setAttribute('aria-disabled', 'true');
};
/**
* Remove class and attribute from HTML element
* @param {Element} elementToLock
* @param {NgLockOption} options
*/
const ngUnLockHtmlElement = (elementToLock, options) => {
if (!elementToLock) {
return;
}
if (options?.lockClass)
elementToLock.classList.remove(options.lockClass);
elementToLock.removeAttribute('disabled');
elementToLock.removeAttribute('aria-disabled');
};
/**
* Check if value is a Promise
*/
const isPromise = (value) => value && typeof value.finally === 'function' && typeof value.then === 'function' && value[Symbol.toStringTag] === 'Promise';
/**
* Check if value is a destination partialObserver
*/
const isObserver = (value) => value && typeof value?.destination?.partialObserver === 'object';
/**
* Uses the provided "selector" to find with "querySelector()" and apply the lockClass on the founded element.
* @param {string} selector A DOMString containing a selector to match.
* @returns {NgLockElementFinder} Return a NgLockElementFinder function
* @throws Error
*/
const ngLockElementByQuerySelector = (selector) => {
return (_self, _args) => {
if (!selector) {
throw new Error('selector is required');
}
const el = document.querySelector(selector);
if (el) {
return el;
}
else {
throw new Error('Element not found');
}
};
};
/**
* Uses a function argument for apply the lockClass. If provided a argsIndex use the specific argument, otherwise
* search an argument with a target property that is a HTMLElement
* @param {number} argsIndex (optional) index of the argument that is HTMLElement or contains target property (also a HTMLElement)
* @returns {NgLockElementFinder} Return a NgLockElementFinder function
* @throws Error
*/
const ngLockElementByTargetEventArgument = (argsIndex) => {
return (_self, args) => {
if (!args || args.length <= 0) {
throw new Error('Method without arguments');
}
let arg;
if (typeof argsIndex === 'number') {
if (argsIndex < 0) {
throw new Error('argsIndex must be grater than or equal 0');
}
else {
if (args.length - 1 < argsIndex) {
throw new Error('argsIndex grater than arguments length');
}
arg = args[argsIndex];
if (arg.currentTarget instanceof HTMLElement) {
return arg.currentTarget;
}
if (arg.target instanceof HTMLElement) {
return arg.target;
}
if (arg instanceof HTMLElement) {
return arg;
}
throw new Error('Argument not an HTMLElement or with target, currentTarget property');
}
}
else {
arg = args.find(_arg => _arg.currentTarget instanceof HTMLElement);
if (arg) {
return arg.currentTarget;
}
arg = args.find(_arg => _arg.target instanceof HTMLElement);
if (arg) {
return arg.target;
}
arg = args.find(_arg => _arg instanceof HTMLElement);
if (arg) {
return arg;
}
throw new Error('Argument not found');
}
};
};
/**
* Apply lockClass to a component property that must be a HTMLElement or element with Angular nativeElement (also a HTMLElement)
* @param {string} property The property name of the component
* @returns {NgLockElementFinder} Return a NgLockElementFinder function
* @throws Error
*/
const ngLockElementByComponentProperty = (property) => {
return (self, _args) => {
if (!property) {
throw new Error('Property is required');
}
const prop = self[property];
if (prop) {
if (prop instanceof HTMLElement) {
return prop;
}
if (prop.nativeElement instanceof HTMLElement) {
return prop.nativeElement;
}
throw new Error('Property must be a HTMLElement or object with nativeElement (also HTMLElement)');
}
else {
throw new Error('Property not found');
}
};
};
/**
* ngLock default options
* @see NgLockOption
*/
const NgLockDefaultOption = {
maxCall: 1,
unlockTimeout: null,
lockElementFunction: ngLockElementByTargetEventArgument(),
lockClass: NG_LOCK_LOCKED_CLASS,
returnLastResultWhenLocked: false,
unlockOnPromiseResolve: true,
unlockOnObservableChanges: true,
debug: false
};
/**
* Lock the decorated function
* @param {NgLockOption} options (optional) NgLockOption
* @return {MethodDecorator} Return a MethodDecorator
*/
function ngLock(options) {
return function (_target, key, descriptor) {
let _options;
if (!options) {
_options = { ...NgLockDefaultOption };
}
else {
_options = { ...NgLockDefaultOption, ...options };
}
let callCounter = 0;
let timeoutHandle = null;
let elementToLock = null;
let lastResult;
const originalMethod = descriptor.value;
const ngLockSignal = signal(false);
const ngLockSubject = new BehaviorSubject(false);
const ngLockLog = (...args) => {
if (_options.debug) {
console.log(...args);
}
};
const ngUnlockCallback = (log) => {
let message = `NgLock: unlock method "${key.toString()}"`;
message += `, reason: ${log ?? 'ngUnlock'}`;
if (!ngIsLockCallback()) {
message += ' (warning! the method is already unlocked)';
}
ngLockLog(message);
callCounter = 0;
ngUnLockHtmlElement(elementToLock, _options);
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
ngLockSignal.set(false);
ngLockSubject.next(false);
};
const ngIsLockCallback = () => callCounter >= _options?.maxCall;
ngLockLog(`NgLock: decorate method "${key.toString()}"`);
descriptor.value = function (...args) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
if (ngIsLockCallback()) {
ngLockLog(`NgLock: method "${key.toString()}" locked`);
if (_options.returnLastResultWhenLocked) {
return lastResult;
}
return;
}
callCounter++;
if (typeof _options.lockElementFunction === 'function') {
elementToLock = _options.lockElementFunction(self, args);
}
if (ngIsLockCallback()) {
ngLockHtmlElement(elementToLock, _options);
ngLockSignal.set(true);
ngLockSubject.next(true);
if (_options.unlockTimeout && _options.unlockTimeout > 0) {
timeoutHandle = setTimeout(() => ngUnlockCallback('unlockTimeout reached'), _options.unlockTimeout);
}
}
lastResult = originalMethod.apply(self, args);
ngLockLog(`NgLock: execute method "${key.toString()}"`);
if (_options.unlockOnPromiseResolve && isPromise(lastResult)) {
lastResult.finally(() => ngUnlockCallback('Promise resolved'));
}
else if (_options.unlockOnObservableChanges && isObserver(lastResult)) {
const obsNext = lastResult.destination.partialObserver.next;
if (typeof obsNext === 'function') {
lastResult.destination.partialObserver.next = (...args) => {
ngUnlockCallback('Subscription changes');
obsNext(args);
};
}
const obsError = lastResult.destination.partialObserver.error;
if (typeof obsError === 'function') {
lastResult.destination.partialObserver.error = (...args) => {
ngUnlockCallback('Subscription error');
obsError(args);
};
}
const obsComplete = lastResult.destination.partialObserver.complete;
if (typeof obsComplete === 'function') {
lastResult.destination.partialObserver.complete = () => {
ngUnlockCallback('Subscription complete');
obsComplete();
};
}
}
return lastResult;
};
Object.defineProperty(descriptor.value, 'ngLock', {
value: {
[NG_LOCK_OPTION]: () => _options,
[NG_UNLOCK_CALLBACK]: ngUnlockCallback,
[NG_IS_LOCK_CALLBACK]: ngIsLockCallback,
[NG_LOCK_SIGNAL]: () => ngLockSignal.asReadonly(),
[NG_LOCK_SUBJECT]: () => ngLockSubject.asObservable(),
}, enumerable: true, writable: false
});
return descriptor;
};
}
/**
* Unlock the given method on HTTP response
*/
const NG_LOCK_CONTEXT = new HttpContextToken(() => (function () { }));
/**
* Return HttpContextToken for unlock the given method on HTTP response
* @param {NgLockFunction} methodToUnlock the method to unlock
* @param {HttpContext} context current context
* @returns {HttpContext}
*/
const withNgLockContext = (methodToUnlock, context = new HttpContext()) => context.set(NG_LOCK_CONTEXT, methodToUnlock);
class NgLockInterceptorService {
intercept(req, next) {
return next.handle(req).pipe(finalize(() => {
const context = req.context.get(NG_LOCK_CONTEXT);
if (typeof context === 'function') {
ngUnlock(context, 'HTTP response');
}
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockInterceptorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockInterceptorService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockInterceptorService, decorators: [{
type: Injectable
}] });
/**
* @ngModule NgLockModule
*
* @description
*
* The `ngLock` directive it's a Angular directive lock html element when a decorated method with `@ngLock` is running a task.
*
* @usageNotes
*
* ### Usage
*
* ```html
* <input [ngLock]="myMethod" /><button (click)="myMethod($event)">Send</button>
* ```
* ```ts
* @ngLock()
* myMethod(event: MouseEvent){
* return new Promise(resolve => setTimeout(resolve, 5000));
* }
* ```
*/
class NgLockDirective {
constructor(eleRef, destroyRef) {
this.eleRef = eleRef;
this.destroyRef = destroyRef;
}
ngOnInit() {
const option = ngLockOption(this.ngLock);
ngLockObservable(this.ngLock).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(status => {
if (status) {
ngLockHtmlElement(this.eleRef.nativeElement, option);
}
else {
ngUnLockHtmlElement(this.eleRef.nativeElement, option);
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockDirective, deps: [{ token: i0.ElementRef }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.2", type: NgLockDirective, isStandalone: true, selector: "[ngLock]", inputs: { ngLock: "ngLock" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngLock]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.DestroyRef }], propDecorators: { ngLock: [{
type: Input,
args: [{ required: true }]
}] } });
class NgLockModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.2", ngImport: i0, type: NgLockModule, imports: [NgLockDirective], exports: [NgLockDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockModule, providers: [
NgLockInterceptorService,
{
provide: HTTP_INTERCEPTORS,
useClass: NgLockInterceptorService,
multi: true,
},
] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: NgLockModule, decorators: [{
type: NgModule,
args: [{
imports: [NgLockDirective],
exports: [NgLockDirective],
providers: [
NgLockInterceptorService,
{
provide: HTTP_INTERCEPTORS,
useClass: NgLockInterceptorService,
multi: true,
},
]
}]
}] });
/**
* Unlock the method when Observable changes
* @param {NgLockFunction} methodToUnlock method to unlock
* @returns {MonoTypeOperatorFunction<T>}
*/
const ngLockChanges = (methodToUnlock) => {
return (source$) => {
return source$.pipe(tap(() => ngUnlock(methodToUnlock, 'Observable changes')));
};
};
function provideNgLock() {
const providers = [makeEnvironmentProviders([{
provide: HTTP_INTERCEPTORS,
useClass: NgLockInterceptorService,
multi: true,
}])];
return providers;
}
/*
* Public API Surface of ng-lock
*/
/**
* Generated bundle index. Do not edit.
*/
export { NG_IS_LOCK_CALLBACK, NG_LOCK_CONTEXT, NG_LOCK_LOCKED_CLASS, NG_LOCK_OPTION, NG_LOCK_SIGNAL, NG_LOCK_SUBJECT, NG_UNLOCK_CALLBACK, NgLockDefaultOption, NgLockDirective, NgLockInterceptorService, NgLockModule, isObserver, isPromise, ngCallbacks, ngIsLock, ngLock, ngLockChanges, ngLockElementByComponentProperty, ngLockElementByQuerySelector, ngLockElementByTargetEventArgument, ngLockHtmlElement, ngLockObservable, ngLockOption, ngLockSignal, ngUnLockHtmlElement, ngUnlock, ngUnlockAll, provideNgLock, withNgLockContext };
//# sourceMappingURL=ng-lock.mjs.map