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. /// limitations under the License.
/// ///
import { import { Component, Injectable, Type, ɵComponentDef, ɵNG_COMP_DEF } from '@angular/core';
Compiler,
Component,
Injectable,
Injector,
NgModule,
NgModuleRef,
OnDestroy,
Type, ɵNG_COMP_DEF,
ɵresetCompiledComponents
} from '@angular/core';
import { from, Observable, of } from 'rxjs'; import { from, Observable, of } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { mergeMap } from 'rxjs/operators'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DynamicComponentFactoryService { export class DynamicComponentFactoryService {
private dynamicComponentModulesMap = new Map<Type<any>, DynamicComponentModuleData>(); constructor() {
constructor(private compiler: Compiler,
private injector: Injector) {
} }
public createDynamicComponent<T>( public createDynamicComponent<T>(
@ -64,59 +32,38 @@ export class DynamicComponentFactoryService {
template: string, template: string,
modules?: Type<any>[], modules?: Type<any>[],
preserveWhitespaces?: boolean, preserveWhitespaces?: boolean,
styles?: string[]): Observable<DynamicComponentData<T>> { styles?: string[]): Observable<Type<T>> {
return from(import('@angular/compiler')).pipe( return from(import('@angular/compiler')).pipe(
mergeMap(() => { mergeMap(() => {
const comp = this._createDynamicComponent(componentType, template, preserveWhitespaces, styles); let componentImports: Type<any>[] = [CommonModule];
let moduleImports: Type<any>[] = [CommonModule];
if (modules) { if (modules) {
moduleImports = [...moduleImports, ...modules]; componentImports = [...componentImports, ...modules];
} }
// noinspection AngularInvalidImportedOrDeclaredSymbol const comp = this.createAndCompileDynamicComponent(componentType, template, componentImports, preserveWhitespaces, styles);
const dynamicComponentInstanceModule = NgModule({ return of(comp.type);
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
});
}) })
); );
} }
public destroyDynamicComponent<T>(componentType: Type<T>) { public destroyDynamicComponent<T>(_componentType: Type<T>) {
const moduleData = this.dynamicComponentModulesMap.get(componentType); }
if (moduleData) {
moduleData.moduleRef.destroy(); public getComponentDef<T>(componentType: Type<T>): ɵComponentDef<T> {
this.compiler.clearCacheFor(moduleData.moduleType); return componentType[ɵNG_COMP_DEF];
this.dynamicComponentModulesMap.delete(componentType);
}
} }
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 // noinspection AngularMissingOrInvalidDeclarationInModule
return Component({ const comp = Component({
template, template,
imports,
preserveWhitespaces, preserveWhitespaces,
styles styles,
standalone: true
})(componentType); })(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, Injectable,
Injector, Injector,
ModuleWithComponentFactories, ModuleWithComponentFactories,
Type Type, ɵNG_MOD_DEF
} from '@angular/core'; } from '@angular/core';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
@ -50,7 +50,6 @@ export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {}; private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {}; private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {}; private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0]; 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>[] { private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
try { try {
let potentialModules = [module]; let potentialModules = [module];
@ -274,7 +208,7 @@ export class ResourcesService {
while (potentialModules.length && currentScanDepth < 10) { while (potentialModules.length && currentScanDepth < 10) {
const newPotentialModules = []; const newPotentialModules = [];
for (const potentialModule of potentialModules) { for (const potentialModule of potentialModules) {
if (potentialModule && ('ɵmod' in potentialModule)) { if (potentialModule && (ɵNG_MOD_DEF in potentialModule)) {
modules.push(potentialModule); modules.push(potentialModule);
} else { } else {
for (const k of Object.keys(potentialModule)) { for (const k of Object.keys(potentialModule)) {
@ -349,7 +283,6 @@ export class ResourcesService {
} }
private clearModulesCache() { private clearModulesCache() {
this.loadedModules = {};
this.loadedModulesAndFactories = {}; 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { import {
Component, Component,
ComponentFactory, ComponentRef,
ComponentRef, HostBinding, HostBinding,
Inject, Inject,
Injector, NgModuleRef, Injector,
OnDestroy, Type, OnDestroy,
Type,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
@ -35,13 +36,11 @@ import {
} from '@home/components/widget/dialog/custom-dialog.component'; } from '@home/components/widget/dialog/custom-dialog.component';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DynamicComponentModule } from '@core/services/dynamic-component-factory.service';
export interface CustomDialogContainerData { export interface CustomDialogContainerData {
controller: (instance: CustomDialogComponent) => void; controller: (instance: CustomDialogComponent) => void;
data?: any; data?: any;
customComponentType: Type<CustomDialogComponent>; customComponentType: Type<CustomDialogComponent>;
customComponentModuleRef: NgModuleRef<DynamicComponentModule>;
} }
@Component({ @Component({
@ -80,7 +79,7 @@ export class CustomDialogContainerComponent extends DialogComponent<CustomDialog
}); });
try { try {
this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentType, this.customComponentRef = this.viewContainerRef.createComponent(this.data.customComponentType,
{index: 0, injector, ngModuleRef: this.data.customComponentModuleRef}); {index: 0, injector});
} catch (e: any) { } catch (e: any) {
let message; let message;
if (e.message?.startsWith('NG0')) { 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( return this.dynamicComponentFactoryService.createDynamicComponent(
class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe( class CustomDialogComponentInstance extends CustomDialogComponent {}, template, modules).pipe(
mergeMap((componentData) => { mergeMap((componentType) => {
const dialogData: CustomDialogContainerData = { const dialogData: CustomDialogContainerData = {
controller, controller,
customComponentType: componentData.componentType, customComponentType: componentType,
customComponentModuleRef: componentData.componentModuleRef,
data data
}; };
let dialogConfig: MatDialogConfig = { let dialogConfig: MatDialogConfig = {
@ -79,7 +78,7 @@ export class CustomDialogService {
CustomDialogContainerComponent, CustomDialogContainerComponent,
dialogConfig).afterClosed().pipe( dialogConfig).afterClosed().pipe(
tap(() => { 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, widgetInfo.templateHtml,
resolvedModules.modules resolvedModules.modules
).pipe( ).pipe(
map((componentData) => { map((componentType) => {
widgetInfo.componentType = componentData.componentType; widgetInfo.componentType = componentType;
widgetInfo.componentModuleRef = componentData.componentModuleRef;
return null; return null;
}), }),
catchError(e => { 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 { try {
this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType, this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentType,
{index: 0, injector, ngModuleRef: this.widgetInfo.componentModuleRef}); {index: 0, injector});
this.cd.detectChanges(); this.cd.detectChanges();
} catch (e) { } catch (e) {
if (this.dynamicWidgetComponentRef) { if (this.dynamicWidgetComponentRef) {

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

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

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

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

Loading…
Cancel
Save