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);
}
}
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
- The container element must have a unique
idornameattribute - Each
addLoopItem()call creates one independent clone - The original container is always removed — clones replace it
- Nested loops require unique container names at each level
- Bindings on loop items are independent — picking on one item does not affect others
- Empty loops produce no output — handle empty state explicitly
12. Common Mistakes
❌ 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
- Always handle the empty state with a visible fallback element
- Use
showIf()inside loop items for conditional sub-elements - Keep loop container HTML minimal — only what repeats
- Use unique, descriptive container IDs —
newsCardnotcard - Avoid deeply nested loops — max 2 levels
- For large datasets, paginate server-side before looping
14. Performance Notes
- Each
addLoopItem()clones the container DOM subtree — keep the container structure lean - Avoid very large loops (100+ items) in a single render — paginate or lazy-load instead
- Nested loops multiply DOM operations — 10 outer × 10 inner = 100 clone operations
pick()
fill in the data per item cleanly.