Query Shapes FULL
The element carrying data-query decides how results render. A template child means a list. A plain subtree means a single record. No mode flags, no configuration.
List Shape
An element whose direct child is a <template> renders the result as a keyed list. This delegates to the same rendering pipeline as data-list, so everything you know about list rendering applies unchanged: row identity via data-key, minimal DOM updates on change, and event delegation.
<tbody data-query="products">
<template>
<tr>
<td data-bind="name"></td>
<td data-bind="price"></td>
</tr>
</template>
</tbody>
The key declared on the query provides row identity, so refreshed data patches rows in place instead of rebuilding them. Declare it once in the query; every element bound to that query inherits it.
Record Shape
An element without a template child treats the single result object as its subtree's binding context. A profile card, a settings panel, a detail pane. A row without a list.
Markup and declaration:
<article data-query="currentUser">
<h2 data-bind="name"></h2>
<p data-bind="title"></p>
<p><span data-bind="unreadCount"></span> unread messages</p>
<p data-bind="lastSeen"></p>
<p data-show="$currentUser.isStale">Refreshing…</p>
</article>
// The example simulates the session service in the page; against a
// real server this is from: '/api/me'.
wildflower.query('currentUser', {
from: () => session.fetchProfile(),
refresh: 'focus',
initial: [{ name: 'Loading…', title: '', unreadCount: '' }]
});
data-bind="name" resolve against the result record. Full $currentUser.* paths keep working for query state, as the stale banner shows. Refresh the profile a few times, or switch tabs and come back: messages keep arriving on the simulated service, and only the changed fields update.
Record Semantics
- A null or missing record is valid. Bound fields render empty and nothing throws. Development builds log a note when a record query resolves to null so you can tell intent from accident.
- Background failures preserve the card. A failed refresh sets
syncErrorand leaves the last good values on screen. - Seed with
initial. The record shown before the first fetch resolves comes from theinitialoption, as in the example above. - Simple paths only. The record context applies to plain dotted paths. Expressions and
$paths pass through untouched, so query state and other entities stay reachable inside the card.
Scalars and the State Surface
Queries register as entities, so the $ accessor exposes their state anywhere in the page with no extra syntax. This is how count badges, loading skeletons, and error banners attach to a query without belonging to any particular shape:
<span data-bind="$products.count"></span> products
<div data-show="$products.isLoading">Loading…</div>
<div data-show="$products.error">
Something went wrong. <button data-action="retry">Retry</button>
</div>
<div data-show="$products.isStale">Refreshing…</div>
Declaring the Expected Shape
A query pipes whatever the source sends into your bindings, and the framework's rendering rules keep drift quiet. A missing field renders empty rather than throwing, and a number that arrives as a string displays fine right up until a computed does arithmetic on it. When an API changes underneath you, the page just looks subtly wrong with nothing in the console.
data-expect is the tripwire for that moment. Declare the fields the markup depends on, with a primitive type where the type matters:
<tbody data-query="products" data-expect="id:number, name:string, price:number, tags">
<template> … </template>
</tbody>
Development builds check every batch of incoming rows against the declaration at the moment it enters the store. A declared field that is missing, or present with a different primitive type, warns once with the query name, the field, and the source that carried it, whether that was a fetch, a stream message, server-rendered adoption, or one of your own patch() calls. A token without a type checks presence alone, and a null value is treated as data rather than drift. When several elements declare the same query, the first element's data-expect is the one that applies. Production builds strip the check entirely. The attribute costs nothing and changes nothing at runtime.
data-expect detects drift. It does not coerce, transform, refine, or reject, and it never runs in production. When you need real validation with rejection and defaults, wrap the function source with a validator and keep data-expect underneath as the ambient check: from: async () => schema.parse(await fetchRows()).