Description
IgxGridForOfDirective's virtualization logic can throw an uncaught TypeError when rapidly pinning and resizing grid columns:
TypeError: Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element'.
The error originates from ResizeObserver.prototype.unobserve()/observe() being called with null because of an unsafe DOM-lookup fallback used to find the "anchor" element of a virtualized embedded view.
Environment
@infragistics/igniteui-angular: 21.2.11 (bug also present in 21.2.13 and 22.0.5 — see "Versions checked" below)
@angular/core: ^21.2.0
- Node:
v24.15.0
- npm:
11.12.1
- Browser: Chromium (Playwright
1.62.0), headless
- OS: Windows 11 Pro
Versions checked
The same unsafe pattern (rootNodes.find(... ELEMENT_NODE) || rootNodes[0].nextElementSibling, passed straight into ResizeObserver.observe/unobserve with no null check) is present, unchanged, in:
21.2.11 (installed) — fesm2022/infragistics-igniteui-angular-directives.mjs:4945, :5391
21.2.13 (latest 21.2.x patch) — same file, :4937, :5379 (plus two more occurrences at :4556, :4577)
22.0.5 (latest major) — same file, :4941, :5384 (plus :4560, :4581)
So upgrading within 21.2.x or to 22.x does not resolve it.
Root cause analysis
In fesm2022/infragistics-igniteui-angular-directives.mjs, the row-virtualization directive resolves the DOM element to observe/unobserve like this (line numbers from 21.2.11):
// removeLastElem() — line 4942
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 // <-- unchecked fallback
);
this.dc.instance._vcr.detach(this.dc.instance._vcr.length - 1);
oldElem.destroy();
this._embeddedViewSizesCache.delete(oldElem);
this.state.chunkSize--;
}
// addLastElem() (row-specific override) — line 5380
addLastElem() {
...
const embeddedView = this.dc.instance._vcr.createEmbeddedView(...);
this._embeddedViews.push(embeddedView);
this.subscribeToViewObserver(
embeddedView.rootNodes.find(node => node.nodeType === Node.ELEMENT_NODE)
|| embeddedView.rootNodes[0].nextElementSibling // <-- same pattern
);
this.state.chunkSize++;
}
If an embedded view's rootNodes contains no Node.ELEMENT_NODE (e.g. its root is currently a comment placeholder), the code falls back to rootNodes[0].nextElementSibling. That fallback is only safe if rootNodes[0] is, at that exact moment, attached to a live parent in the DOM. Under rapid, successive pin/resize interactions, applyChunkSizeChange() can run several times in close succession (each pin toggle or resize can flip horizontal-scrollbar visibility, which changes the vertical container size and re-triggers row chunk-size recalculation via ngOnChanges → _recalcOnContainerChange). When removeLastElem/addLastElem run against a view whose root node is momentarily detached (no parent) — e.g. mid-way through a previous _vcr.detach()/createEmbeddedView() cycle — nextElementSibling evaluates to null.
Passing null to ResizeObserver.prototype.observe()/unobserve() fails the browser's IDL Element type check and throws the TypeError seen above.
Framework
Angular
Component / Area
Grid
Steps to Reproduce
A minimal, runnable Angular + Playwright project is attached (example-app-repro.zip):
- Unzip, then
npm install.
npx playwright test (this spins up ng serve automatically per playwright.config.ts and runs e2e/repro-resize.spec.ts).
(It may take 3+ min to reproduce the issue.
Try again a few times if it's not reproduced at the first attempt.)
The grid itself (src/app/sample-grid/sample-grid.ts) is a standalone igx-grid bound to 30 columns / 50 rows, all columns resizable (resizable="true") and pinnable via a header template. The component has no logic beyond column/data setup and a pin()/unpin() toggle — there is no columnResized/columnPinned handler and no other app-level reaction to resize/pin events.
e2e/repro-resize.spec.ts drives the repro:
- Load the grid.
- Repeatedly, in tight succession (tens of ms apart):
- Toggle pin state on 3 columns (a randomly chosen, shifting range each round).
- Drag-resize a few randomly chosen columns back and forth (to avoid cumulative width growth).
- Within a handful of rounds, the console logs the
TypeError above (or it surfaces as an uncaught pageerror), and the Playwright test asserts on it.
Actual Result
An uncaught TypeError is thrown from inside IgxGridForOfDirective:
TypeError: Failed to execute 'unobserve' on 'ResizeObserver': parameter 1 is not of type 'Element'.
at _IgxGridForOfDirective.removeLastElem
at _IgxGridForOfDirective.applyChunkSizeChange
at _IgxGridForOfDirective._recalcOnContainerChange
at _IgxGridForOfDirective.ngOnChanges
at _IgxGridForOfDirective.rememberChangeHistoryAndInvokeOnChangesHook
at callHookInternal
at callHook
at callHooks
at executeCheckHooks
at selectIndexInternal
Expected Result
Pinning and resizing columns repeatedly should never throw an uncaught error; virtualization bookkeeping (ResizeObserver.observe/unobserve) should only ever receive a valid Element, or should skip the call when no valid element is available.
Attachments
example-app-repro.zip
Description
IgxGridForOfDirective's virtualization logic can throw an uncaughtTypeErrorwhen rapidly pinning and resizing grid columns:The error originates from
ResizeObserver.prototype.unobserve()/observe()being called withnullbecause of an unsafe DOM-lookup fallback used to find the "anchor" element of a virtualized embedded view.Environment
@infragistics/igniteui-angular:21.2.11(bug also present in21.2.13and22.0.5— see "Versions checked" below)@angular/core:^21.2.0v24.15.011.12.11.62.0), headlessVersions checked
The same unsafe pattern (
rootNodes.find(... ELEMENT_NODE) || rootNodes[0].nextElementSibling, passed straight intoResizeObserver.observe/unobservewith no null check) is present, unchanged, in:21.2.11(installed) —fesm2022/infragistics-igniteui-angular-directives.mjs:4945,:539121.2.13(latest 21.2.x patch) — same file,:4937,:5379(plus two more occurrences at:4556,:4577)22.0.5(latest major) — same file,:4941,:5384(plus:4560,:4581)So upgrading within 21.2.x or to 22.x does not resolve it.
Root cause analysis
In
fesm2022/infragistics-igniteui-angular-directives.mjs, the row-virtualization directive resolves the DOM element to observe/unobserve like this (line numbers from21.2.11):If an embedded view's
rootNodescontains noNode.ELEMENT_NODE(e.g. its root is currently a comment placeholder), the code falls back torootNodes[0].nextElementSibling. That fallback is only safe ifrootNodes[0]is, at that exact moment, attached to a live parent in the DOM. Under rapid, successive pin/resize interactions,applyChunkSizeChange()can run several times in close succession (each pin toggle or resize can flip horizontal-scrollbar visibility, which changes the vertical container size and re-triggers row chunk-size recalculation viangOnChanges→_recalcOnContainerChange). WhenremoveLastElem/addLastElemrun against a view whose root node is momentarily detached (no parent) — e.g. mid-way through a previous_vcr.detach()/createEmbeddedView()cycle —nextElementSiblingevaluates tonull.Passing
nulltoResizeObserver.prototype.observe()/unobserve()fails the browser's IDLElementtype check and throws theTypeErrorseen above.Framework
Angular
Component / Area
Grid
Steps to Reproduce
A minimal, runnable Angular + Playwright project is attached (
example-app-repro.zip):npm install.npx playwright test(this spins upng serveautomatically perplaywright.config.tsand runse2e/repro-resize.spec.ts).(It may take 3+ min to reproduce the issue.
Try again a few times if it's not reproduced at the first attempt.)
The grid itself (
src/app/sample-grid/sample-grid.ts) is a standaloneigx-gridbound to 30 columns / 50 rows, all columns resizable (resizable="true") and pinnable via a header template. The component has no logic beyond column/data setup and apin()/unpin()toggle — there is nocolumnResized/columnPinnedhandler and no other app-level reaction to resize/pin events.e2e/repro-resize.spec.tsdrives the repro:TypeErrorabove (or it surfaces as an uncaughtpageerror), and the Playwright test asserts on it.Actual Result
An uncaught
TypeErroris thrown from insideIgxGridForOfDirective:Expected Result
Pinning and resizing columns repeatedly should never throw an uncaught error; virtualization bookkeeping (
ResizeObserver.observe/unobserve) should only ever receive a validElement, or should skip the call when no valid element is available.Attachments
example-app-repro.zip