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
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"
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
devat7f0856c106in the unit harness, on a grid with a lockedidcolumn,body: {animatedRowSorting: true, highlightModifiedCells: true}andheaderToolbar: {showHeaderFilters: true}:animatedRowSortinghighlightModifiedCellsshowHeaderFiltersSo in every grid with locked columns, the locked regions ignore the grid's
bodyandheaderToolbarconfigs: 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#createOrUpdateSubGridsbuildsbodyStartandbodyEndfrom...me.body.initialConfig, andsrc/grid/header/Wrapper.mjsbuildsheaderStartandheaderEndfrom...headerToolbar.initialConfig. Both came with the multi-body layout (#9487, 2026-03-31).initialConfigis assigned nowhere insrc. Core records an instance's construction config asoriginalConfig(src/core/Base.mjs,Neo.clone(config, true, true)), so each spread is ofundefined.originalConfigholds the merged grid defaults and user keys (parentId,store,theme,animatedRowSorting,highlightModifiedCellsin the probe); the header toolbar's holdsparentId,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.cloneNeoInstancealready 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 oforiginalConfig). Test each surface: a locked body inherits abodyconfig value, a locked header toolbar inherits aheaderToolbarconfig value, and, deferred from #6858, a locked body moves its rows with the center body underanimatedRowSorting.Contract Ledger
Neo.cloneNeoInstance(instance, config={})(src/Neo.mjs). Theconfigargument is new; the helper had one argument.instance.className, so a subclass stays a subclass, from a copy ofinstance.originalConfigwithout_idandid, withconfigspread over it. A key inconfigreplaces the original value whole (no deep merge), and a caller may pass any key on purpose,idincluded.originalConfigis the construction snapshotcore.Base#mergeConfigrecords (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 inconfig. An instance withoutoriginalConfig(preventOriginalConfig) clones fromconfigalone.cloneNeoInstanceLockedSubGridConfigs.spec.mjs› "Neo.cloneNeoInstance applies the given configs over the original ones and drops the id"Neo.clone(instance)(src/Neo.mjs,cloneMap.NeoInstance)cloneNeoInstance(instance)without configs, soconfigis{}.Neo.clonehandsNeoInstanceitsdeepflag where that expectsignoreNeoInstances. SoNeo.clone(instance, true)returns the same instance andNeo.clone(instance, false, true)a new one, the reverse of the docs; the snapshot call (true, true) shares instances either way.Neo.clone(instance)bodyStart,bodyEnd(grid.Container#createOrUpdateSubGrids)me.body): its class, and the configs the container created it with (the grid'sbodyconfig over the container's defaults). The region sets its ownflex: 'none',gridContainer,parentId(the view),reference: null,rowHeight,store,theme,useInternalIdandwindowId.set()on the center body reaches no locked body (Out of Scope).headerStart,headerEnd(grid.header.Wrapper#updateHeaders)headerToolbar): its class, and the configs it was created with (the grid'sheaderToolbarconfig). The region sets its ownflex: 'none',gridContainer,items(its locked buttons),layoutLock,parentId(the wrapper),reference: null,themeandwindowId.itemsandcreateItems(). A region that empties moves its buttons, then destroys its toolbar. Same construction-only boundary.Acceptance Criteria
bodyStartandbodyEndcarry the grid'sbodyconfig values (pinned:animatedRowSorting,highlightModifiedCells).headerStartandheaderEndcarry the grid'sheaderToolbarconfig values (pinned:showHeaderFilters,sortable,useTriStateSorting).parentId,storeandthemecome from the explicit keys.animatedRowSortingand 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.Evidence for each: #19777 at
7c07b98906, its AC Evidence table. AC-4 also holds in the browser:examples/grid/animatedRowSortinglocks its Id column, andAnimatedRowSorting.spec.mjsfails ondev's source (the locked row jumps while the center row animates) and passes with the fix.Out of Scope
set(), before or after this fix.Neo.clone'signoreNeoInstancesargument (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"