Loops

Loops allow you to dynamically repeat a section of HTML using server-side data. Each loop item clones the original DOM structure, applies bindings independently, and replaces the original template element in the final output.

---

1. How Loops Work

The element matching the container key acts as the template. It is cloned once per addLoopItem() call, then the original is removed. The cloned items appear in order in its place.

addLoopItem('newsCard')     — clone template element
    ↓
pick() on each item         — queue bindings per clone
    ↓
render()                    — apply bindings, replace original with clones
---

2. Basic Loop

HTML template:


<div id="newsCard">
    <img id="cardImage" src="" alt="">
    <div>
        <span id="cardCategory"></span>
        <span id="cardDate"></span>
        <a id="cardTitle" href="#"></a>
    </div>
</div>

PHP:


foreach ($news as $article) {
    $item = $tpl->addLoopItem('newsCard');
    $item->pick('cardImage')->src($article->image);
    $item->pick('cardCategory')->content($article->category);
    $item->pick('cardDate')->content($article->date);
    $item->pick('cardTitle')
        ->content($article->title)
        ->href($article->url);
}
---

3. Loop with First / Last Detection

addLoopItem() returns a CandidTemplate. Track first and last items manually for special styling:


$articles = $news->getAll();
$total    = count($articles);

foreach ($articles as $i => $article) {
    $item = $tpl->addLoopItem('newsCard');
    $item->pick('cardTitle')->content($article->title);

    // First item — featured styling
    if ($i === 0) {
        $item->pick('newsCard')->addClass('featured');
    }

    // Last item — border removal
    if ($i === $total - 1) {
        $item->pick('newsCard')->removeClass('border-bottom');
    }
}
---

4. Loop with Conditional Visibility


foreach ($articles as $article) {
    $item = $tpl->addLoopItem('newsCard');
    $item->pick('cardTitle')->content($article->title);

    // Show badge only for breaking news
    $item->pick('.breaking-badge')->showIf($article->isBreaking());

    // Show premium lock icon only for premium content
    $item->pick('.premium-icon')->showIf($article->isPremium());

    // Highlight sponsored content
    $item->pick('newsCard')->toggleClass('sponsored', $article->isSponsored());
}
---

5. Loop with Multiple Fields and Attributes


foreach ($articles as $article) {
    $item = $tpl->addLoopItem('newsCard');

    $item->pick('cardImage')
        ->src($article->thumbnail)
        ->attribute('alt', $article->title)
        ->attribute('loading', 'lazy');

    $item->pick('cardCategory')
        ->content($article->category)
        ->addClass('badge-' . $article->categorySlug);

    $item->pick('cardDate')->content($article->publishedAt);

    $item->pick('cardTitle')
        ->content($article->title)
        ->href($article->url)
        ->data('id', $article->id)
        ->aria('label', $article->title);
}
---

6. Nested Loop

HTML template:


<div id="categoryBlock">
    <h4 id="categoryName"></h4>
    <div id="newsCard">
        <a id="cardTitle" href="#"></a>
    </div>
</div>

PHP:


foreach ($categories as $category) {
    $cat = $tpl->addLoopItem('categoryBlock');
    $cat->pick('categoryName')->content($category->name);

    foreach ($category->articles as $article) {
        $card = $cat->addLoopItem('newsCard');
        $card->pick('cardTitle')
            ->content($article->title)
            ->href($article->url);
    }
}
Nested loop containers must have unique IDs — categoryBlock and newsCard must not share names.
---

7. Loop with Includes

Inject a separate template into each loop item:


foreach ($articles as $article) {
    $item = $tpl->addLoopItem('newsCard');
    $card = $item->include('#cardBody', 'news-card');
    $card->pick('title')->content($article->title);
    $card->pick('author')->content($article->author);
}
---

8. Loop with Slots

Register a slot template inside each loop item:


foreach ($articles as $article) {
    $item  = $tpl->addLoopItem('newsCard');
    $child = $item->slot('content', 'article-detail');
    $child->pick('headline')->content($article->title);
    $child->pick('summary')->content($article->excerpt);
}
---

9. Empty Loop — Handle No Results

If no addLoopItem() calls are made, the original container element is removed entirely — nothing is rendered. Handle the empty state explicitly:


if ($articles->isEmpty()) {
    // Show empty state element instead
    $tpl->pick('#noResults')->showIf(true);
    $tpl->pick('#noResults')->content('No articles found.');
} else {
    $tpl->pick('#noResults')->showIf(false);
    foreach ($articles as $article) {
        $item = $tpl->addLoopItem('newsCard');
        $item->pick('cardTitle')->content($article->title);
    }
}

HTML:


<div id="noResults" class="alert alert-info"></div>
<div id="newsCard">...</div>
---

10. Loop Item Structure

The container element including all its children is cloned per item. The name attribute on the container is removed after cloning to keep the output clean.

Original:                     After render (2 items):
─────────────────────         ──────────────────────────────
<div id="newsCard">           <div>
  <a id="cardTitle">            <a>Article One</a>
</div>                        </div>
                              <div>
                                <a>Article Two</a>
                              </div>
---

11. Rules

---

12. Common Mistakes

❌ Duplicate loop container IDs — only first match is used

❌ Using the same container name in nested loops — inner loop overwrites outer

❌ Not handling empty state — original element disappears silently

❌ Picking on $tpl inside a loop — always pick on the loop item $item

❌ Calling addLoopItem() after render()
---

13. Best Practices

---

14. Performance Notes

---
Loops clone structure, not logic — keep container HTML minimal, handle empty state explicitly, and let pick() fill in the data per item cleanly.