LearnNewsExamplesServices
Frontmatter
id18817
titleA landing render undoes a checkbox''s checked vnode sync, so a click is written back over
stateClosed
labels
bugaicore
assigneesneo-opus-vega
createdAtSep 17, 2026, 11:13 AM
updatedAtSep 17, 2026, 12:56 PM
githubUrlhttps://github.com/neomjs/neo/issues/18817
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 17, 2026, 12:56 PM

A landing render undoes a checkbox's checked vnode sync, so a click is written back over

neo-opus-vega
neo-opus-vega commented on Sep 17, 2026, 11:13 AM

Context

@neo-opus-grace's defect-note from 2026-09-16T13:36Z, broadcast unowned while she closed #18792 for form.field.Text: the same class lives in form.field.CheckBox, and nothing was tracking it.

Reproduced deterministically on dev (aadafbad1b), unit runtime. Two clicks with the first render held open between its payload collection and its landing:

HELD   : []
SETTLED: ["checked-sync-…__input=false"]

The settled render re-sends checked="false" that the DOM already holds. Held open, the intermediate click is visually reverted until the next render lands — invisible at normal speed, which is what makes it expensive to diagnose from the symptom.

The symptom class has cost a debugging session before. On 2026-02-25 (session 6c17bf58-3525-492f-af63-98411c45ef22) @tobiu reported "clicks no longer change the checked state at all" and wrote "i suppose the checkbox might get reset so fast on dom level, that i can not visually see it… even if we monkey-patch around this bug, the next user will encounter it sooner or later." That session attributed the failure to a state-provider binding and repaired it there. This ticket does not claim that incident was this defect — the attribution then was its own, and the engine mechanism below was not measured until now. It is cited because the reading "reset so fast on dom level" describes exactly what the reproducer shows, and because it is the reason to fix the engine rather than the consumer.

Live latest-open sweep: latest 20 open issues read at 2026-09-17T09:12Z; nothing equivalent. Targeted in:title searches on checkbox / checked / vnode sync returned empty, as did grep over resources/content/issues and resources/content/discussions for form.field.CheckBox and onInputValueChange. A2A sweep of the latest 30 messages: Grace's defect-note is the only mention and carries no lane claim. Own-assignment sweep: no overlap across my 7 open tickets.

The Problem

CheckBox#onInputValueChange writes what the DOM reports into the input's vnode so the next diff has nothing to send — the DOM already holds it:

me.vnode.childNodes[0].childNodes[me.hideLabel ? 0 : 1].attributes.checked = `${checked}`;

A render collected before that report lands after it, carrying the vnode it was diffed against. That vnode does not hold the reported state, so the sync is silently undone, and the next diff computes against a stale checked. The user's click is written back over.

This is the identical mechanism #18792 measured for Text, one field class across. Text writes attributes.value; CheckBox writes attributes.checked. Neither write survives a render that was already in the air.

The Architectural Reality

  • src/form/field/CheckBox.mjs:610-620 — onInputValueChange() performs the sync by index-walking me.vnode.childNodes[0].childNodes[me.hideLabel ? 0 : 1], then assigns me.checked.
  • src/form/field/CheckBox.mjs:9 — class CheckBox extends Field (form.field.Base), not form.field.Text. So #18798's repair is not inherited; the two fields are siblings, and this one has no resolveVdomUpdate override at all.
  • src/form/field/Text.mjs:1539 — the merged shape: resolveVdomUpdate() restores reportedInputValue into the landed vnode unless that render wrote the input's value itself (checked by scanning data.deltas for a removeNode or a value attribute on the input's id), and afterSetMounted drops it on unmount.
  • src/mixin/VdomLifecycle.mjs#resolveVdomUpdate — the landing hook both overrides chain into.
  • test/playwright/unit/form/field/InputValueSync.spec.mjs — #18798's three arms (one defect, two controls), the fixture shape this ticket's arms extend: hold VdomHelper.updateBatch open, record every render's deltas, assert on what was written.

The Fix

Mirror the merged Text repair at the sibling, adapted to checked:

  1. A reportedChecked protected member holding what the DOM last reported through onInputValueChange().
  2. A resolveVdomUpdate() override that restores it into the landed vnode, unless that render wrote the input's checked itself — same delta scan as Text, keyed on the input node rather than the id-walk, so the repair does not inherit the index-walk's fragility.
  3. Cleared when the field unmounts, as Text clears on unmount.

Not by moving CheckBox under Text, and not by lifting the repair into form.field.Base: the two fields sync different attributes through different node lookups, and a shared base method would have to know both. If a third field class turns up with the same shape, that is the moment to extract — named here so the next reader does not re-derive it.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
CheckBox#resolveVdomUpdate (new override) src/form/field/Text.mjs:1539 — the merged sibling repair restores the DOM-reported checked into the landed vnode, unless that render wrote checked for the input itself none — absent a report (reportedChecked === null) the method is a straight super call, so an untouched field is byte-unchanged override JSDoc naming the in-the-air window and what it restores, as Text's does the reproducer arm below, red on dev; plus the two controls
CheckBox#reportedChecked (new protected member) src/form/field/Text.mjs:321 — reportedInputValue the last DOM-reported checked state, held until a render writes it or the field unmounts null — the untouched state member JSDoc pointing at resolveVdomUpdate cleared-on-unmount arm

Retirement: both rows retire together if a third field class needs the same repair and it is extracted to a shared owner. Until then, two siblings with one shape each is cheaper than a base method that knows both.

Decision Record impact

none — a defect repair inside an existing component, no contract surface added beyond the two protected members above.

Acceptance Criteria

  • AC-1 — a unit arm: with one render held open between its collection and its landing, a click made inside that window leaves no render writing a checked the DOM already holds. Red on dev (measured: the settled render sends checked="false").
  • AC-2 — CONTROL: checked set in code while a render is in the air still reaches the DOM — the repair must not suppress a programmatic write.
  • AC-3 — CONTROL: when the render in the air writes checked itself, the next render restores what the user clicked, rather than the repair silently losing to it.
  • AC-4 — unmounting drops the reported state, so a remount cannot restore a stale checked into a fresh vnode.
  • AC-5 — full unit and component suites green; examples/form/field and the dashboard units unchanged.

Out of Scope

  • form.field.Radio and any other field sharing the shape — unmeasured. If one turns up, it is its own ticket and the extraction question above opens with it.
  • The index-walk in onInputValueChange itself (childNodes[0].childNodes[hideLabel ? 0 : 1]). It is fragile and it is not this defect; touching it here would mix a repair with a refactor.
  • The Text repair, merged as #18798.
  • mergedCallbackMap orphan entries — @neo-opus-grace's separate defect-note, out of scope of #18802 too.

Avoided Traps

  • ⛔ Do not fix this in the consumer. The 2026-02-25 session repaired a checkbox symptom at the binding layer and the engine mechanism survived it. A consumer-side repair here would do the same again.
  • ⛔ Do not widen Text's reportedInputValue to carry both. The name would then lie about one of its two callers, and CheckBox does not extend Text.
  • ⛔ Do not assert the reproducer by reading me.checked. The config is correct throughout — the defect is entirely in what the render writes to the DOM, so the arm must read the deltas, as #18798's do.

Related

  • #18792 / PR #18798 — the Text sibling, merged; the repair shape this mirrors
  • #18762 — the grid-native cell-editing epic where the Text instance surfaced as the CellEditingPooling:225 flake
  • @neo-opus-grace's defect-note of 2026-09-16T13:36Z — the origin

Origin Session ID: 2b78af80-54a8-4e14-afd8-85c6dfe41004

Retrieval Hint: "checkbox checked vnode sync undone by a landing render, reportedChecked resolveVdomUpdate, sibling of the Text input value repair"