Context
examples/grid/cellEditing renders no rows at all. Measured on dev@d486a03a8d by loading /examples/grid/cellEditing/index.html in Chromium: 0 elements match .neo-grid-cell, and the App Worker logs
vdom update failed neo-grid-body-1 TypeError: this.getStateProvider is not a function
at Column.countryRenderer (examples/grid/cellEditing/MainContainer.mjs:34:33)
at Row.applyRendererOutput (src/grid/Row.mjs:190:42)Column.countryRenderer is the tell: the method is defined on Neo.examples.grid.cellEditing.MainContainer and reached through renderer: 'up.countryRenderer', but it runs with the column as this.
This is a regression of #6620, which fixed exactly this example:
| commit |
date |
scope the renderer is called with |
dd4572d8d5 (#6620) |
2025-04-05 |
column.rendererScope || me — me was grid.View, the caller |
f691b3faf0 (#8970, Row pooling) |
2026-02-03 |
column.rendererScope || column — reintroduced in the new grid/Row.mjs |
The Row-pooling refactor moved applyRendererOutput from grid.View into Neo.grid.Row and restored the pre-#6620 default on the way.
The Problem
Two independent losses put the ancestor's method on the column's this:
grid.column.Base#beforeSetRenderer (src/grid/column/Base.mjs:219) calls resolveCallback(value, this) and keeps only .fn:
return resolveCallback(value, this).fn || value
resolveCallback (src/util/Function.mjs:158-171) returns {fn, scope} and, for an up. name, walks the parent chain until it finds the owner — {fn: MainContainer.prototype.countryRenderer, scope: theMainContainer}. The scope it found is dropped on the floor.
grid.Row#applyRendererOutput (src/grid/Row.mjs:190) then supplies its own default:
rendererOutput = column.renderer.call(column.rendererScope || column, rendererConfig)
So a renderer that came from an ancestor is invoked with the column. Neo.grid.column.Base has no getStateProvider, and the render throws inside the body's update — which is why the grid renders nothing rather than one bad cell.
The default renderer is unaffected and must stay that way: renderer_: 'cellRenderer' (src/grid/column/Base.mjs:73) resolves on the column itself, so resolveCallback returns the column as scope, which is correct.
The Architectural Reality
| surface |
today |
src/util/Function.mjs:158 resolveCallback |
returns {fn, scope}; up. walks scope.parent until the name resolves |
src/grid/column/Base.mjs:219 beforeSetRenderer |
keeps .fn, discards .scope |
src/grid/column/Base.mjs:81 rendererScope |
{Neo.core.Base|String|null}, the designed home for a scope |
src/grid/Row.mjs:172 |
rendererScope 'me'/'this' means the column — grid.column.Component (:55) relies on it |
src/grid/Row.mjs:190/:196 |
calls renderer and extraCls with column.rendererScope || column |
src/table/Body.mjs:175 |
the Table sibling defaults to tableContainer, never the column |
src/table/header/Button.mjs:228 |
discards .scope the same way; Table's container default hides it |
The Fix
grid.column.Base#beforeSetRenderer keeps the scope it resolved. When resolveCallback returns a scope that is not the column and rendererScope is unset, the column records it, so an up. renderer runs on the instance that defines it — the whole point of up..
grid.Row#applyRendererOutput defaults to the grid Container, not the column, for both renderer and extraCls, restoring #6620's contract in today's topology and matching table/Body.mjs:175.
rendererScope: 'me' | 'this' keeps meaning the column.
- Anchor & Echo JSDoc on
rendererScope and beforeSetRenderer stating which scope wins, in which order.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
Neo.grid.column.Base#renderer_ string resolution |
src/grid/column/Base.mjs:219 |
an up. name keeps the ancestor resolveCallback found as its scope |
a name found on the column keeps the column, as today |
config JSDoc |
unit arm, red on dev |
Neo.grid.column.Base#rendererScope |
src/grid/column/Base.mjs:81 |
explicit config still wins over everything; 'me'/'this' still means the column |
unchanged |
config JSDoc |
unit arm |
Neo.grid.Row#applyRendererOutput default scope |
src/grid/Row.mjs:190, :196 |
the grid Container, as table/Body.mjs:175 does |
a column with rendererScope is unaffected |
method JSDoc |
unit arm + the example arm |
Acceptance Criteria
Out of Scope
table.header.Button#beforeSetRenderer, which drops the same scope — Table's container default hides it, and Table is a separate control. Recorded here; file it separately if a Table renderer needs its owner. · The cell-editing feature itself (#18762, #18764). · Any renderer API beyond scope resolution.
Avoided Traps
- Defaulting to the Row. The Row is a pooled projection; a renderer holding it would hold a slot, not the grid.
- Only changing the Row default. It fixes this example by accident — the container happens to have
getStateProvider — and still runs an ancestor's method with the wrong this the moment the method touches its own members.
- Resolving the string per render.
beforeSetRenderer resolves once at config time; per-render resolution would put a parent-chain walk in the pooled render path.
Related
#6620 (the regressed fix) · #6655 (string-based renderer resolution) · #8970 (the Row-pooling refactor that reintroduced the default) · #18762 · #18764 · #286 / #287 (the Table precedent)
Live latest-open sweep: latest 20 open issues at 2026-09-16T09:57Z; no equivalent. A2A claim sweep: latest 30 inbox messages, all read-states, at 2026-09-16T09:57Z; no claim on grid renderers. Memory Core rationale sweep: query_raw_memories on the symptom nouns; the hits are the 2026-03 Row performance refactors, no decision on renderer scope. Closed-issue sweep: rendererScope/"renderer scope" across all states surfaced #6620, #286, #287, #321, all closed. Own-assignment sweep: 9 open, none on grid renderers.
Origin Session ID: a758f0e1-0dc1-4e4e-a975-4824a9319487
Retrieval Hint: "grid column renderer up. string resolves the function but loses resolveCallback's scope; Row.applyRendererOutput defaults to the column since the Row-pooling refactor"
Context
examples/grid/cellEditingrenders no rows at all. Measured ondev@d486a03a8dby loading/examples/grid/cellEditing/index.htmlin Chromium:0elements match.neo-grid-cell, and the App Worker logsvdom update failed neo-grid-body-1 TypeError: this.getStateProvider is not a function at Column.countryRenderer (examples/grid/cellEditing/MainContainer.mjs:34:33) at Row.applyRendererOutput (src/grid/Row.mjs:190:42)Column.countryRendereris the tell: the method is defined onNeo.examples.grid.cellEditing.MainContainerand reached throughrenderer: 'up.countryRenderer', but it runs with the column asthis.This is a regression of #6620, which fixed exactly this example:
dd4572d8d5(#6620)column.rendererScope || me—mewasgrid.View, the callerf691b3faf0(#8970, Row pooling)column.rendererScope || column— reintroduced in the newgrid/Row.mjsThe Row-pooling refactor moved
applyRendererOutputfromgrid.ViewintoNeo.grid.Rowand restored the pre-#6620 default on the way.The Problem
Two independent losses put the ancestor's method on the column's
this:grid.column.Base#beforeSetRenderer(src/grid/column/Base.mjs:219) callsresolveCallback(value, this)and keeps only.fn:return resolveCallback(value, this).fn || valueresolveCallback(src/util/Function.mjs:158-171) returns{fn, scope}and, for anup.name, walks the parent chain until it finds the owner —{fn: MainContainer.prototype.countryRenderer, scope: theMainContainer}. The scope it found is dropped on the floor.grid.Row#applyRendererOutput(src/grid/Row.mjs:190) then supplies its own default:rendererOutput = column.renderer.call(column.rendererScope || column, rendererConfig)So a renderer that came from an ancestor is invoked with the column.
Neo.grid.column.Basehas nogetStateProvider, and the render throws inside the body's update — which is why the grid renders nothing rather than one bad cell.The default renderer is unaffected and must stay that way:
renderer_: 'cellRenderer'(src/grid/column/Base.mjs:73) resolves on the column itself, soresolveCallbackreturns the column as scope, which is correct.The Architectural Reality
src/util/Function.mjs:158resolveCallback{fn, scope};up.walksscope.parentuntil the name resolvessrc/grid/column/Base.mjs:219beforeSetRenderer.fn, discards.scopesrc/grid/column/Base.mjs:81rendererScope{Neo.core.Base|String|null}, the designed home for a scopesrc/grid/Row.mjs:172rendererScope'me'/'this'means the column —grid.column.Component(:55) relies on itsrc/grid/Row.mjs:190/:196rendererandextraClswithcolumn.rendererScope || columnsrc/table/Body.mjs:175tableContainer, never the columnsrc/table/header/Button.mjs:228.scopethe same way; Table's container default hides itThe Fix
grid.column.Base#beforeSetRendererkeeps the scope it resolved. WhenresolveCallbackreturns a scope that is not the column andrendererScopeis unset, the column records it, so anup.renderer runs on the instance that defines it — the whole point ofup..grid.Row#applyRendererOutputdefaults to the grid Container, not the column, for bothrendererandextraCls, restoring #6620's contract in today's topology and matchingtable/Body.mjs:175.rendererScope: 'me' | 'this'keeps meaning the column.rendererScopeandbeforeSetRendererstating which scope wins, in which order.Contract Ledger
Neo.grid.column.Base#renderer_string resolutionsrc/grid/column/Base.mjs:219up.name keeps the ancestorresolveCallbackfound as its scopedevNeo.grid.column.Base#rendererScopesrc/grid/column/Base.mjs:81'me'/'this'still means the columnNeo.grid.Row#applyRendererOutputdefault scopesrc/grid/Row.mjs:190,:196table/Body.mjs:175doesrendererScopeis unaffectedAcceptance Criteria
renderer: 'up.someMethod', wheresomeMethodlives on an ancestor, invokes it with that ancestor asthis(red ondev).rendererand norendererScopeinvokes it with the grid Container asthis(red ondev).rendererScope: 'me'and'this'still invoke with the column, and the defaultcellRendererstill resolves on the column:grid.column.Componentkeeps rendering.examples/grid/cellEditingrenders its rows with no page error and no App Worker error (red ondev: 0 cells,this.getStateProvider is not a function).cellClsfollows the same scope rule asrenderer(src/grid/Row.mjs:196applies the same default).Out of Scope
table.header.Button#beforeSetRenderer, which drops the same scope — Table's container default hides it, and Table is a separate control. Recorded here; file it separately if a Table renderer needs its owner. · The cell-editing feature itself (#18762, #18764). · Any renderer API beyond scope resolution.Avoided Traps
getStateProvider— and still runs an ancestor's method with the wrongthisthe moment the method touches its own members.beforeSetRendererresolves once at config time; per-render resolution would put a parent-chain walk in the pooled render path.Related
#6620 (the regressed fix) · #6655 (string-based renderer resolution) · #8970 (the Row-pooling refactor that reintroduced the default) · #18762 · #18764 · #286 / #287 (the Table precedent)
Live latest-open sweep: latest 20 open issues at 2026-09-16T09:57Z; no equivalent. A2A claim sweep: latest 30 inbox messages, all read-states, at 2026-09-16T09:57Z; no claim on grid renderers. Memory Core rationale sweep:
query_raw_memorieson the symptom nouns; the hits are the 2026-03 Row performance refactors, no decision on renderer scope. Closed-issue sweep:rendererScope/"renderer scope" across all states surfaced #6620, #286, #287, #321, all closed. Own-assignment sweep: 9 open, none on grid renderers.Origin Session ID: a758f0e1-0dc1-4e4e-a975-4824a9319487
Retrieval Hint: "grid column renderer up. string resolves the function but loses resolveCallback's scope; Row.applyRendererOutput defaults to the column since the Row-pooling refactor"