From c4aff620e36fdd83c2799cfdc9bddb71914a42dc Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 11 Sep 2024 13:39:06 +0300 Subject: [PATCH] UI: Improve custom components compilation -> use standalone components. --- .../dynamic-component-factory.service.ts | 93 ++++--------------- .../app/core/services/resources.service.ts | 71 +------------- .../custom-dialog-container.component.ts | 13 ++- .../widget/dialog/custom-dialog.service.ts | 7 +- .../widget/widget-component.service.ts | 5 +- .../components/widget/widget.component.ts | 2 +- .../home/models/widget-component.models.ts | 4 +- .../shared/components/markdown.component.ts | 12 ++- 8 files changed, 42 insertions(+), 165 deletions(-) diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index d18797a1be..1c7150bac3 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/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 { - componentType: Type; - componentModuleRef: NgModuleRef; -} - -interface DynamicComponentModuleData { - moduleRef: NgModuleRef; - moduleType: Type; -} - @Injectable({ providedIn: 'root' }) export class DynamicComponentFactoryService { - private dynamicComponentModulesMap = new Map, DynamicComponentModuleData>(); - - constructor(private compiler: Compiler, - private injector: Injector) { + constructor() { } public createDynamicComponent( @@ -64,59 +32,38 @@ export class DynamicComponentFactoryService { template: string, modules?: Type[], preserveWhitespaces?: boolean, - styles?: string[]): Observable> { + styles?: string[]): Observable> { return from(import('@angular/compiler')).pipe( mergeMap(() => { - const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles); - let moduleImports: Type[] = [CommonModule]; + let componentImports: Type[] = [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; - 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(componentType: Type) { - const moduleData = this.dynamicComponentModulesMap.get(componentType); - if (moduleData) { - moduleData.moduleRef.destroy(); - this.compiler.clearCacheFor(moduleData.moduleType); - this.dynamicComponentModulesMap.delete(componentType); - } + public destroyDynamicComponent(_componentType: Type) { + } + + public getComponentDef(componentType: Type): ɵComponentDef { + return componentType[ɵNG_COMP_DEF]; } - private _createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type { + private createAndCompileDynamicComponent(componentType: Type, template: string, imports: Type[], + preserveWhitespaces?: boolean, styles?: string[]): ɵComponentDef { // noinspection AngularMissingOrInvalidDeclarationInModule - return Component({ + const comp = Component({ template, + imports, preserveWhitespaces, - styles + styles, + standalone: true })(componentType); + // Trigger component compilation + return comp[ɵNG_COMP_DEF]; } } diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index 449e950a65..013c3e5d72 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/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 } = {}; private loadedResources: { [url: string]: ReplaySubject } = {}; - private loadedModules: { [url: string]: ReplaySubject[]> } = {}; private loadedModulesAndFactories: { [url: string]: ReplaySubject } = {}; 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[]> { - 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[]>(); - 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>[] = []; - 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[] = []): Type[] { 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 = {}; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts index 089297427f..889a19a5aa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog-container.component.ts +++ b/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; - customComponentModuleRef: NgModuleRef; } @Component({ @@ -80,7 +79,7 @@ export class CustomDialogContainerComponent extends DialogComponent { + 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); }) ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 508d07ea0d..8e1e6f1b3a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/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 => { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 76845a11ef..5bd4910af7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/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) { diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 5a1a28472e..673bc21edb 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/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; - componentModuleRef?: NgModuleRef; } export interface WidgetConfigComponentData { diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 069bca2b2d..d890817d04 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/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; 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];