UNPKG

@deepkit/desktop-ui

Version:

Library for desktop UI widgets in Angular 10+

281 lines (246 loc) 9.79 kB
/* * Deepkit Framework * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt * * This program is free software: you can redistribute it and/or modify * it under the terms of the MIT License. * * You should have received a copy of the MIT License along with this program. */ import { AfterViewInit, ApplicationRef, inject, NgModule, OnDestroy, Type, ɵComponentDef as ComponentDef, ɵNG_COMP_DEF as NG_COMP_DEF, } from '@angular/core'; import { getClassName, nextTick, throttleTime } from '@deepkit/core'; import { EventDispatcher, EventToken } from '@deepkit/event'; import { Subscription } from 'rxjs'; export function observeAction() { return function (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> | void { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { const result = originalMethod.apply(this, args); if (result && result.then) { result.then(() => { ReactiveChangeDetectionModule.tick(); }, () => { ReactiveChangeDetectionModule.tick(); }); } else { ReactiveChangeDetectionModule.tick(); } return result; }; return descriptor; }; } const lazyValuesStore = new WeakMap<any, object>(); const lazyValuesSubscriptions = new WeakMap<any, object>(); const lazyValuesDestroyed = new WeakMap<any, object>(); function lazyInitialize(target: any, map: WeakMap<any, object> = lazyValuesStore): any { let object = map.get(target); if (object) return object; object = {}; map.set(target, object); return object; } function getRealMethodHookName(value: string): string { return 'ng' + value.substr(0, 1).toUpperCase() + value.substr(1); } function addComponentHook<T>(target: T, hookName: 'onDestroy' | 'onChanges' | 'onInit' | 'afterViewInit', fn: (this: T) => void) { const cdef: ComponentDef<any> = ((target as any).constructor as any)[NG_COMP_DEF]; if (cdef) { //prod build const ori = (cdef as any)[hookName]; ((cdef as any)['onDestroy'] as any) = function (this: any, ...args: any[]) { fn.call(this); ori && (ori as any).apply(this, args); }; } else { const ori = (target as any).constructor.prototype[getRealMethodHookName(hookName)]; (target as any).constructor.prototype[getRealMethodHookName(hookName)] = function (this: any, ...args: any[]) { fn.call(this); ori && (ori as any).apply(this, args); }; } } /** * Automatically unsubscribe the value (calling unsubscribe() on the current value) * when ngOnDestroy is called or a new value has been set. * When the component is already destroyed, newly set values will be unscubribed immediately. * This makes sure when a component is destroyed too fast before a async operation is completed * that the result is unsubscribed, otherwise it would be a memory leak. */ export function unsubscribe<T extends OnDestroy>() { return function (target: T, propertyKey: string | symbol) { function unsub(value: any) { if (value && value.unsubscribe) { try { value.unsubscribe(); } catch (error) { console.log('Subscription was already unsubscribed.', getClassName(target), propertyKey); } } } Object.defineProperty(target, propertyKey, { enumerable: true, configurable: false, //even with true the prop cant be deleted using `delete this.name` get() { const store = lazyInitialize(this); return store[propertyKey]; }, set(value) { const destroyed = lazyInitialize(this, lazyValuesDestroyed); const store = lazyInitialize(this); unsub(store[propertyKey]); if (destroyed['destroyed']) { unsub(value); } store[propertyKey] = value; } }); addComponentHook(target, 'onDestroy', function () { const destroyed = lazyInitialize(this, lazyValuesDestroyed); destroyed['destroyed'] = true; const store = lazyInitialize(this); if (store[propertyKey]) { unsub(store[propertyKey]); } }); }; } /** * Listens on the given event token and calls the method when the event is triggered. * * @example * ```typescript * * const MyEvent = new EventToken('my-event'); * * @Component({ * //.. * }); * class MyComponent { * @EventListener(MyEvent) * onMyEvent(event: MyEvent) { * console.log('event triggered', event); * } * } * ``` */ export function EventListener(eventToken: EventToken) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalConstructor = target.constructor; const newConstructor: any = function (...args: any[]) { const instance = new originalConstructor(...args); const store = lazyInitialize(instance); const eventDispatcher = inject(EventDispatcher); console.log('listen', eventToken, propertyKey); store['Ωlistener_' + propertyKey] = eventDispatcher.listen(eventToken, (event) => { instance[propertyKey](event); }); return instance; }; newConstructor.prototype = originalConstructor.prototype; target.constructor = newConstructor; addComponentHook(newConstructor, 'onDestroy', function () { const store = lazyInitialize(this); const unsubscribe = store['Ωlistener_' + propertyKey]; if (unsubscribe) unsubscribe(); }); }; } /** * Important for components that use material design, which need Tick in AfterViewInit. */ export function reactiveComponent<T extends AfterViewInit>() { return function (target: Type<T>) { addComponentHook(target.prototype, 'afterViewInit', function () { ReactiveChangeDetectionModule.tick(); }); }; } /** * Automatically subscribes on the value (when set) to trigger application ticks automatically. * When value is changed, the old subscription is cancelled and a new on the new value is created. * * Optionally @observe({unsubscribe: true}) unsubscribes the whole value as well (calling unsubscribe() on current value) on NgOnDestroy or when net property value is set. */ export function observe<T extends {}>(options: { unsubscribe?: true } = {}) { return function (target: T, propertyKey: string | symbol) { function unsub(value: any) { if (value && value.unsubscribe) { try { value.unsubscribe(); } catch (error) { console.log('Subscription was already unsubscribed.', getClassName(target), propertyKey); } } } Object.defineProperty(target, propertyKey, { enumerable: true, configurable: false, //even with true the prop cant be deleted using `delete this.name` get() { const store = lazyInitialize(this); return store[propertyKey]; }, set(value) { const destroyed = lazyInitialize(this, lazyValuesDestroyed); const store = lazyInitialize(this); const subscriptions = lazyInitialize(this, lazyValuesSubscriptions); if (subscriptions[propertyKey]) { unsub(subscriptions[propertyKey] as Subscription); delete subscriptions[propertyKey]; } if (options.unsubscribe && store[propertyKey] && store[propertyKey].unsubscribe) { unsub(store[propertyKey]); } if (!destroyed['destroyed'] && value && value.subscribe) { subscriptions[propertyKey] = value.subscribe(() => { ReactiveChangeDetectionModule.tick(); }); } if (destroyed['destroyed'] && options.unsubscribe) { unsub(value); } ReactiveChangeDetectionModule.tick(); store[propertyKey] = value; } }); addComponentHook(target, 'onDestroy', function () { const destroyed = lazyInitialize(this, lazyValuesDestroyed); destroyed['destroyed'] = true; const store = lazyInitialize(this); const subscriptions = lazyInitialize(this, lazyValuesSubscriptions); if (subscriptions[propertyKey]) { unsub(subscriptions[propertyKey]); delete subscriptions[propertyKey]; } if (options.unsubscribe) { unsub(store[propertyKey]); } }); }; } @NgModule({}) export class ReactiveChangeDetectionModule { private static a: ApplicationRef; // private static lastAnimationFrame?: number; private static throttled: Function; constructor(a: ApplicationRef) { ReactiveChangeDetectionModule.a = a; ReactiveChangeDetectionModule.throttled = throttleTime(() => { ReactiveChangeDetectionModule.a.tick(); }, 1000 / 25); } public static tick() { nextTick(() => { ReactiveChangeDetectionModule.throttled(); }); } }