Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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>('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>('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');
Expand Down Expand Up @@ -1426,6 +1495,22 @@ export class TestIgxForOfDirective<T> extends IgxForOfDirective<T> {
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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,28 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
}
}

protected subscribeToViewObserver(target: Element) {
/**
* @hidden
* Resolves the element a view is tracked by.
*
* The view's root nodes are not always elements: when the template's root is
* a control flow block they are comment anchors, and the rendered element
* follows the first of them. Both lookups can come up empty - the view may
* hold no element at all, or be torn down before its content is rendered -
* in which case there is nothing to observe and null is returned.
*/
protected getViewObservedNode(view: EmbeddedViewRef<any> | 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) {
Expand Down Expand Up @@ -1456,7 +1477,16 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
protected removeLastElem() {
const oldElem = this._embeddedViews.pop();
this.beforeViewDestroyed.emit(oldElem);
this.viewObserver?.unobserve(oldElem.rootNodes.find(node => 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();
Expand Down Expand Up @@ -1930,7 +1960,7 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> 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++;
}

Expand Down
Loading