Browse Source

UI: Improve custom components compilation -> use standalone components.

pull/11660/head
Igor Kulikov 2 years ago
parent
commit
c4aff620e3
  1. 93
      ui-ngx/src/app/core/services/dynamic-component-factory.service.ts
  2. 71
      ui-ngx/src/app/core/services/resources.service.ts
  3. 13
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts
  4. 7
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  5. 5
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  6. 2
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  7. 4
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  8. 12
      ui-ngx/src/app/shared/components/markdown.component.ts

93
ui-ngx/src/app/core/services/dynamic-component-factory.service.ts

@ -14,49 +14,17 @@
/// limitations under the License.
///
import {
Compiler,
Component,
Injectable,
Injector,
NgModule,
NgModuleRef,
OnDestroy,
Type, ɵNG_COMP_DEF,
ɵresetCompiledComponents
} from '@angular/core';
import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core';
import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common';
import { mergeMap } from 'rxjs/operators';
@NgModule()
export abstract class DynamicComponentModule implements OnDestroy {
// eslint-disable-next-line @angular-eslint/contextual-lifecycle
ngOnDestroy(): void {
}
}
interface DynamicComponentData<T> {
componentType: Type<T>;
componentModuleRef: NgModuleRef<DynamicComponentModule>;
}
interface DynamicComponentModuleData {
moduleRef: NgModuleRef<DynamicComponentModule>;
moduleType: Type<DynamicComponentModule>;
}
@Injectable({
providedIn: 'root'
})
export class DynamicComponentFactoryService {
private dynamicComponentModulesMap = new Map<Type<any>, DynamicComponentModuleData>();
constructor(private compiler: Compiler,
private injector: Injector) {
constructor() {
}
public createDynamicComponent<T>(
@ -64,59 +32,38 @@ export class DynamicComponentFactoryService {
template: string,
modules?: Type<any>[],
preserveWhitespaces?: boolean,
styles?: string[]): Observable<DynamicComponentData<T>> {
styles?: string[]): Observable<Type<T>> {
return from(import('@angular/compiler')).pipe(
mergeMap(() => {
const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles);
let moduleImports: Type<any>[] = [CommonModule];
let componentImports: Type<any>[] = [CommonModule];
if (modules) {
moduleImports = [...moduleImports, ...modules];
componentImports = [...componentImports, ...modules];
}
// noinspection AngularInvalidImportedOrDeclaredSymbol
const dynamicComponentInstanceModule = NgModule({
declarations: [comp],
imports: moduleImports
})(class DynamicComponentInstanceModule extends DynamicComponentModule {});
const module = this.compiler.compileModuleSync(dynamicComponentInstanceModule);
let moduleRef: NgModuleRef<any>;
try {
moduleRef = module.create(this.injector);
// eslint-disable-next-line
comp[ɵNG_COMP_DEF];
} catch (e) {
this.compiler.clearCacheFor(module.moduleType);
ɵresetCompiledComponents();
throw e;
}
this.dynamicComponentModulesMap.set(comp, {
moduleRef,
moduleType: module.moduleType
});
return of( {
componentType: comp,
componentModuleRef: moduleRef
});
const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles);
return of(comp.type);
})
);
}
public destroyDynamicComponent<T>(componentType: Type<T>) {
const moduleData = this.dynamicComponentModulesMap.get(componentType);
if (moduleData) {
moduleData.moduleRef.destroy();
this.compiler.clearCacheFor(moduleData.moduleType);
this.dynamicComponentModulesMap.delete(componentType);
}
public destroyDynamicComponent<T>(_componentType: Type<T>) {
}
public getComponentDef<T>(componentType: Type<T>): ɵComponentDef<T> {
return componentType[ɵNG_COMP_DEF];
}
private _createDynamicComponent<T>(componentType: Type<T>, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type<T> {
private createAndCompileDynamicComponent<T>(componentType: Type<T>, template: string, imports: Type<any>[],
preserveWhitespaces?: boolean, styles?: string[]): ɵComponentDef<T> {
// noinspection AngularMissingOrInvalidDeclarationInModule
return Component({
const comp = Component({
template,
imports,
preserveWhitespaces,
styles
styles,
standalone: true
})(componentType);
// Trigger component compilation
return comp[ɵNG_COMP_DEF];
}
}

71
ui-ngx/src/app/core/services/resources.service.ts

@ -21,7 +21,7 @@ import {
Injectable,
Injector,
ModuleWithComponentFactories,
Type
Type, ɵNG_MOD_DEF
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
@ -50,7 +50,6 @@ export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
@ -201,71 +200,6 @@ export class ResourcesService {
);
}
public loadModules(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<Type<any>[]> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<Type<any>[]>();
this.loadedModules[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url, undefined, meta).then(
(module) => {
try {
let modules;
try {
modules = this.extractNgModules(module);
} catch (e) {
console.error(e);
}
if (modules && modules.length) {
const tasks: Promise<ModuleWithComponentFactories<any>>[] = [];
for (const m of modules) {
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
}
forkJoin(tasks).subscribe((compiled) => {
try {
for (const c of compiled) {
c.ngModuleFactory.create(this.injector);
}
this.loadedModules[url].next(modules);
this.loadedModules[url].complete();
} catch (e) {
this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
}
},
(e) => {
this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
});
} else {
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
}
} catch (e) {
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
}
},
(e) => {
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
console.error(`Unable to load module from url: ${url}`, e);
}
);
}
);
return subject.asObservable().pipe(
tap({
next: () => System.delete(url),
error: () => {
delete this.loadedModulesAndFactories[url];
System.delete(url);
},
complete: () => System.delete(url)
})
);
}
private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
try {
let potentialModules = [module];
@ -274,7 +208,7 @@ export class ResourcesService {
while (potentialModules.length && currentScanDepth < 10) {
const newPotentialModules = [];
for (const potentialModule of potentialModules) {
if (potentialModule && ('ɵmod' in potentialModule)) {
if (potentialModule && (ɵNG_MOD_DEF in potentialModule)) {
modules.push(potentialModule);
} else {
for (const k of Object.keys(potentialModule)) {
@ -349,7 +283,6 @@ export class ResourcesService {
}
private clearModulesCache() {
this.loadedModules = {};
this.loadedModulesAndFactories = {};
}
}

13
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts

@ -17,11 +17,12 @@
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import {
Component,
ComponentFactory,
ComponentRef, HostBinding,
ComponentRef,
HostBinding,
Inject,
Injector, NgModuleRef,
OnDestroy, Type,
Injector,
OnDestroy,
Type,
ViewContainerRef
} from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
@ -35,13 +36,11 @@ import {
} from '@home/components/widget/dialog/custom-dialog.component';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
export interface CustomDialogContainerData {
controller: (instance: CustomDialogComponent) => void;
data?: any;
customComponentType: Type<CustomDialogComponent>;
customComponentModuleRef: NgModuleRef<DynamicComponentModule>;
}
@Component({
@ -80,7 +79,7 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
});
try {
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentType,
{index: 0, injector, ngModuleRef: this.data.customComponentModuleRef});
{index: 0, injector});
} catch (e: any) {
let message;
if (e.message?.startsWith('NG0')) {

7
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -60,11 +60,10 @@ export class CustomDialogService {
}
return this.dynamicComponentFactoryService.createDynamicComponent(
class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
mergeMap((componentData) => {
mergeMap((componentType) => {
const dialogData: CustomDialogContainerData = {
controller,
customComponentType: componentData.componentType,
customComponentModuleRef: componentData.componentModuleRef,
customComponentType: componentType,
data
};
let dialogConfig: MatDialogConfig = {
@ -79,7 +78,7 @@ export class CustomDialogService {
CustomDialogContainerComponent,
dialogConfig).afterClosed().pipe(
tap(() => {
this.dynamicComponentFactoryService.destroyDynamicComponent(componentData.componentType);
this.dynamicComponentFactoryService.destroyDynamicComponent(componentType);
})
);
}

5
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -381,9 +381,8 @@ export class WidgetComponentService {
widgetInfo.templateHtml,
resolvedModules.modules
).pipe(
map((componentData) => {
widgetInfo.componentType = componentData.componentType;
widgetInfo.componentModuleRef = componentData.componentModuleRef;
map((componentType) => {
widgetInfo.componentType = componentType;
return null;
}),
catchError(e => {

2
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -760,7 +760,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
try {
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType,
{index: 0, injector, ngModuleRef: this.widgetInfo.componentModuleRef});
{index: 0, injector});
this.cd.detectChanges();
} catch (e) {
if (this.dynamicWidgetComponentRef) {

4
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -47,7 +47,7 @@ import {
WidgetActionsApi,
WidgetSubscriptionApi
} from '@core/api/widget-api.models';
import { ChangeDetectorRef, Injector, NgModuleRef, NgZone, Type } from '@angular/core';
import { ChangeDetectorRef, Injector, NgZone, Type } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { RafService } from '@core/services/raf.service';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -101,7 +101,6 @@ import { AlarmQuery, AlarmSearchStatus, AlarmStatus } from '@app/shared/models/a
import { ImagePipe, MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api';
import { UserId } from '@shared/models/id/user-id';
import { UserSettingsService } from '@core/http/user-settings.service';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { UtilsService } from '@core/services/utils.service';
@ -549,7 +548,6 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
description?: string;
tags?: string[];
componentType?: Type<IDynamicWidgetComponent>;
componentModuleRef?: NgModuleRef<DynamicComponentModule>;
}
export interface WidgetConfigComponentData {

12
ui-ngx/src/app/shared/components/markdown.component.ts

@ -22,7 +22,8 @@ import {
EventEmitter,
Inject,
Injector,
Input, NgZone,
Input,
NgZone,
OnChanges,
Output,
Renderer2,
@ -132,7 +133,8 @@ export class TbMarkdownComponent implements OnChanges {
let readyObservable: Observable<void>;
if (this.applyDefaultMarkdownStyle) {
if (!defaultMarkdownStyle) {
defaultMarkdownStyle = deepClone(TbMarkdownComponent['ɵcmp'].styles)[0].replace(/\[_nghost\-%COMP%\]/g, '')
const compDef = this.dynamicComponentFactoryService.getComponentDef(TbMarkdownComponent);
defaultMarkdownStyle = deepClone(compDef.styles[0]).replace(/\[_nghost\-%COMP%\]/g, '')
.replace(/\[_ngcontent\-%COMP%\]/g, '');
}
styles.push(defaultMarkdownStyle);
@ -161,13 +163,13 @@ export class TbMarkdownComponent implements OnChanges {
template,
compileModules,
true, styles
).subscribe((componentData) => {
this.tbMarkdownInstanceComponentType = componentData.componentType;
).subscribe((componentType) => {
this.tbMarkdownInstanceComponentType = componentType;
const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector});
try {
this.tbMarkdownInstanceComponentRef =
this.markdownContainer.createComponent(this.tbMarkdownInstanceComponentType,
{index: 0, injector, ngModuleRef: componentData.componentModuleRef});
{index: 0, injector});
if (this.context) {
for (const propName of Object.keys(this.context)) {
this.tbMarkdownInstanceComponentRef.instance[propName] = this.context[propName];

Loading…
Cancel
Save