π‘ Example: Product ListΒΆ
A product listing page using the reusable block system, context.fetch(), and app.map(). Demonstrates both dynamic rendering and named instance scopes.
π― What this coversΒΆ
<template>definition-only source<use template="..." name="..." />named instancesapp.map()for dynamic list generationcontext.fetch()for data loadingNamed instance data mutation
{path}bindings inside cloned blocks
π HTMLΒΆ
<app>
<page name="store" id="store" oncreate="loadStore">
<!-- Template source: not rendered in place -->
<template id="product-card">
<div class="product-card">
<img class="product-thumb" src="{image}" alt="{name}">
<div class="product-info">
<h3 class="product-name">{name}</h3>
<p class="product-desc">{description}</p>
<p class="product-price">{price}</p>
<button action="addToCart" class="btn-add">Add to cart</button>
</div>
</div>
</template>
<!-- Featured product: static named instance -->
<section class="featured">
<h2>Featured</h2>
<use template="product-card" name="featured" />
</section>
<!-- Dynamic product grid: generated by app.map() -->
<section class="grid">
<h2>All Products</h2>
<div id="product-grid"></div>
</section>
</page>
</app>
<script src="clera.js"></script>
<script src="store.js"></script>
β‘ JavaScriptΒΆ
// store.js
async function loadStore(context) {
const data = await context.fetch("/api/store");
// Set featured product directly
const featured = data.featured;
context.featured.name = featured.name;
context.featured.description = featured.description;
context.featured.price = formatPrice(featured.price);
context.featured.image = featured.image;
// Generate dynamic list with app.map()
let html = "";
for (const product of data.products) {
html += app.map(product, `<use template="product-card" name="{id}" />`);
}
context.render("#product-grid", html);
// Set data for each instance
for (const product of data.products) {
const inst = context[product.id];
inst.name = product.name;
inst.description = product.description;
inst.price = formatPrice(product.price);
inst.image = product.image;
}
}
function addToCart(context) {
const card = context.element.closest(".product-card");
const name = card.querySelector(".product-name").textContent;
console.log("Added to cart:", name);
}
function formatPrice(cents) {
return "$" + (cents / 100).toFixed(2);
}
π¨ CSSΒΆ
.featured {
padding: 24px;
background: #f9f9f9;
border-radius: 12px;
margin-bottom: 32px;
}
.grid {
padding: 24px 0;
}
#product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
}
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.product-thumb {
width: 100%;
height: 180px;
object-fit: cover;
}
.product-info {
padding: 16px;
}
.product-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.product-desc {
color: #666;
font-size: 14px;
margin-bottom: 12px;
}
.product-price {
font-size: 20px;
font-weight: 700;
color: #007aff;
margin-bottom: 16px;
}
.btn-add {
width: 100%;
padding: 10px;
background: #007aff;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
π§ How it worksΒΆ
At mount time:
<template id="product-card">is registered as a reusable source and not rendered.<use template="product-card" name="featured" />is replaced by a clone of the card template, with bindings tagged asinstanceName: "featured".context.featuredis created as a live reference to the instance scope.
At loadStore time:
context.fetch("/api/store")loads data and the DOM updates automatically after.context.featured.*mutations update the featured card bindings.app.map()builds<use>HTML strings, one per product.context.render("#product-grid", html)injects them and processes all<use>elements.Each
context[product.id].*mutation updates that instanceβs bindings.
π‘ Key patternsΒΆ
Template declared once, used many times:
<template id="product-card">...</template>
<use template="product-card" name="featured" />
<!-- more instances added via app.map() -->
app.map() only builds strings. The developer owns the loop:
for (const product of products) {
html += app.map(product, `<use template="product-card" name="{id}" />`);
}
Each named instance is independently mutable:
context.featured.price = "$199"; // only affects the featured card
context.abc123.price = "$99"; // only affects that product card