LearnNewsExamplesServices
Frontmatter
id19772
titleLocked grid bodies and header toolbars ignore the grid's configs
stateClosed
labels
bugaigrid
assigneesneo-opus-ada
createdAtOct 15, 2026, 8:59 PM
updatedAtOct 16, 2026, 1:15 AM
githubUrlhttps://github.com/neomjs/neo/issues/19772
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 16, 2026, 1:15 AM
milestonev13.3

Locked grid bodies and header toolbars ignore the grid's configs

neo-opus-ada
neo-opus-ada commented on Oct 15, 2026, 8:59 PM

Context

Found while testing #6858 (AnimateRows) on a grid with a locked column: the locked body never loads the plugin, so its rows cannot move with the center body's.

The Problem

Measured on dev at 7f0856c106 in the unit harness, on a grid with a locked id column, body: {animatedRowSorting: true, highlightModifiedCells: true} and headerToolbar: {showHeaderFilters: true}:

Instance animatedRowSorting highlightModifiedCells showHeaderFilters
center body true true
locked start body false false
center header toolbar true
locked start header toolbar false

So in every grid with locked columns, the locked regions ignore the grid's body and headerToolbar configs: locked columns get no header filters, no sorting options (sortable, useTriStateSorting), no row animation, no modified-cell highlighting, and no other setting passed there.

The Architectural Reality

  • src/grid/Container.mjs#createOrUpdateSubGrids builds bodyStart and bodyEnd from ...me.body.initialConfig, and src/grid/header/Wrapper.mjs builds headerStart and headerEnd from ...headerToolbar.initialConfig. Both came with the multi-body layout (#9487, 2026-03-31).
  • initialConfig is assigned nowhere in src. Core records an instance's construction config as originalConfig (src/core/Base.mjs, Neo.clone(config, true, true)), so each spread is of undefined.
  • The center body's originalConfig holds the merged grid defaults and user keys (parentId, store, theme, animatedRowSorting, highlightModifiedCells in the probe); the header toolbar's holds parentId, showHeaderFilters, sortable, theme, useTriStateSorting. The keys after each spread already override the instance-specific ones (parentId, store, theme, gridContainer, windowId, flex, layoutLock, items).

The Fix

Build each locked instance from the center instance's originalConfig, without its ids. Neo.cloneNeoInstance already does exactly that, so it takes an optional second argument for the configs the region owns, and both sites call it (this replaces the first prescription, an inline spread of originalConfig). Test each surface: a locked body inherits a body config value, a locked header toolbar inherits a headerToolbar config value, and, deferred from #6858, a locked body moves its rows with the center body under animatedRowSorting.

Contract Ledger

Target Surface Source of Authority Behavior Fallback / Edge Case Docs Evidence
Neo.cloneNeoInstance(instance, config={}) (src/Neo.mjs). The config argument is new; the helper had one argument. This ticket Creates an instance of instance.className, so a subclass stays a subclass, from a copy of instance.originalConfig without _id and id, with config spread over it. A key in config replaces the original value whole (no deep merge), and a caller may pass any key on purpose, id included. originalConfig is the construction snapshot core.Base#mergeConfig records (Neo.clone(config, true, true)): the configs the instance was created with, not its live state. A value set after construction does not carry over, and nothing reaches the clone later. Nested objects and arrays in the snapshot are copies; Neo instances in it are shared, so a clone that needs its own passes it in config. An instance without originalConfig (preventOriginalConfig) clones from config alone. JSDoc of cloneNeoInstance LockedSubGridConfigs.spec.mjs › "Neo.cloneNeoInstance applies the given configs over the original ones and drops the id"
Neo.clone(instance) (src/Neo.mjs, cloneMap.NeoInstance) Existing Unchanged: calls cloneNeoInstance(instance) without configs, so config is {}. Pre-existing since #6951 and unchanged here: Neo.clone hands NeoInstance its deep flag where that expects ignoreNeoInstances. So Neo.clone(instance, true) returns the same instance and Neo.clone(instance, false, true) a new one, the reverse of the docs; the snapshot call (true, true) shares instances either way. — The same test clones through Neo.clone(instance)
bodyStart, bodyEnd (grid.Container#createOrUpdateSubGrids) #9487 (multi-body), this ticket A locked body clones the center body (me.body): its class, and the configs the container created it with (the grid's body config over the container's defaults). The region sets its own flex: 'none', gridContainer, parentId (the view), reference: null, rowHeight, store, theme, useInternalId and windowId. Cloned when its region gets a locked column, and kept while it has one. A region that empties destroys its body; its next locked column clones a new one from the same snapshot. A later set() on the center body reaches no locked body (Out of Scope). Inline comment AC-1, AC-3 and AC-4 arms
headerStart, headerEnd (grid.header.Wrapper#updateHeaders) #9487, this ticket A locked header toolbar clones the center toolbar (headerToolbar): its class, and the configs it was created with (the grid's headerToolbar config). The region sets its own flex: 'none', gridContainer, items (its locked buttons), layoutLock, parentId (the wrapper), reference: null, theme and windowId. Cloned when its region gets a locked column; an existing locked toolbar takes new buttons through items and createItems(). A region that empties moves its buttons, then destroys its toolbar. Same construction-only boundary. Inline comment AC-2 and AC-3 arms

Acceptance Criteria

  • AC-1 With locked start and end columns, bodyStart and bodyEnd carry the grid's body config values (pinned: animatedRowSorting, highlightModifiedCells).
  • AC-2 headerStart and headerEnd carry the grid's headerToolbar config values (pinned: showHeaderFilters, sortable, useTriStateSorting).
  • AC-3 No instance-specific key carries over: ids stay unique, and parentId, store and theme come from the explicit keys.
  • AC-4 With animatedRowSorting and a locked column, every record's row sits at the same offset in the locked and the center body at both steps of a move.
  • AC-5 The unit, component and e2e suites pass.

Evidence for each: #19777 at 7c07b98906, its AC Evidence table. AC-4 also holds in the browser: examples/grid/animatedRowSorting locks its Id column, and AnimatedRowSorting.spec.mjs fails on dev's source (the locked row jumps while the center row animates) and passes with the fix.

Out of Scope

  • Changing the center body's or header toolbar's configs at runtime: the locked instances do not follow a later set(), before or after this fix.
  • Neo.clone's ignoreNeoInstances argument (Contract Ledger, second row): every deep clone relies on keeping nested instances, so a fix is its own decision.

Related

#9487 (introduced the spread) · #6858 (where it surfaced) · #9486 (Grid Multi-Body) · #6951 (Neo.cloneMap)

Sweeps at 2026-10-15 18:58Z: the latest 20 open issues and the searches "locked header filters", "initialConfig" and "locked body config" hold no equivalent (only #12758, closed and unrelated). A2A since 18:00Z holds no claim on it. Memory Core ("locked columns lose the grid body config, header filters missing on locked columns") holds no prior decision. My open assignments do not overlap.

Origin Session ID: 8779ddeb-f661-411b-b93d-c3b6c2158b11

Retrieval Hint: "locked body initialConfig originalConfig", "locked columns lose header filters", "grid multi-body config propagation", "cloneNeoInstance override contract"

tobiu referenced in commit 5572021 - "docs(release): the 13.3.0 notes' iteration 5, animated row sorting and locked columns (#19782) (#19783) on Oct 16, 2026, 1:14 AM
tobiu referenced in commit 0659150 - "fix(grid): locked bodies and header toolbars clone their center instances (#19772) (#19777) on Oct 16, 2026, 1:15 AM
tobiu closed this issue on Oct 16, 2026, 1:15 AM