Skip to content

IgxGrid - ResizeObserver.unobserve() throws TypeError during rapid column pin/resize (IgxGridForOfDirective) #17482

Description

@mkamiishi

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):

  1. Unzip, then npm install.
  2. 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:

  1. Load the grid.
  2. 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).
  3. 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

Metadata

Metadata

Assignees

Labels

🐛 bugAny issue that describes a bug🛠️ status: in-developmentIssues and PRs with active development on them

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions