List the catalog商品一覧を並べる

One product becomes seven. A <template> repeats the card for every product, and three bindings mark the one that is sold out.

商品が1つから7つになります。<template>が商品ごとにカードを繰り返し、3つのバインディングが売り切れの商品に印を付けます。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

The resultできあがり

Seven cards from one template. Matcha is out of stock: its card is greyed out, carries a “Sold out” badge, and its button is disabled.

1つのテンプレートから7枚のカードができます。Matchaは在庫切れなので、カードが灰色になり、「Sold out」のバッジが付き、ボタンが押せなくなっています。

Live — this step's pageライブ — このステップのページOpen in a new tab ↗新しいタブで開く ↗

Change the codeコードを変える

Start from your page of step 1. In the state, replace product with a list of products. Each product also gets an id and a stock.

ステップ1のページから始めます。stateのproductを、productsのリストに置き換えます。各商品にはidとstock(在庫数)も加えます。

index.html — <wcs-state>replaces productproductを置き換え
products: [
  { id: 1, name: "House Blend", description: "Chocolate, caramel, a hint of citrus.", price: 14, stock: 12, icon: "☕" },
  { id: 2, name: "Ethiopia Yirgacheffe", description: "Bright and floral, with jasmine and lemon.", price: 18, stock: 5, icon: "☕" },
  { id: 3, name: "Colombia Supremo", description: "Nutty and smooth, with a cocoa finish.", price: 16, stock: 8, icon: "☕" },
  { id: 4, name: "Sencha", description: "Fresh, grassy Japanese green tea.", price: 12, stock: 20, icon: "🍵" },
  { id: 5, name: "Matcha", description: "Stone-ground, ceremonial grade.", price: 24, stock: 0, icon: "🍵" },
  { id: 6, name: "Measuring Spoon", description: "One scoop, one cup.", price: 6, stock: 30, icon: "🥄" },
  { id: 7, name: "Milk Pitcher", description: "Stainless steel, 350 ml.", price: 15, stock: 4, icon: "🥛" }
],
cartCount: 0,

In the markup, replace the single card inside <main> with a section that loops over the list:

マークアップでは、<main>の中の1枚のカードを、リストを回すセクションに置き換えます。

index.html — <main>replaces the cardカードを置き換え
<main class="shop">
  <section class="catalog">
    <h2>Products</h2>
    <div class="grid">
      <template data-wcs="for: products">
        <article class="card" data-wcs="class.sold-out: .stock|eq(0)">
          <div class="card-icon">{{ .icon }}</div>
          <h3>{{ .name }}</h3>
          <p>{{ .description }}</p>
          <p class="price">${{ .price|toFixed(2) }}</p>
          <p class="badge" data-wcs="hidden: .stock|ne(0)">Sold out</p>
          <button type="button" data-wcs="onclick: addToCart; disabled: .stock|eq(0)">Add to cart</button>
        </article>
      </template>
    </div>
  </section>
</main>

How it worksしくみ

for: repeats a templatefor:はテンプレートを繰り返す

The standard <template> element is inert: the browser parses it but shows nothing. data-wcs="for: products" turns it into a loop. Its content is copied once for every element of products and placed right after the template. When the array changes later, only the cards that changed are added or removed.

標準の<template>要素は不活性で、ブラウザはパースするだけで何も表示しません。data-wcs="for: products"がこれをループにします。中身がproductsの要素ごとに1回ずつ複製され、テンプレートのすぐ後ろに置かれます。あとで配列が変わったときは、変わったカードだけが足されたり取り除かれたりします。

.name means “this row's name”.nameは「この行のname」

Inside the loop, .name is short for products.*.name. The * stands for “the row this element belongs to”. A row never stores its index or a reference to its object; it holds only the wildcard path, and wcstack supplies the index. It is the same contract as in step 1 — a path — with one segment left open.

ループの中の.nameはproducts.*.nameの省略形です。*は「この要素が属する行」を表します。行は自分のインデックスもオブジェクトへの参照も持たず、ワイルドカードのパスだけを持っていて、インデックスはwcstackが補います。ステップ1と同じ「パス」という契約で、1つの区間だけが空いている形です。

{{ }} inside templatesテンプレートの中の{{ }}

{{ .name }} is a text binding, the same as textContent: .name on a wrapper element. Inside a template nothing is visible until the row is rendered, so the braces never flash on screen. A {{ }} takes filters too: {{ .price|toFixed(2) }}.

{{ .name }}はテキストのバインディングで、包む要素にtextContent: .nameと書くのと同じです。テンプレートの中は行が描画されるまで何も見えないので、波括弧が画面にちらつくことはありません。{{ }}にもフィルタが使えます({{ .price|toFixed(2) }})。

Any property, through a filterどのプロパティにも、フィルタを通して

class.sold-out: .stock|eq(0) adds the class sold-out while the row's stock equals 0 and removes it otherwise. hidden: and disabled: are not special words: they are ordinary DOM properties, and data-wcs can set any property an element has. The comparison filters eq(0) and ne(0) turn the number into the true / false those properties expect. Several bindings on one element are separated with ;.

class.sold-out: .stock|eq(0)は、行のstockが0の間はクラスsold-outを付け、それ以外では外します。hidden:とdisabled:は特別な語ではなく、ふつうのDOMプロパティです。data-wcsは要素が持つどのプロパティにも値を入れられます。比較フィルタeq(0)・ne(0)が、数値をそれらのプロパティが期待するtrue / falseに変えます。1つの要素の複数のバインディングは;で区切ります。

One handler for every cardどのカードも同じハンドラ

Every card's button calls the same addToCart, which still only counts clicks. In the next step the handler learns which product was clicked.

どのカードのボタンも同じaddToCartを呼び、それはまだクリックを数えるだけです。次のステップで、ハンドラはどの商品が押されたかを知るようになります。

Try itやってみよう

Set Sencha's stock to 0 and reload. Its card greys out, shows the badge and disables its button — data changed, markup didn't.

Senchaのstockを0にして再読み込みしてみましょう。カードが灰色になり、バッジが出て、ボタンが押せなくなります。変えたのはデータだけで、マークアップには触れていません。

Common mistakesよくある間違い

class: without a class nameクラス名のないclass:

class: .stock|eq(0) names no class. Up to 4.0.0 it did nothing at all — it set a property named class that the browser ignores, and nothing reported it. Since 4.0.1 the state refuses it when the page loads, with [wcs/binding-syntax] … "class:" needs a class name in the console, and the checker reports it as an error. Name the class after the dot: class.sold-out:.

class: .stock|eq(0)はクラスを名指していません。4.0.0までは何も起きませんでした。ブラウザが無視するclassという名前のプロパティに値を入れるだけで、何も報告されなかったのです。4.0.1からは、ページの読み込み時にstateがこれを拒み、コンソールに[wcs/binding-syntax] … "class:" needs a class nameが出て、検査器もerrorとして報告します。クラス名はドットの後ろに書きます(class.sold-out:)。

* outside a loopループの外の*

products.*.name only means something inside for: products, where there is a row for the *. Written on the page itself, it stops the state from starting, and the console says [wcs/wildcard-rank] "products.*.name" needs 1 enclosing loop level(s); the scope provides 0.

products.*.nameが意味を持つのは、*に当てはまる行があるfor: productsの中だけです。ページに直接書くとstateが起動せず、コンソールに[wcs/wildcard-rank] "products.*.name" needs 1 enclosing loop level(s); the scope provides 0.が出ます。

The whole fileファイル全体

Lost track of an edit? Here is the complete index.html of this step. npx @wcstack/lint index.html reports 0 error(s), 0 warning(s), 0 info for it.

どこを直したか分からなくなったら、このステップのindex.html全体と見比べてください。npx @wcstack/lint index.htmlの結果は0 error(s), 0 warning(s), 0 infoです。

Show index.htmlindex.htmlを表示
index.htmlstep 2ステップ2
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Little Roastery</title>
  <link rel="stylesheet" href="https://wcstack.github.io/tutorial/shop.css">
  <script type="module" src="https://esm.run/@wcstack/state@4/auto"></script>
</head>
<body>

<wcs-state>
  <script type="module">
    export default {
      products: [
        { id: 1, name: "House Blend", description: "Chocolate, caramel, a hint of citrus.", price: 14, stock: 12, icon: "☕" },
        { id: 2, name: "Ethiopia Yirgacheffe", description: "Bright and floral, with jasmine and lemon.", price: 18, stock: 5, icon: "☕" },
        { id: 3, name: "Colombia Supremo", description: "Nutty and smooth, with a cocoa finish.", price: 16, stock: 8, icon: "☕" },
        { id: 4, name: "Sencha", description: "Fresh, grassy Japanese green tea.", price: 12, stock: 20, icon: "🍵" },
        { id: 5, name: "Matcha", description: "Stone-ground, ceremonial grade.", price: 24, stock: 0, icon: "🍵" },
        { id: 6, name: "Measuring Spoon", description: "One scoop, one cup.", price: 6, stock: 30, icon: "🥄" },
        { id: 7, name: "Milk Pitcher", description: "Stainless steel, 350 ml.", price: 15, stock: 4, icon: "🥛" }
      ],
      cartCount: 0,

      addToCart() {
        this.cartCount += 1;
      }
    };
  </script>
</wcs-state>

<header class="shop-header">
  <h1>Little Roastery</h1>
  <p class="cart-badge">Cart: <span data-wcs="textContent: cartCount"></span></p>
</header>

<main class="shop">
  <section class="catalog">
    <h2>Products</h2>
    <div class="grid">
      <template data-wcs="for: products">
        <article class="card" data-wcs="class.sold-out: .stock|eq(0)">
          <div class="card-icon">{{ .icon }}</div>
          <h3>{{ .name }}</h3>
          <p>{{ .description }}</p>
          <p class="price">${{ .price|toFixed(2) }}</p>
          <p class="badge" data-wcs="hidden: .stock|ne(0)">Sold out</p>
          <button type="button" data-wcs="onclick: addToCart; disabled: .stock|eq(0)">Add to cart</button>
        </article>
      </template>
    </div>
  </section>
</main>

</body>
</html>