πŸ’‘ 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 instances

  • app.map() for dynamic list generation

  • context.fetch() for data loading

  • Named 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:

  1. <template id="product-card"> is registered as a reusable source and not rendered.

  2. <use template="product-card" name="featured" /> is replaced by a clone of the card template, with bindings tagged as instanceName: "featured".

  3. context.featured is created as a live reference to the instance scope.

At loadStore time:

  1. context.fetch("/api/store") loads data and the DOM updates automatically after.

  2. context.featured.* mutations update the featured card bindings.

  3. app.map() builds <use> HTML strings, one per product.

  4. context.render("#product-grid", html) injects them and processes all <use> elements.

  5. 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

NextΒΆ

10-ai: Clera AI Overview