Add support for dynamic themes

This commit is contained in:
Art Lowel
2021-02-25 15:04:32 +01:00
parent 70dac6bc8f
commit 5a6e4b1278
224 changed files with 3429 additions and 1017 deletions

View File

@@ -0,0 +1,116 @@
import {
Component,
ViewChild,
ViewContainerRef,
ComponentRef,
SimpleChanges,
OnInit,
OnDestroy,
ComponentFactoryResolver,
ChangeDetectorRef,
OnChanges
} from '@angular/core';
import { hasValue, isNotEmpty } from '../empty.util';
import { Subscription } from 'rxjs';
import { ThemeService } from './theme.service';
import { fromPromise } from 'rxjs/internal-compatibility';
import { catchError, switchMap, map } from 'rxjs/operators';
import { GenericConstructor } from '../../core/shared/generic-constructor';
@Component({
selector: 'ds-themed',
styleUrls: ['./themed.component.scss'],
templateUrl: './themed.component.html',
})
export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges {
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
protected compRef: ComponentRef<T>;
protected lazyLoadSub: Subscription;
protected themeSub: Subscription;
protected inAndOutputNames: (keyof T & keyof this)[] = [];
constructor(
protected resolver: ComponentFactoryResolver,
protected cdr: ChangeDetectorRef,
protected themeService: ThemeService
) {
}
protected abstract getComponentName(): string;
protected abstract importThemedComponent(themeName: string): Promise<any>;
protected abstract importUnthemedComponent(): Promise<any>;
ngOnChanges(changes: SimpleChanges): void {
// if an input or output has changed
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
this.connectInputsAndOutputs();
}
}
ngOnInit(): void {
this.destroyComponentInstance();
this.themeSub = this.themeService.getThemeName$().subscribe(() => {
this.renderComponentInstance();
});
}
ngOnDestroy(): void {
[this.themeSub, this.lazyLoadSub].filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
this.destroyComponentInstance();
}
protected renderComponentInstance(): void {
this.destroyComponentInstance();
if (hasValue(this.lazyLoadSub)) {
this.lazyLoadSub.unsubscribe();
}
this.lazyLoadSub =
fromPromise(this.importThemedComponent(this.themeService.getThemeName())).pipe(
// if there is no themed version of the component an exception is thrown,
// catch it and return null instead
catchError(() => [null]),
switchMap((themedFile: any) => {
if (hasValue(themedFile) && hasValue(themedFile[this.getComponentName()])) {
// if the file is not null, and exports a component with the specified name,
// return that component
return [themedFile[this.getComponentName()]];
} else {
// otherwise import and return the default component
return fromPromise(this.importUnthemedComponent()).pipe(
map((unthemedFile: any) => {
return unthemedFile[this.getComponentName()];
})
);
}
}),
).subscribe((constructor: GenericConstructor<T>) => {
const factory = this.resolver.resolveComponentFactory(constructor);
this.compRef = this.vcr.createComponent(factory);
this.connectInputsAndOutputs();
this.cdr.markForCheck();
});
}
protected destroyComponentInstance(): void {
if (hasValue(this.compRef)) {
this.compRef.destroy();
this.compRef = null;
}
if (hasValue(this.vcr)) {
this.vcr.clear();
}
}
protected connectInputsAndOutputs(): void {
if (isNotEmpty(this.inAndOutputNames) && hasValue(this.compRef) && hasValue(this.compRef.instance)) {
this.inAndOutputNames.forEach((name: any) => {
this.compRef.instance[name] = this[name];
});
}
}
}