0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Day 8: forブロックとループコンテキストの仕組み

Last updated at Posted at 2025-12-07

この記事は「構造パスで作るシンプルなフロントエンドフレームワーク(Structive)」Advent Calendarの8日目です。
Structiveについて詳しくはこちらより

Week 2 のスタート

Week 1では構造パスの基礎を学びました。Week 2では、より高度な機能に踏み込んでいきます。今日は、配列を扱う際に必須となる「forブロックとループコンテキスト」を詳しく見ていきます。

forブロックの基本

forブロックは、配列の各要素に対してUIを繰り返し描画するための構文です。

基本的な使い方

<ul>
  {{ for:users }}
    <li>{{ users.*.name }}</li>
  {{ endfor: }}
</ul>
export default class {
  users = [
    { name: "Alice", age: 25 },
    { name: "Bob", age: 30 },
    { name: "Carol", age: 28 }
  ];
}

展開されるHTML:

<ul>
  <li>Alice</li>
  <li>Bob</li>
  <li>Carol</li>
</ul>

構文の要素

{{ for:パス }}
  <!-- ループ内容 -->
{{ endfor: }}
  • {{ for:users }}: users配列に対してループを開始
  • {{ users.*.name }}: 各要素のnameプロパティにアクセス
  • {{ endfor: }}: ループを終了

ワイルドカードの解決問題

forブロック内でusers.*.nameという構造パスをどう解釈するかが重要です。

問題の本質

{{ for:users }}
  <li>{{ users.*.name }}</li>
{{ endfor: }}

users.*.nameは抽象的な表現です:

  • 0番目の要素ではusers.0.name("Alice")
  • 1番目の要素ではusers.1.name("Bob")
  • 2番目の要素ではusers.2.name("Carol")

疑問: フレームワークはどうやって「今何番目の要素を処理しているか」を知るのか?

解決策:ループコンテキスト

ループコンテキストは、現在処理中の配列インデックスを記録する仕組みです。

class LoopContext {
  constructor() {
    this.stack = [];  // インデックスのスタック
  }
  
  push(index) {
    this.stack.push(index);
  }
  
  pop() {
    this.stack.pop();
  }
  
  current() {
    return this.stack[this.stack.length - 1];
  }
}

forブロックの実行フロー

forブロックがどのように処理されるか、ステップバイステップで見ていきましょう。

Step 1: テンプレートのパース

{{ for:users }}
  <li>{{ users.*.name }}</li>
{{ endfor: }}

フレームワークはこれを以下のように解釈:

{
  type: "for",
  arrayPath: "users",
  template: "<li>{{ users.*.name }}</li>"
}

Step 2: ループの実行

function renderForBlock(forNode, state) {
  const array = state[forNode.arrayPath];  // users配列を取得
  const fragment = document.createDocumentFragment();
  
  array.forEach((item, index) => {
    // ループコンテキストをプッシュ
    loopContext.push(index);
    
    // テンプレートを描画
    const element = renderTemplate(forNode.template, state);
    fragment.appendChild(element);
    
    // ループコンテキストをポップ
    loopContext.pop();
  });
  
  return fragment;
}

Step 3: ワイルドカードの解決

テンプレート内でusers.*.nameに遭遇したとき:

function resolveWildcard(path, loopContext) {
  if (!path.includes('*')) {
    return path;
  }
  
  const currentIndex = loopContext.current();
  return path.replace('*', currentIndex);
}

// 例
// index=0 のとき: "users.*.name" → "users.0.name"
// index=1 のとき: "users.*.name" → "users.1.name"
// index=2 のとき: "users.*.name" → "users.2.name"

実装例:シンプルなforブロック

実際のコードで確認しましょう。

class ForBlockRenderer {
  constructor(state) {
    this.state = state;
    this.loopContext = [];
  }
  
  render(arrayPath, template, container) {
    const array = this.getByPath(this.state, arrayPath);
    
    // 既存の内容をクリア
    container.innerHTML = '';
    
    array.forEach((item, index) => {
      // コンテキストをプッシュ
      this.loopContext.push(index);
      
      // テンプレートを描画
      const element = this.renderTemplate(template);
      container.appendChild(element);
      
      // コンテキストをポップ
      this.loopContext.pop();
    });
  }
  
  renderTemplate(template) {
    const element = document.createElement('div');
    element.innerHTML = template;
    
    // {{ ... }} を解決
    this.resolveBindings(element);
    
    return element.firstChild;
  }
  
  resolveBindings(element) {
    const textNodes = this.getTextNodes(element);
    
    textNodes.forEach(node => {
      const match = node.textContent.match(/\{\{\s*(.+?)\s*\}\}/);
      if (match) {
        const path = match[1];
        const resolvedPath = this.resolveWildcard(path);
        const value = this.getByPath(this.state, resolvedPath);
        node.textContent = node.textContent.replace(match[0], value);
      }
    });
  }
  
  resolveWildcard(path) {
    if (!path.includes('*')) {
      return path;
    }
    const currentIndex = this.loopContext[this.loopContext.length - 1];
    return path.replace('*', currentIndex);
  }
  
  getByPath(obj, path) {
    const keys = path.split('.');
    let current = obj;
    for (const key of keys) {
      if (current == null) return undefined;
      current = current[key];
    }
    return current;
  }
  
  getTextNodes(element) {
    const textNodes = [];
    const walker = document.createTreeWalker(
      element,
      NodeFilter.SHOW_TEXT,
      null
    );
    let node;
    while (node = walker.nextNode()) {
      textNodes.push(node);
    }
    return textNodes;
  }
}

使用例

const state = {
  users: [
    { name: "Alice", age: 25 },
    { name: "Bob", age: 30 }
  ]
};

const renderer = new ForBlockRenderer(state);
const container = document.querySelector('#user-list');
const template = '<li>{{ users.*.name }} ({{ users.*.age }})</li>';

renderer.render('users', template, container);

ネストしたforブロック

forブロックは入れ子にできます。

{{ for:categories }}
  <div>
    <h3>{{ categories.*.name }}</h3>
    <ul>
      {{ for:categories.*.items }}
        <li>{{ categories.*.items.*.name }}</li>
      {{ endfor: }}
    </ul>
  </div>
{{ endfor: }}
export default class {
  categories = [
    {
      name: "Electronics",
      items: [
        { name: "Laptop" },
        { name: "Phone" }
      ]
    },
    {
      name: "Books",
      items: [
        { name: "Novel" },
        { name: "Textbook" }
      ]
    }
  ];
}

ループコンテキストのスタック

ネストの場合、複数のインデックスを追跡する必要があります:

// 外側のループ: index=0 (Electronics)
loopContext.push(0);  // stack = [0]

  // 内側のループ: index=0 (Laptop)
  loopContext.push(0);  // stack = [0, 0]
  // categories.0.items.0.name → "Laptop"
  loopContext.pop();    // stack = [0]
  
  // 内側のループ: index=1 (Phone)
  loopContext.push(1);  // stack = [0, 1]
  // categories.0.items.1.name → "Phone"
  loopContext.pop();    // stack = [0]

loopContext.pop();      // stack = []

// 外側のループ: index=1 (Books)
loopContext.push(1);    // stack = [1]
  // ...

重要: ワイルドカードが複数ある場合、内側から順に解決します。

"categories.*.items.*.name"
↓ 外側のコンテキスト(index=0)で解決
"categories.0.items.*.name"
↓ 内側のコンテキスト(index=1)で解決
"categories.0.items.1.name"

特殊変数:ループインデックス

多くのフレームワークと同様、ループインデックスを直接参照できます。

{{ for:items }}
  <li>{{ $1 }}. {{ items.*.name }}</li>
{{ endfor: }}

$1は現在のループインデックス(0始まり)を表します。

展開結果:

<li>0. Laptop</li>
<li>1. Mouse</li>
<li>2. Keyboard</li>

1始まりのインデックス

$1|inc,1のようにパイプフィルターを使って調整できます:

{{ for:items }}
  <li>{{ $1|inc,1 }}. {{ items.*.name }}</li>
{{ endfor: }}

展開結果:

<li>1. Laptop</li>
<li>2. Mouse</li>
<li>3. Keyboard</li>

forブロック内のイベントハンドラ

forブロック内のイベントハンドラにも、ループコンテキストが渡されます。

{{ for:items }}
  <li>
    {{ items.*.name }}
    <button data-bind="onclick:deleteItem">Delete</button>
  </li>
{{ endfor: }}
export default class {
  items = [
    { name: "Laptop" },
    { name: "Mouse" },
    { name: "Keyboard" }
  ];
  
  deleteItem(event, index) {
    // index は自動的に渡される
    this.items = this.items.toSpliced(index, 1);
  }
}

ポイント: deleteItemメソッドの第2引数indexは、フレームワークが自動的に渡すループインデックスです。

仕組み

// イベントハンドラの登録時
button.addEventListener('click', (event) => {
  const currentIndex = loopContext.current();
  this.deleteItem(event, currentIndex);
});

実践例:商品リスト

forブロックを使った実用的な例を見てみましょう。

<template>
  <div class="product-list">
    <h2>Products</h2>
    <div class="products">
      {{ for:products }}
        <div class="product-card">
          <h3>{{ products.*.name }}</h3>
          <p class="price">${{ products.*.price }}</p>
          <p class="stock">
            Stock: {{ products.*.stock }}
            {{ if:products.*.isLowStock }}
              <span class="warning">⚠️ Low Stock</span>
            {{ endif: }}
          </p>
          <button data-bind="onclick:addToCart">Add to Cart</button>
        </div>
      {{ endfor: }}
    </div>
  </div>
</template>

<script type="module">
export default class {
  products = [
    { id: 1, name: "Laptop", price: 999, stock: 5 },
    { id: 2, name: "Mouse", price: 29, stock: 2 },
    { id: 3, name: "Keyboard", price: 79, stock: 15 }
  ];
  
  get "products.*.isLowStock"() {
    return this["products.*.stock"] < 5;
  }
  
  addToCart(event, index) {
    const product = this.products[index];
    console.log(`Added ${product.name} to cart`);
    // カートへの追加処理
  }
}
</script>

この例では:

  • forブロックで商品リストを描画
  • 派生状態(getter)で在庫警告を判定
  • イベントハンドラでインデックスを受け取る

まとめ

今日は、forブロックとループコンテキストの仕組みを学びました:

forブロックの役割:

  • 配列の各要素に対してUIを繰り返し描画
  • {{ for:配列パス }}{{ endfor: }}の構文

ループコンテキスト:

  • 現在処理中のインデックスを追跡
  • スタック構造でネストに対応
  • ワイルドカードを具体的なインデックスに解決

重要な概念:

  • ワイルドカード解決: users.*.nameusers.0.name
  • ネストしたループ: 複数のインデックスを管理
  • 特殊変数: $1でインデックスを参照
  • イベントハンドラ: インデックスが自動的に渡される

次回予告:
明日は、「ifブロックで条件付きレンダリング」を学びます。条件によってUIの表示/非表示を切り替える仕組みと、DOMの効率的な追加/削除を解説します。


次回: Day 9「ifブロックで条件付きレンダリング」

forブロックやループコンテキストについて質問があれば、コメントでぜひ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?