A page that lists products, events, posts or any other record has two ways to do it. Both are worth knowing, because the declarative one is genuinely faster for most cases and genuinely wrong for a few.
The declarative route
The runtime scans your HTML for six attributes and wires them to window.appmint. No expression parsing, no inline JavaScript.
| Attribute | What it does |
|---|---|
data-source | Fetches and sets a data context on a container |
data-bind-each | Repeat marker — parent must carry data-source |
data-bind | Live value, read-only |
data-bind-html, data-bind-attr-*, data-bind-class, data-bind-style-* | Bind to markup, attributes, classes, styles |
data-action | Run an action on click, submit or change |
data-show / data-hide | Conditional visibility |
Modifiers: data-trigger, data-confirm, data-format.
A product list is this and nothing else:
<div data-source="product.list" data-payload='{"limit":12,"categories":"chairs"}'>
<article data-bind-each>
<img data-bind-attr-src="data.image" alt="">
<h3 data-bind="data.name"></h3>
<span data-bind="data.calculatedPrice.finalPrice" data-format="currency"></span>
<button data-action="cart.add"
data-payload="this"
data-on-success="notify:Added to your basket">Add</button>
</article>
</div>No JavaScript file, no readiness gate, no render function.
What data-payload accepts
| Form | Meaning |
|---|---|
JSON — '{"limit":12}' | A literal payload |
{{path}} | Interpolated from the data context |
"form" | The enclosing form, serialised |
"this" | The current item in a data-bind-each |
The template flash, and why it is handled
The runtime injects a stylesheet on startup that hides any data-bind-each element which has not yet been rendered:
[data-bind-each]:not([data-each-rendered]) { display: none !important; }Without it, server-rendered markup shows the empty template row for a moment before the runtime replaces it — the "flashes then disappears" effect. Rendered rows get data-each-rendered and stay visible.
This means an empty list shows nothing, not an empty row. Provide your own empty state with data-show/data-hide, or the page just looks blank when a query matches nothing.
Formatting
Binding expressions cannot call JavaScript globals. Number, Math, parseInt and toLocaleString are shadowed to undefined, and the binding silently renders blank.
<!-- blank, always -->
<span data-bind="Number(data.price).toFixed(2)"></span>
<!-- correct -->
<span data-bind="data.price" data-format="currency"></span>Use data-format, or compute the value in code and bind the result.
Reading in code
When the markup stops being enough, the same data is a call away:
const r = await appmint.product.list({ limit: 12, categories: 'chairs', sort: 'price' });
const rows = r.data || [];product.list takes limit, page, sort, sortType, keyword and categories. For anything that is not a product, the generic reader reaches any datatype:
// Custom listings only — to play posts, turn on the Content Player (see below).
await appmint.repository.find('post', { where: [['data.status', 'eq', 'published']] });
await appmint.repository.findOne('post', id);
await appmint.repository.search('sf_product', 'chair');
await appmint.repository.categories('sf_product');To show posts themselves — blog posts, galleries, courses, applications — you usually need none of this: switch on Content Player (or Blog) in Site Features and the site serves /<page>/<post>/<page-id> itself, inside the picked page's header and footer. See Content Player.
Remember that repository is read-only from the runtime — see the read/write asymmetry.
Which to use
| Situation | Route |
|---|---|
| A list, a grid, a detail panel | Declarative |
| A button that adds to a cart or opens a drawer | Declarative |
| Derived values — totals, groupings, "3 of 12 selected" | Code |
| Filters that combine, or a query built from several inputs | Code |
| Anything needing loading, empty and error states distinctly | Code |
| Canvas, drag, animation | Code |
The honest line: attributes stop paying at the point you need derived state. One list, one detail page, one add-to-cart — declarative wins, by a lot. The moment you are computing something from the rows rather than displaying them, you are writing code anyway, and half-and-half is worse than either.
Three states, every time
The declarative route gives you the success state. Everything else is yours:
function load() {
note.textContent = 'Loading…';
list.innerHTML = '';
appmint.repository.find('event', query)
.then(function (r) {
var rows = (r && r.data) || [];
if (!rows.length) {
note.textContent = 'Nothing coming up just yet.';
return;
}
note.textContent = '';
rows.forEach(renderCard);
})
.catch(function () {
note.textContent = 'Could not load these just now.';
});
}Loading, empty, error — three distinct messages. "Nothing coming up just yet" and "Could not load these" mean completely different things to whoever is reading, and collapsing them tells someone there are no events when in fact the request failed.
An evergreen page that renders whatever records exist is worth more than a page listing this month's events in hard-coded markup. The listing is written once; the content is maintained in records by whoever owns it, with no deploy.
Rendering safely
function renderCard(row) {
var d = row.data || {};
var el = document.createElement('article');
var h = document.createElement('h3');
h.textContent = d.name; // not innerHTML
el.appendChild(h);
list.appendChild(el);
}Record values go in with textContent. Product names, event titles and post excerpts are written by people, and an innerHTML template interpolating them is an injection site. data-bind is safe by default; data-bind-html is the deliberate exception and should only carry markup you control.
Pagination
let page = 1;
async function more() {
const r = await appmint.product.list({ limit: 24, page });
(r.data || []).forEach(renderCard);
page += 1;
moreBtn.hidden = !r.hasNext;
}Page on hasNext, not by counting against a total. A loop that stops when it has seen total rows never terminates if anything is written while it runs.
Related
- Runtime attributes — the full attribute reference
- Actions reference — every namespace and method
- State and rendering — holding state once you are in code
- Configurable products — what the rows contain