diff --git a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts index 81d87eab2fc..d6c252db2cb 100644 --- a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts @@ -379,6 +379,75 @@ describe('IgxForOf directive -', () => { node.remove(); }); + it('should resolve the observed node from a comment-rooted view', () => { + const virtualContainer = fix.componentInstance.parentVirtDir; + const anchor = document.createComment('container'); + const element = document.createElement('div'); + fix.nativeElement.appendChild(anchor); + fix.nativeElement.appendChild(element); + + // A control flow root leaves only comment anchors among the root + // nodes; the rendered element follows the first of them. + expect(virtualContainer.testGetViewObservedNode({ rootNodes: [anchor] })).toBe(element); + + anchor.remove(); + element.remove(); + }); + + it('should resolve no observed node when the view holds no element', () => { + const virtualContainer = fix.componentInstance.parentVirtDir; + const orphan = document.createComment('container'); + fix.nativeElement.appendChild(orphan); + + // Nothing follows the anchor - the view was torn down before its + // content rendered. + expect(virtualContainer.testGetViewObservedNode({ rootNodes: [orphan] })).toBeNull(); + expect(virtualContainer.testGetViewObservedNode({ rootNodes: [] })).toBeNull(); + expect(virtualContainer.testGetViewObservedNode(null)).toBeNull(); + + orphan.remove(); + }); + + it('should not unobserve a view that resolves to no element', () => { + const virtualContainer = fix.componentInstance.parentVirtDir; + const observer = jasmine.createSpyObj('ResizeObserver', [ + 'observe', + 'unobserve', + 'disconnect' + ]); + virtualContainer.testSetViewObserver(observer); + + // A comment anchor with no element after it - ResizeObserver.unobserve() + // throws on anything that is not an Element. + const orphan = document.createComment('container'); + virtualContainer.testSetEmbeddedViews([{ rootNodes: [orphan], destroy: () => { } }]); + + expect(() => virtualContainer.testRemoveLastElem()).not.toThrow(); + expect(observer.unobserve).not.toHaveBeenCalled(); + }); + + it('should unobserve the element that follows a comment-rooted view', () => { + const virtualContainer = fix.componentInstance.parentVirtDir; + const observer = jasmine.createSpyObj('ResizeObserver', [ + 'observe', + 'unobserve', + 'disconnect' + ]); + virtualContainer.testSetViewObserver(observer); + + const anchor = document.createComment('container'); + const element = document.createElement('div'); + fix.nativeElement.appendChild(anchor); + fix.nativeElement.appendChild(element); + virtualContainer.testSetEmbeddedViews([{ rootNodes: [anchor], destroy: () => { } }]); + + virtualContainer.testRemoveLastElem(); + expect(observer.unobserve).toHaveBeenCalledWith(element); + + anchor.remove(); + element.remove(); + }); + it('should preserve valid border sizes when another side cannot be parsed', () => { const virtualContainer = fix.componentInstance.parentVirtDir; const node = document.createElement('div'); @@ -1426,6 +1495,22 @@ export class TestIgxForOfDirective extends IgxForOfDirective { public testGetBorder(node: Element, dimension: string): number { return super.getBorder(node, dimension); } + + public testGetViewObservedNode(view: any): Element | null { + return super.getViewObservedNode(view); + } + + public testRemoveLastElem(): void { + super.removeLastElem(); + } + + public testSetEmbeddedViews(views: any[]): void { + this._embeddedViews = views; + } + + public testSetViewObserver(observer: ResizeObserver): void { + this.viewObserver = observer; + } } /** Empty virtualized component */ diff --git a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts index b1ce3e59401..9bc1e8cfb19 100644 --- a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts @@ -523,7 +523,28 @@ export class IgxForOfDirective extends IgxForOfToken | null | undefined): Element | null { + const rootNodes = view?.rootNodes ?? []; + const elementNode = rootNodes.find(node=> node?.nodeType === Node.ELEMENT_NODE); + const candidate = elementNode ?? rootNodes[0]?.nextElementSibling; + return candidate?.nodeType === Node.ELEMENT_NODE ? candidate : null; + } + + protected subscribeToViewObserver(target: Element | null) { + if (!target) { + return; + } + if (this.igxForScrollOrientation === 'vertical' && this.viewObserver) { this._zone.runOutsideAngular(() => { if (this.platformUtil.isBrowser) { @@ -1456,7 +1477,16 @@ export class IgxForOfDirective extends IgxForOfToken node.nodeType === Node.ELEMENT_NODE) || oldElem.rootNodes[0].nextElementSibling); + // The observed node is not necessarily a root node of the view: when the + // template's root is a control flow block the root nodes are comment + // anchors, and the rendered element follows the first of them. Either + // lookup can come up empty - the view may hold no element at all, or be + // torn down before its content is rendered - and ResizeObserver throws + // on anything that is not an Element. + const observedNode = this.getViewObservedNode(oldElem); + if (observedNode) { + this.viewObserver?.unobserve(observedNode); + } // also detach from ViewContainerRef to make absolutely sure this is removed from the view container. this.dc.instance._vcr.detach(this.dc.instance._vcr.length - 1); oldElem.destroy(); @@ -1930,7 +1960,7 @@ export class IgxGridForOfDirective extends IgxForOfDirec ); this._embeddedViews.push(embeddedView); - this.subscribeToViewObserver(embeddedView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE) || embeddedView.rootNodes[0].nextElementSibling); + this.subscribeToViewObserver(this.getViewObservedNode(embeddedView)); this.state.chunkSize++; }