Data Queries FULL
Bind markup to an external data source and declare how fresh it should stay. The framework fetches, renders, refreshes, and handles the loading, error, and stale states for you.
The Data Query Primitive
A query is a named source declaration in JavaScript and a data-query="name" attribute in markup. The element's own shape decides how results render. An element with a <template> child renders the result as a keyed list, using the same machinery as data-list. An element without one treats the single result record as its subtree's binding context. Query state such as row count and loading flags is readable anywhere through the $name accessor, the same way store state is.
One name is doing two jobs here. A query is a reactive entity, the same kind of thing as a store: $inventory is its state and methods (rows, count, isLoading, error, isStale, refresh(), invalidate()). It is also the data it holds: $inventory.rows is the actual list or record. A store keeps those two apart because it holds many fields, so warehouse is the container and inventory is a field inside it (warehouse.inventory). A query holds exactly one dataset, always rows, so the two collapse into one name: the query's name is the dataset's name. That is why data-query="inventory" renders the inventory without you writing .rows anywhere; the engine reads rows for you. Name a query for the data it delivers (a noun like inventory or orders, never fetchInventory), and read $inventory as the query, $inventory.rows as the inventory.
The markup:
<div data-component="product-board">
<p data-show="$products.isLoading">Loading products…</p>
<p><span data-bind="$products.count"></span> products
· checked <span data-bind="checkedAt"></span></p>
<tbody data-query="products">
<template>
<tr>
<td data-bind="name"></td>
<td data-bind="price"></td>
<td data-bind="stock"></td>
</tr>
</template>
</tbody>
</div>
The declaration and the write pattern:
// The example's backend is simulated in the page; against a real
// server the declaration is the same with a URL:
// wildflower.query('products', { from: '/api/products', key: 'id',
// refresh: ['focus', 'etag:60'] });
wildflower.query('products', {
from: () => warehouse.fetchAll(),
key: 'id'
});
wildflower.component('product-board', {
state: {},
// Writes: mutate, then invalidate. The refreshed result flows back
// through the query; row identity keeps the DOM patches minimal.
sellOne() {
warehouse.sell(4);
wildflower.getQuery('products').invalidate();
}
});
The Declaration Surface
wildflower.query(name, config) registers a query globally, like a store. The full configuration surface is small:
| Option | Type | Meaning |
|---|---|---|
from | URL or function | Where data comes from. A URL is fetched. A function returns rows, or a promise of them, from any transport you like. |
key | string | Row identity for list rendering. Defaults to 'id'. |
refresh | string, number, or array | The freshness ladder. See Freshness and Live Data. |
params | object | Appended to URL requests as query parameters. |
initial | array | Seed rows shown before the first fetch resolves. |
stream | URL | Explicit event-stream endpoint for the 'sse' rung. |
There are no source types, no adapters, and no connector registry. Behind a URL, the server queries whatever it likes. Behind a function, your app fetches however it likes. The framework's contract is the shape of what comes back.
Query State
Every query exposes its state through the $ accessor in markup and through wildflower.getQuery(name) in JavaScript:
| Field | Meaning |
|---|---|
$name.rows | The current result rows. |
$name.count | Row count. |
$name.isLoading | True during the initial load, before any data exists. |
$name.error | Set when the initial load failed and there is no usable data. |
$name.syncError | Set when a background refresh failed. Existing rows are kept. |
$name.isStale | True while a refresh is in flight or the stream is interrupted. |
$name.lastSync | Timestamp of the last successful sync. |
The error surface is deliberately split in two. error means the query has nothing to show. syncError means the data on screen is fine and a background refresh failed. A background failure never wipes rows the user is looking at.
// In component methods and computeds:
const products = wildflower.getQuery('products');
products.rows // current rows
products.refresh() // fetch now
products.invalidate() // re-check the source (conditional when possible)
// Reads inside computeds track automatically, like getStore():
computed: {
inStock() {
return wildflower.getQuery('products').rows.filter(p => p.stock > 0);
}
}
Where It Runs
Data queries ship in the Full build, alongside SSR. SSR owns the first materialization of your data into HTML; queries own every one after it.
What a Query Is Not
There is no query language. Filtering, sorting, and slicing are ordinary computed properties over rows, written in plain JavaScript, and the existing data-list renders them. The section on Sources, Refinement, and Writes shows the pattern.
Section Contents
- Query Shapes. List elements, record elements, and the state surface.
- Freshness and Live Data. The refresh ladder, from one fetch to a server push stream.
- Sources, Refinement, and Writes. URL and function sources, computeds over rows, and the write pattern.
- SSR with Data Queries. The server renders it; the query keeps it true.