diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index e577e37b21c..6473aa6bcaa 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -1,4 +1,5 @@ import { IgxColumnComponent, IgxGridComponent, IgxHierarchicalGridComponent } from 'igniteui-angular'; +import { html } from 'lit-html'; import { firstValueFrom, fromEvent, skip, timer } from 'rxjs'; import { ComponentRefKey, IgcNgElement } from './custom-strategy'; import hgridData from '../assets/data/projects-hgrid.js'; @@ -52,6 +53,43 @@ describe('Elements: ', () => { const columnComponent = (await columnEl.ngElementStrategy[ComponentRefKey]).instance as IgxColumnComponent; expect(gridComponent.columnList.toArray()).toContain(columnComponent); }); + + it(`should keep IgcNgElement instance in template of another IgcNgElement #15678`, async () => { + const gridEl = document.createElement("igc-grid"); + testContainer.appendChild(gridEl); + const columnEl = document.createElement("igc-column") as IgcNgElement; + gridEl.appendChild(columnEl); + gridEl.primaryKey = 'id'; + gridEl.data = [{ id: '1' }]; + (gridEl as any).detailTemplate = (ctx) => { + return html`
+ +
`; + } + + // TODO: Better way to wait - potentially expose the queue or observable for update on the strategy + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + + // sigh (ã€‚īšã€‚*) + (gridEl as any).toggleRow('1'); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + + let detailGrid = document.querySelector('#child1'); + expect(detailGrid).toBeDefined(); + let detailGridComponent = (await detailGrid?.ngElementStrategy[ComponentRefKey])?.instance as IgxGridComponent; + expect(detailGridComponent).toBeDefined(); + + // close and re-expand row detail: + (gridEl as any).toggleRow('1'); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + (gridEl as any).toggleRow('1'); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + + detailGrid = document.querySelector('#child1'); + expect(detailGrid).toBeDefined(); + detailGridComponent = (await detailGrid?.ngElementStrategy[ComponentRefKey])?.instance as IgxGridComponent; + expect(detailGridComponent).toBeDefined("Detail child grid was destroyed on re-expand"); + }); }); describe('Grid integration scenarios.', () => { diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index a114035ff5b..d8cfcb7ae2b 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -1,4 +1,5 @@ -import { ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, Injector, OnChanges, QueryList, Type, ViewContainerRef, reflectComponentType } from '@angular/core'; +import { ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, DestroyRef, Injector, OnChanges, QueryList, Type, ViewContainerRef, reflectComponentType } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NgElement } from '@angular/elements'; import { fromEvent } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -48,6 +49,14 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { return this._templateWrapper; } + private _configSelectors: string; + public get configSelectors(): string { + if (!this._configSelectors) { + this._configSelectors = this.config.map(x => x.selector).join(','); + } + return this._configSelectors; + } + constructor(private _componentFactory: ComponentFactory, private _injector: Injector, private config: ComponentConfig[]) { super(_componentFactory, _injector); } @@ -233,6 +242,14 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { } value = this.templateWrapper.addTemplate(value); // TODO: discard oldValue + + // check template for any angular-element components + this.templateWrapper.templateRendered.pipe(takeUntilDestroyed(componentRef.injector.get(DestroyRef))).subscribe((element) => { + element.querySelectorAll(this.configSelectors)?.forEach((c) => { + // tie to angularParent lifecycle for cached scenarios like detailTemplate: + c.ngElementStrategy.angularParent = componentRef; + }); + }); } if (componentRef && componentConfig?.boolProps?.includes(property)) { // bool coerce: diff --git a/projects/igniteui-angular-elements/src/app/wrapper/wrapper.component.ts b/projects/igniteui-angular-elements/src/app/wrapper/wrapper.component.ts index 35ba746cd94..5e782714f5f 100644 --- a/projects/igniteui-angular-elements/src/app/wrapper/wrapper.component.ts +++ b/projects/igniteui-angular-elements/src/app/wrapper/wrapper.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectorRef, Component, QueryList, TemplateRef, ViewChildren } from '@angular/core'; +import { Subject } from 'rxjs'; import { TemplateRefWrapper } from './template-ref-wrapper'; import { render, TemplateResult } from 'lit-html'; @@ -14,6 +15,7 @@ type TemplateFunction = (arg: any) => TemplateResult; export class TemplateWrapperComponent { public templateFunctions: TemplateFunction[] = []; + public templateRendered = new Subject(); /** * All template refs @@ -27,6 +29,7 @@ export class TemplateWrapperComponent { public litRender(container: HTMLElement, templateFunc: (arg: any) => TemplateResult, arg: any) { render(templateFunc(arg), container); + this.templateRendered.next(container); } public addTemplate(templateFunc: TemplateFunction): TemplateRef { diff --git a/projects/igniteui-angular-elements/src/index.html b/projects/igniteui-angular-elements/src/index.html index c1db7ce6a44..783773b1434 100644 --- a/projects/igniteui-angular-elements/src/index.html +++ b/projects/igniteui-angular-elements/src/index.html @@ -195,7 +195,11 @@

Flat Grid (MRL column layout)

ctx.cell.editValue = e.detail.newValue} single-select> `; - grid1.detailTemplate = (ctx) => html`
Stock: ${ctx.implicit.InStock}
`; + grid1.detailTemplate = (ctx) => { + return html`
+ +
`; + } grid2.querySelector('igc-column[field="ProductName"]').inlineEditorTemplate = (ctx) => html`