Pooled Grid Animation Contract
Neo grids virtualize rows by recycling a fixed DOM pool. That is the right default for scrolling, but it changes the animation contract: row transitions must not assume that logical records own stable DOM rows.
This guide records the design contract for animating TreeGrid structural changes under pooled rows. It is intentionally a contract first, not visual tuning. Easing, duration, and stagger only matter after the renderer and data-layer invariants are preserved.
Decision
Use a snapshot overlay for TreeGrid expand and collapse animation.
The TreeStore keeps its current synchronous projection semantics. expand() and
collapse() continue to mutate the visible flat projection immediately, and bulk
operations continue to rebuild the projection and fire one load event. The grid body
then renders the final state through the normal pooled row layer, while a temporary
overlay owns the visual transition for the affected viewport rows.
The overlay is a visual artifact, not a second data source. It captures before and after row geometry, paints only during the transition window, and is removed once the animation finishes or is cancelled.
Snapshot overlay is accepted because it keeps data truth and visual continuity separate: the store remains immediately correct, the pooled row layer remains the only live interactive layer, and animation cancellation can discard a stale overlay without rolling back data or replaying queued mutations.
Source Contracts
| Contract | Source | Required behavior |
|---|---|---|
| Fixed row pool | src/grid/Body.mjs#createRowPool() |
VDOM children remain aligned to the full row pool. Animation must not remove, append, or reorder pooled row VDOM nodes. |
| Modulo row identity | src/grid/Body.mjs#getRowId() |
Physical row ids are slot ids, not record ids. Tree structural animation must not key movement by pooled row ids. |
| View projection | src/data/TreeStore.mjs#expand() and src/data/TreeStore.mjs#collapse() |
Projection mutation stays immediate for ordinary TreeStore use. No async-only TreeStore path is introduced. |
| Bulk projection | src/data/TreeStore.mjs#expandAll() and src/data/TreeStore.mjs#collapseAll() |
Large projection rebuilds may fall back to no animation or a bounded viewport overlay. They must not emit per-node splice storms. |
| Existing plugin | src/grid/plugin/AnimateRows.mjs |
AnimateRows remains a flat-store row sorting/filtering plugin until it is repaired behind the shared pooled-grid contract. |
Rejected Alternatives
Delayed permutation
Rejected for the first TreeGrid substrate.
Delayed permutation would expose structural mutation intent, animate the visual gap, and
commit the TreeStore splice after the transition. That makes an ordinary data operation
depend on animation lifecycle timing. It also adds queueing questions for selection,
filters, sorting, async child loading, singleExpand, rapid toggles, and bulk
expandAll() / collapseAll(). The existing TreeStore contract is cleaner: the data
layer commits the truth immediately, and the view decides how much of that change can be
animated.
Current AnimateRows enablement
Rejected for TreeGrid structural changes.
AnimateRows maps VDOM rows by row.id, pushes inserted VDOM rows, fades leftovers, and
later forces owner.createViewData(). That can work for flat row sorting, but it fights
the current grid body contract where row.id is a modulo pool slot. Enabling it for
TreeGrid expand/collapse would create a second row lifecycle over a renderer that
intentionally keeps the pool stable.
Global transform transitions
Rejected.
Normal virtual scrolling updates pooled rows with transforms. A global
transition: transform on grid rows would animate ordinary scroll recycling, making
scrolling lag behind the user's input. Structural animation must be explicitly scoped to
the overlay or to a future animation layer that can distinguish scroll movement from
TreeStore projection changes.
Snapshot Overlay Requirements
- Capture the visible viewport before the TreeStore mutation. The capture keys logical records through record identity, not pooled row ids.
- Let the TreeStore mutation commit immediately.
- Render the grid body in its final state through
createViewData(). - Capture the post-mutation viewport geometry.
- Animate an overlay for rows that entered, exited, or moved within the affected viewport.
- Remove the overlay on transition end, scroll, store load/filter/sort, resize, or any subsequent structural mutation that would make the snapshot stale.
The overlay must not intercept selection, focus, or row toggle interaction. During an active transition, user input continues to target the live pooled row layer.
Guardrails
- TreeGrid must not add
.neo-tree-row-entering/.neo-tree-row-leavingas a parallel row lifecycle over pooled rows. animatedRowSortingremains off by default for TreeGrid.- Bulk expand/collapse may use a bounded viewport overlay or skip animation entirely. The fallback must be explicit and deterministic.
- Rapid toggle, scroll, resize, load, filter, and sort cancel the current overlay before the next render decision.
- Async child loading animates only after children are available and the projection changes. Loading state itself is outside this contract.
- Selection is validated by record identity after the structural change, not by physical row slot.
Verification Contract
The first runtime PR that implements this contract must verify the following paths with the chosen animation config enabled:
test/playwright/e2e/GridTree.spec.mjstest/playwright/e2e/GridTreeBigData.spec.mjs
The verification must cover:
- single expand and collapse
- mixed expand, collapse, and sort sequences
- bulk expand all followed by individual collapse
- bulk collapse all
- selection persistence after structural changes
- rapid toggle or explicit cancellation behavior
- scroll during an active transition or explicit cancellation behavior
If a behavior is intentionally non-animated, the test should assert the fallback contract rather than only asserting the final visual state.
Implementation Notes
Future data-layer hooks for mutation intent or commit timing must include JSDoc and
@summary coverage. The preferred path does not require such hooks for the first slice:
the grid can treat TreeStore projection changes as immediately committed data and keep
animation lifecycle inside the view layer.
The implementation should also narrow or repair AnimateRows before exposing it to
TreeGrid. A safe narrow path is to keep it documented as a flat-store row sorting plugin
until the shared overlay layer exists.