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. There are no mode flags to set.

Key Concept: the element structure you would write for static content is the structure that receives the live data. There is nothing extra to declare.

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 list rendering behaves exactly as it does elsewhere, including 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. Use it for a profile card, a settings panel, or a detail pane.

Live Example A profile card bound to one record Open Full Example ↗
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: '' }]
});
Inside the record element, plain binding paths like 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 syncError and leaves the last good values on screen.
  • Seed with initial. The record shown before the first fetch resolves comes from the initial option, as in the example above.
  • The row is in scope for the whole subtree. Bindings resolve against the row merged over component state, the same way a data-list template resolves against its item, so a bare field name works in a plain path, inside an expression, and in data-bind-attr, data-bind-class, data-bind-style, and data-show alike.
  • The row wins a name collision. When the row and the component both carry title, a binding in the card reads the row's, matching how a list item shadows component state. Names the row does not carry still resolve against the component.
  • Other entities stay reachable. $ paths pass through untouched, so query state and other stores work inside the card as they do anywhere else. A name that neither the row nor the component provides renders empty, and development builds warn with WF-997.

Scalars and the State Surface

Queries register as entities, so the $ accessor exposes their state anywhere in the page with no extra syntax. A count badge, a loading skeleton, or an error banner can read a query without being the element that renders its rows:

<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>
One query, several views. A single query can render a list in one component, a count in the header, and a loading banner somewhere else entirely. They all read the same entity and update on the same boundary.

Declaring the Expected Shape

A query passes whatever the source sends straight to your bindings, and the rendering rules hide the mismatch. 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 looks subtly wrong with nothing in the console.

data-expect is what catches that. 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.

Live Example Catching an API change the moment it arrives Open Full Example ↗
This example loads the development build so the warning can fire, and mirrors the console onto the page. Drift the payload and the console names the broken field while the page is still rendering something plausible. That is the case data-expect exists to catch.
A tripwire, not a validator. 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 in place, since it also checks rows that arrive from streams and patch() calls: from: async () => schema.parse(await fetchRows()).