@deepkit/desktop-ui
Version:
Library for desktop UI widgets in Angular 10+
281 lines (246 loc) • 9.79 kB
text/typescript
/*
* 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]);
}
});
};
}
({})
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();
});
}
}