Declarative bindings over an owned JavaScript model

Observed scope: WG 2.4.1.0 #977 / Cohtml 2.2.8.1. A small frontend-owned model can drive declarative text, style, class, conditional and repeated-row bindings. This is separate from Python/Wulf transactions and native proxy collections; keep its fields out of the native window.model.

  1. Check the required engine methods, reserve a unique name and publish the owned object with engine.createJSModel(name, object).
  2. Attach owned markup. The tested attributes included data-bind-value, data-bind-style-width, data-bind-class-toggle, data-bind-if and data-bind-for over object records.
  3. After changing the plain object, call engine.updateWholeModel(ownedModel), then engine.synchronizeModels(). Direct mutations alone did not refresh the tested bindings.
  4. Remove owned bound markup before engine.unregisterModel(ownedModel), then release references and owned action namespaces.

Keep structural for/if children under binding ownership. Change their source data rather than moving generated nodes manually. Resolve row actions through a stable record key; a rendered node or loop position is not durable record identity.

Bounded virtual lists and source-size changes

The tested engine.createVirtualList() pager bounded generated output from a 1,000-object array. Its startIndex and pageSize select the source slice; changing the source still requires explicit model dirtying and synchronization. Scrolling, row heights, overscan and page selection remain the feature's responsibility. A bounded row count alone proves neither faster frames nor lower memory use.

Version-sensitive failure: with an unchanged pager, shrinking a three-record source to empty left blank generated rows; refilling still produced the old row count. Another dirty/sync pass did not repair it. In the tested fixed-height controller, bounding pageSize to the smaller of the page capacity and current record count correctly handled empty/refill. Recheck this specific boundary after an engine update.

Use independent pagers for independent lists. The tested loop counter reported source indices, so adding startIndex again was wrong. Keep source index, visible slot and stable record key distinct. Remove the bound subtree and unregister the source model at teardown; no separate pager destructor was established.

Engine listener identity and connection release

In the attached #977 runtime, two engine.on registrations with the same function/context delivered twice. One exact engine.off(name, fn, context) removed one registration. A new function or a new context object with identical fields removed neither.

The returned connection's clear() was not a unique-registration token: clearing the first connection twice could consume both identical registrations. Guard initialization and release each connection at most once. Separate owners should retain distinct callback/context identities. Save a bound function rather than creating another bind() result during removal.

These were owned JavaScript events, not a guarantee about native gameplay dispatch. DOM listeners have a different duplicate/removal contract; see deferred DOM actions. Hidden-state rendering has a separate visibility gate.

Payload conversion and validation are separate steps

The Python Command call/notification path passes a non-None payload as one argument to an ordinary Event. A None payload instead means zero arguments; it is not an empty dictionary. Native proxy execution is a separate path.

args2params adapts dictionary fields to named Python parameters and mutates that dictionary in place. A conversion's ValueError or TypeError produces a warning but leaves the original field value and still calls the handler. Missing converted keys can raise before Python defaults are applied, while extra payload keys are forwarded to the handler's signature. A conversion annotation therefore does not establish that input has been rejected or validated.

Later Python listeners on the same command can see converted fields. An escaping handler error follows ordinary Event propagation without undoing earlier payload mutation. Keep accepted keys, types, ranges and current-owner checks in the command's explicit contract; real handlers can add their own domain guards. These are inspected Python wrapper and controlled-event results, not tests of native/frontend delivery or a client-validation defect.