Every view is data-bound: the layout area declares what to render — controls and node paths —
and the view binds the values on the GUI side (core: Doc/GUI/DataBinding → Templates first, data
later). So there is always a window in which the template is on screen and its data is not: a node
read, a cold NodeType compile, a /data section the owner writes after its template.
That window used to be invisible. An input drew empty and editable, a person typed into it, and
the first emission then either overwrote the keystrokes or — through FormComponentBase's stale-echo
filter — was itself rejected as an echo, so the loaded value never showed.
The rule
A view awaiting its first bound emission draws the loading shape, and its inputs are read-only.
It is decided in ONE place, BlazorView, so no view and no layout area implements it:
| Pending | every JsonPointerReference property counts from Subscribe until its FIRST emission — or its fault, which is surfaced and must not also hold the view in its loading shape, or its completion (a stream that ends will deliver nothing more). |
| Node-bound pointer | released by MeshNodeBindingExtensions.Bind's first emission. Bind always emits first — null for an absent node or field, and null again when its first-value budget lapses — so a node-bound form can never be locked. |
/data pointer |
released by the value or by the first frame in which the ENTITY the pointer lives in exists or by the interactive read budget (ReadBudget.Default, 10 s). Stream.DataBind drops nulls, so an empty member of a loaded entity never emits; waiting for the value alone would lock that input for ever. The entity is the SLOT, /data/"{id}" — the unit an owner writes in one UpdateData — whatever the depth of the data context: an item template's row context (/data/"{id}"/{index}) is a part of that slot, and a row view exists only for a row the slot already carries. |
| An entity nobody writes | is a real state, not only a slow one: an input bound by an absolute pointer to an unseeded scalar slot, which its own first edit creates, or a form whose owner forgot the seed. Nothing would ever release it, so the budget does: the view drops its loading shape and edits as it did before the gate, the value binding stays subscribed so a late write still lands, and the degradation is logged at Warning naming the pointer, the area and the entity. Seed the entity when you render (host.UpdateData(id, …)) — a relative pointer under an unseeded slot cannot be written at all (the client's JSON-patch add has no parent to add to), so that form is dead with or without the gate. |
| Skeleton | BlazorView.Class carries BlazorView.AwaitingDataClass (mw-awaiting-data) while anything is pending. Every view applies its author's class to its root, so every view draws the skeleton without markup of its own: a text-free pulse (standard-page-layout.css, still under prefers-reduced-motion) with pointer input off. Nothing to translate. |
| Read-only | FormComponentBase.Readonly is the author's flag OR the gate. And because some Fluent inputs ignore ReadOnly (checkbox, select, radio group), the Value setter also refuses an edit that arrives before the first emission. Only a PERSON's edit reaches that setter (@bind-Value, a picker's selection): the binding's own writes go to the backing field (DataBind(ViewModel.Data, x => x.data, …), SetValue), so the gate can never refuse the loaded value, however many pointers a view binds or in whatever order they release. |
| Re-bind | a parameter-driven re-bind resets the gate with the bindings it replaced; an emission from a disposed binding cannot release a slot that belongs to the new ones. Replayed streams answer inside the same BindData, so a re-bind does not flicker. |
Pointers resolved against a cascaded Model, ContextProperty values and plain values are not
pending — they are available when the view binds.
A Thumbnail needs only the path
MeshNodeThumbnailView reads the node itself (Hub.GetMeshNodeStream(path), shared per path by the
process-wide cache) and projects it with the same MeshNodeThumbnailControl.FromNode the server
uses — name, abstract (node.Description, else the content's Abstract, typed or a JSON frame)
and image — LIVE, so an edit of the node reaches a card already on screen. A Thumbnail area therefore
passes ONLY the path:
new MeshNodeThumbnailControl(path, path) // the title may simply repeat the path
Until the node arrives, the card draws skeleton lines where the title and abstract go (pulsing, and
still under prefers-reduced-motion like the platform shape) — never the bare path as if it were
the title. The window ends on the node's first answer of ANY kind: a node,
a null (absent or deleted), no answer within the interactive read budget (ReadBudget, logged at
Warning, still subscribed so a late node lands), a fault — a read the viewer may not make (#434) —
or a completion. In every case but the first the card falls back to what the control was seeded
with, so a dangling path never pulses for ever.
Pinned by
src/MeshWeaver.Blazor.Views.Test/LoadingShapeTest.cs — the real TextFieldView is read-only with
the skeleton class while its slot is unwritten and editable once the owner writes it (with the bound
member still empty); an edit before the first emission is refused and one after it reaches the owner;
a node-bound input is released by the node's emission; a path-only Thumbnail renders the node's
live description and follows an edit of it; and a Thumbnail bound to a path with no node drops its
skeleton and shows its seeded title. With the production change reverted, the three rows
about the gate and the Thumbnail fail; the node-bound row is the guard that the gate never locks.
The absence rows — nothing the gate waits on may stay away for ever: a node-bound input over a path
with NO node is released by the binding's null (content and whole-node roots; red when the release
is made conditional on a non-null value); an input whose entity the owner never writes is still
locked inside the budget, released at it, shows a write that arrives afterwards and edits through to
the owner; an input bound by an absolute pointer to an unseeded scalar slot becomes editable and its
first edit creates the slot at the owner (both red with the budget removed — the input stays
read-only for the whole wait); and a row context waits for its SLOT, not for the /data collection
(red when the entity root is cut at /data).