From 30acd6013bac100156cd494a2262598d834cd37e Mon Sep 17 00:00:00 2001 From: chrismclarke Date: Tue, 10 Jan 2023 18:32:23 -0800 Subject: [PATCH] spike: add support for alternative template renderer --- package.json | 2 +- packages/data-models/flowTypes.ts | 2 + .../scripts/src/commands/app-data/copy.ts | 3 +- src/app/feature/template/template.page.html | 12 +- src/app/feature/template/template.page.ts | 29 +++- .../components/template/containers/index.ts | 8 + .../template-dynamic.component.html | 4 + .../template-dynamic.component.scss | 0 .../template-dynamic.component.spec.ts | 22 +++ .../template-dynamic.component.ts | 137 ++++++++++++++++++ .../directives/templateHost.directive.ts | 22 +++ .../template/services/template.service.ts | 21 +++ .../components/template/template.module.ts | 14 +- 13 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 src/app/shared/components/template/containers/index.ts create mode 100644 src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.html create mode 100644 src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.scss create mode 100644 src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.spec.ts create mode 100644 src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.ts create mode 100644 src/app/shared/components/template/directives/templateHost.directive.ts diff --git a/package.json b/package.json index 284a35f579..8292d56afb 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "ng": "ng", "start": "yarn precompile && yarn workflow populate_src_assets && concurrently --kill-others --raw \"yarn data-models:serve\" \"yarn data:serve\" \"ng serve --open\"", - "start:local": "yarn workflow populate_src_assets && concurrently --kill-others --raw \"ng serve --open\" \"yarn workflow sync_local\"", + "start:local": "yarn precompile && yarn workflow populate_src_assets && concurrently --kill-others --raw \"ng serve --open\" \"yarn workflow sync_local\"", "precompile": "yarn scripts compile types", "build": "yarn precompile && yarn workflow populate_src_assets && ng build --configuration=production", "data:serve": "yarn workspace app-data serve", diff --git a/packages/data-models/flowTypes.ts b/packages/data-models/flowTypes.ts index 4a035a03ef..15a7504b33 100644 --- a/packages/data-models/flowTypes.ts +++ b/packages/data-models/flowTypes.ts @@ -229,6 +229,8 @@ export namespace FlowTypes { flow_type: "template"; rows: TemplateRow[]; comments?: string; + /** Optional display type override to render template in experimental format */ + template_type?: "default" | "dynamic"; } export type TemplateRowType = diff --git a/packages/scripts/src/commands/app-data/copy.ts b/packages/scripts/src/commands/app-data/copy.ts index 0b0adbc185..27dd9799f6 100644 --- a/packages/scripts/src/commands/app-data/copy.ts +++ b/packages/scripts/src/commands/app-data/copy.ts @@ -312,7 +312,8 @@ export const ASSETS_CONTENTS_LIST = ${JSON.stringify(cleanedContents, null, 2)} private extractContentsData(flow: FlowTypes.FlowTypeWithData): FlowTypes.FlowTypeBase { // remove rows property (if exists) - const { rows, status, _processed, ...keptFields } = flow; + const { rows, status, _processed, ...templateFields } = flow; + const { template_type, ...keptFields } = templateFields as FlowTypes.Template; return keptFields as FlowTypes.FlowTypeBase; } private sheetsWriteContents(baseFolder: string, contents: ISheetContents) { diff --git a/src/app/feature/template/template.page.html b/src/app/feature/template/template.page.html index 4d0c18faa9..5638f704fa 100644 --- a/src/app/feature/template/template.page.html +++ b/src/app/feature/template/template.page.html @@ -1,9 +1,9 @@ - -
+ + + + +

Select a Template

@@ -13,5 +13,5 @@

Select a Template

>{{template.flow_name}}
-
+
diff --git a/src/app/feature/template/template.page.ts b/src/app/feature/template/template.page.ts index 4bdf70af19..9fa78d0ab0 100644 --- a/src/app/feature/template/template.page.ts +++ b/src/app/feature/template/template.page.ts @@ -1,6 +1,8 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { FlowTypes } from "packages/data-models"; +import { TemplateHostDirective } from "src/app/shared/components/template/directives/templateHost.directive"; +import { TemplateService } from "src/app/shared/components/template/services/template.service"; import { AppDataService } from "src/app/shared/services/data/app-data.service"; @Component({ @@ -13,14 +15,27 @@ export class TemplatePage implements OnInit { filterTerm: string; allTemplates: FlowTypes.FlowTypeBase[] = []; filteredTemplates: FlowTypes.FlowTypeBase[] = []; - constructor(private route: ActivatedRoute, private appDataService: AppDataService) {} - ngOnInit() { - this.templateName = this.route.snapshot.params.templateName; - const allTemplates = this.appDataService.listSheetsByType("template"); + @ViewChild(TemplateHostDirective, { static: true }) templateHost!: TemplateHostDirective; + + constructor( + private route: ActivatedRoute, + private appDataService: AppDataService, + private templateService: TemplateService + ) {} - this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); - this.filteredTemplates = allTemplates; + ngOnInit() { + const templateName = this.route.snapshot.params.templateName; + if (templateName) { + this.templateName = templateName; + this.templateService.injectTemplate(templateName, this.templateHost); + } + // Display list of all templates if not specified + else { + const allTemplates = this.appDataService.listSheetsByType("template"); + this.allTemplates = allTemplates.sort((a, b) => (a.flow_name > b.flow_name ? 1 : -1)); + this.filteredTemplates = allTemplates; + } } search() { diff --git a/src/app/shared/components/template/containers/index.ts b/src/app/shared/components/template/containers/index.ts new file mode 100644 index 0000000000..99593445d3 --- /dev/null +++ b/src/app/shared/components/template/containers/index.ts @@ -0,0 +1,8 @@ +import { TemplateContainerComponent } from "../template-container.component"; +import { TemplateDynamicComponent } from "./template-dynamic/template-dynamic.component"; + +/** Available template containers to handle template rendering strategy */ +export const TEMLATE_CONTAINERS = { + default: TemplateContainerComponent, + dynamic: TemplateDynamicComponent, +} as const; diff --git a/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.html b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.html new file mode 100644 index 0000000000..a0b290b4a1 --- /dev/null +++ b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.html @@ -0,0 +1,4 @@ + + + + diff --git a/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.scss b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.spec.ts b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.spec.ts new file mode 100644 index 0000000000..3ac75dd9a1 --- /dev/null +++ b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { TemplateDynamicComponent } from "./template-dynamic.component"; + +describe("TemplateDynamicComponent", () => { + let component: TemplateDynamicComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TemplateDynamicComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TemplateDynamicComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.ts b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.ts new file mode 100644 index 0000000000..950cd2138f --- /dev/null +++ b/src/app/shared/components/template/containers/template-dynamic/template-dynamic.component.ts @@ -0,0 +1,137 @@ +import { + Component, + Input, + QueryList, + ViewChildren, + ChangeDetectorRef, + ChangeDetectionStrategy, + OnInit, + Type, +} from "@angular/core"; +import { TEMPLATE_COMPONENT_MAPPING } from "../../components"; +import { TemplateHostDirective } from "../../directives/templateHost.directive"; +import { FlowTypes, ITemplateRowProps } from "../../models"; + +@Component({ + selector: "plh-template-dynamic", + templateUrl: "./template-dynamic.component.html", + styleUrls: ["./template-dynamic.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TemplateDynamicComponent implements OnInit { + public renderedRows: BaseRow[] = []; + + /** */ + @ViewChildren(TemplateHostDirective, { read: TemplateHostDirective }) + rowHosts: QueryList; + + @Input() template: FlowTypes.Template; + + private allRows: BaseRow[] = []; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.loadRows(); + this.render(); + } + + private loadRows() { + this.allRows = this.template.rows.map((r) => { + const displayComponent = TEMPLATE_COMPONENT_MAPPING[r.type]; + if (displayComponent) { + return new DisplayRowBase(r, displayComponent); + } + const structuralCommponent = STRUCTURAL_ROW_MAPPING[r.type]; + if (structuralCommponent) { + return new structuralCommponent(r); + } + return new BaseRow(r); + }); + } + + private render() { + // evaluate rows to determine number of display components to render + const renderedRows = []; + for (const row of this.allRows) { + const { rendered } = row.evaluate(); + if (rendered) { + renderedRows.push(row); + } + } + this.renderedRows = renderedRows; + this.cdr.detectChanges(); + // render components + this.rowHosts.map((host, index) => { + this.renderedRows[index].render(host); + }); + this.cdr.detectChanges(); + } + + trackByFn(index) { + return index; + } +} + +/*************************************************************** + * Row Renderers + **************************************************************/ + +class BaseRow { + public rendered: Boolean = false; + public renderComponents: any[] = []; + public render(host: TemplateHostDirective) {} + private listeners() { + // TODO - find a way to attach dynamic variables (or in parent) and queue re-render as required + } + + private children?: BaseRow[]; + + constructor(public row: FlowTypes.TemplateRow) {} + + public evaluate() { + this.rendered = true; + + if (this.children) { + // TODO + } + return this; + } +} + +class DisplayRowBase extends BaseRow { + private renderCount = 0; + + public renderID: string; + + constructor( + public row: FlowTypes.TemplateRow, + private displayComponent: Type + ) { + super(row); + } + + public override render(host: TemplateHostDirective) { + this.renderCount++; + const viewContainerRef = host.viewContainerRef; + viewContainerRef.clear(); + const componentRef = viewContainerRef.createComponent(this.displayComponent); + componentRef.instance.row = this.row; + componentRef.instance.parent = null; + } +} +class TemplateRow extends BaseRow {} + +class SetLocalRow extends BaseRow {} + +// TODO - better if distinction between display/structural component made at origingal type-def and +// folder levels. Also should move into components/structural folder + +const STRUCTURAL_ROW_MAPPING: PartialRecord = { + set_local: SetLocalRow, + template: TemplateRow, +}; + +type PartialRecord = { + [P in K]?: T; +}; diff --git a/src/app/shared/components/template/directives/templateHost.directive.ts b/src/app/shared/components/template/directives/templateHost.directive.ts new file mode 100644 index 0000000000..7dee8669f3 --- /dev/null +++ b/src/app/shared/components/template/directives/templateHost.directive.ts @@ -0,0 +1,22 @@ +import { Directive, ViewContainerRef } from "@angular/core"; + +/** + * Provides reference for injecting template + * @example component.html + * ```ts + *
+ * ``` + * + * @example component.ts + * ```ts + * @ViewChild(TemplateHostDirective, { static: true }) templateHost!: TemplateHostDirective; + * + * ngOnInit(){ + * this.templateService.injectTemplate(name,this.templateHost) + * } + * ``` + */ +@Directive({ selector: "[templateHost]" }) +export class TemplateHostDirective { + constructor(public viewContainerRef: ViewContainerRef) {} +} diff --git a/src/app/shared/components/template/services/template.service.ts b/src/app/shared/components/template/services/template.service.ts index 030a293b14..13d99c9cab 100644 --- a/src/app/shared/components/template/services/template.service.ts +++ b/src/app/shared/components/template/services/template.service.ts @@ -11,6 +11,8 @@ import { TemplateVariablesService } from "./template-variables.service"; import { TemplateFieldService } from "./template-field.service"; import { arrayToHashmap } from "src/app/shared/utils"; import { SyncServiceBase } from "src/app/shared/services/syncService.base"; +import { TemplateHostDirective } from "../directives/templateHost.directive"; +import { TEMLATE_CONTAINERS } from "../containers"; @Injectable({ providedIn: "root", @@ -85,6 +87,25 @@ export class TemplateService extends SyncServiceBase { return { modal, ...dismissData }; } + /** Dynamically inject a template within a given host container */ + public async injectTemplate(name: string, host: TemplateHostDirective) { + const template = await this.getTemplateByName(name, false); + const viewContainerRef = host.viewContainerRef; + viewContainerRef.clear(); + // WiP - Experimental new format + if (template.template_type === "dynamic") { + const container = TEMLATE_CONTAINERS.dynamic; + const componentRef = viewContainerRef.createComponent(container); + componentRef.instance.template = template; + } + // Standard template container + else { + const container = TEMLATE_CONTAINERS.default; + const componentRef = viewContainerRef.createComponent(container); + componentRef.instance.templatename = name; + } + } + /** * Iterate over global template rows, assigning `declare_field_default` values to fields if they do not already exist, * and `declare_global_constant` values to global regardless. diff --git a/src/app/shared/components/template/template.module.ts b/src/app/shared/components/template/template.module.ts index 6c87f2c1e1..2252707823 100644 --- a/src/app/shared/components/template/template.module.ts +++ b/src/app/shared/components/template/template.module.ts @@ -16,6 +16,10 @@ import { TmplCompHostDirective, TemplateComponent } from "./template-component"; import { appendStyleSvgDirective } from "./directives/shadowStyleSvg.directive"; import { createCustomElement } from "@angular/elements"; +import { TemplateDynamicComponent } from "./containers/template-dynamic/template-dynamic.component"; +import { TemplateHostDirective } from "./directives/templateHost.directive"; + +const TEMPLATE_DIRECTIVES = [TemplateHostDirective, appendStyleSvgDirective]; @NgModule({ imports: [ @@ -29,15 +33,21 @@ import { createCustomElement } from "@angular/elements"; RouterModule, SwiperModule, ], - exports: [...TEMPLATE_COMPONENTS, ...TEMPLATE_PIPES, TemplateContainerComponent], + exports: [ + ...TEMPLATE_COMPONENTS, + ...TEMPLATE_DIRECTIVES, + ...TEMPLATE_PIPES, + TemplateContainerComponent, + ], declarations: [ TmplCompHostDirective, TemplateComponent, TooltipDirective, ...TEMPLATE_COMPONENTS, + ...TEMPLATE_DIRECTIVES, ...TEMPLATE_PIPES, TemplateContainerComponent, - appendStyleSvgDirective, + TemplateDynamicComponent, ], // Include the container component as an entry component so that we can a custom elements for it (see below) entryComponents: [TemplateContainerComponent],