Lists & Loops
The data-for attribute creates multiple instances of an element for each item in an array. This is Hyper's way of handling dynamic lists that change on the client side—lists where users can add, remove, or reorder items without a server round trip.
Basic Usage
data-for (Hyper)
Use data-for on a <template> element to loop over an array signal:
<div data-signals="{users: [
{id: 1, name: 'Alice'},
{id: 2, name: 'Bob'}
]}">
<template data-for="user in $users">
<div data-text="user.name"></div>
</template>
</div>This creates a div for each user. When the users signal changes, the DOM updates automatically.
Syntax
item in $signalNameitem- Your name for each array element$signalName- The signal containing your array
Template Requirements
The template must contain exactly one root element:
<!-- ✅ Correct -->
<template data-for="user in $users">
<div>
<h3 data-text="user.name"></h3>
<p data-text="user.email"></p>
</div>
</template>
<!-- ❌ Wrong - multiple roots -->
<template data-for="user in $users">
<h3 data-text="user.name"></h3>
<p data-text="user.email"></p>
</template>Using Keys
data-for__key (Hyper)
Keys tell Hyper which items are which. This is critical for efficient updates and preserving element state:
<template data-for__key.id="user in $users">
<div data-text="user.name"></div>
</template>The __key modifier specifies which property uniquely identifies each item. Without keys, Hyper can't tell if you reordered items or replaced them, leading to incorrect behavior.
Default Keys
If you don't specify a key, Hyper automatically looks for:
idpropertyuuidpropertykeyproperty
If none exist, it falls back to array index (which breaks when items are reordered).
Why Keys Matter
<!-- Without proper keys, form state gets confused -->
<div data-signals="{todos: [
{id: 1, title: 'Buy milk', done: false},
{id: 2, title: 'Walk dog', done: true}
]}">
<template data-for__key.id="todo in $todos">
<div>
<input type="checkbox" data-bind="todo.done" />
<span data-text="todo.title"></span>
</div>
</template>
</div>When you reorder todos, the keys ensure checkboxes stay with their correct items.
Working with Index
Getting the Index
Access the current index with a second variable:
<template data-for="item, index in $items">
<div data-text="(index + 1) + '. ' + item"></div>
</template>The index starts at 0 and updates automatically when items are added, removed, or reordered.
Dynamic Buttons Based on Index
<div data-signals="{
phones: [{id: 1, number: '555-1234'}],
nextId: 2
}">
<template data-for__key.id="phone, index in $phones">
<div>
<label data-text="'Phone ' + (index + 1)"></label>
<input data-bind="phone.number" />
<!-- Show "Add" button only on last item -->
<button
data-on:click="$phones.push({id: $nextId, number: ''}); $nextId++;"
data-show="index === $phones.length - 1 && $phones.length < 3">
Add
</button>
<!-- Show "Remove" button if more than one item -->
<button
data-on:click="$phones.splice(index, 1)"
data-show="$phones.length > 1">
Remove
</button>
</div>
</template>
</div>When to Use data-for
Server-Driven Philosophy
Datastar is server-driven. Most lists should render on the server with @foreach:
<!-- ✅ Server-side rendering for static lists -->
@foreach($products as $product)
<div>{{ $product->name }}</div>
@endforeachThe server knows your data, handles authorization, and delivers complete HTML. This is faster and simpler than client-side rendering.
When data-for Makes Sense
Use data-for when users modify the list on the client:
Dynamic Form Fields
<div data-signals="{
items: [{id: 1, value: ''}],
nextId: 2
}">
<template data-for__key.id="item, index in $items">
<input data-bind="item.value" />
<button data-on:click="$items.splice(index, 1)">Remove</button>
</template>
<button data-on:click="$items.push({id: $nextId, value: ''}); $nextId++;">
Add Item
</button>
</div>Real-Time Updates
When the server sends updated lists via fragments, data-for updates the DOM efficiently.
When Not to Use data-for
Don't use data-for if:
- The list doesn't change after initial render
- Users don't interact with the list
- The list is very large (hundreds of items)
For these cases, server-side @foreach is simpler and faster.
Integrating with Server Data
Transforming Backend Data
Your backend might structure data differently than data-for expects. Transform it in Blade:
@php
// Backend stores: ['555-1234', '555-5678']
// data-for needs: [{id: 1, number: '555-1234'}, {id: 2, number: '555-5678'}]
$phoneData = [];
$nextId = 1;
foreach ($contact->phone_numbers ?? [] as $phone) {
$phoneData[] = ['id' => $nextId++, 'number' => $phone];
}
@endphp
<form @signals([
'phone_numbers' => $phoneData,
'next_phone_id' => $nextId
])>
<template data-for__key.id="phone in $phone_numbers">
<input data-bind="phone.number" />
</template>
</form>Transform back in your controller before saving:
// Transform [{id, number}] back to ['number']
$phoneNumbers = array_map(
fn($item) => $item['number'],
signals('phone_numbers')
);Common Patterns
Dynamic Phone Number Fields
<div data-signals="{
phoneNumbers: [{id: 1, number: ''}],
nextPhoneId: 2
}">
<template data-for__key.id="phone, index in $phoneNumbers">
<div class="flex gap-2 mb-2">
<input
data-bind="phone.number"
placeholder="Phone number"
class="border px-3 py-2 rounded" />
<button
data-on:click="$phoneNumbers.push({id: $nextPhoneId, number: ''}); $nextPhoneId++;"
data-show="index === $phoneNumbers.length - 1 && $phoneNumbers.length < 3"
class="text-blue-500">
Add
</button>
<button
data-on:click="$phoneNumbers.splice(index, 1)"
data-show="$phoneNumbers.length > 1"
class="text-red-500">
Remove
</button>
</div>
</template>
</div>Todo List
<div @signals(['todos' => $todos])>
<form data-on:submit__prevent="@postx('/todos')">
<input data-bind="newTodo" placeholder="New todo" />
<button type="submit">Add</button>
</form>
<template data-for__key.id="todo in $todos">
<div>
<input type="checkbox" data-bind="todo.done" />
<span data-text="todo.title"></span>
<button data-on:click="@deletex('/todos/' + todo.id)">Delete</button>
</div>
</template>
</div>Product List
<div @signals(['products' => $products])>
<template data-for__key.id="product in $products">
<div class="product-card">
<h3 data-text="product.name"></h3>
<p data-text="'$' + product.price"></p>
<button data-on:click="@postx('/cart/add/' + product.id)">
Add to Cart
</button>
</div>
</template>
</div>Nested Lists
<div data-signals="{categories: [
{
name: 'Electronics',
products: [
{id: 1, name: 'Laptop'},
{id: 2, name: 'Phone'}
]
}
]}">
<template data-for="category in $categories">
<div>
<h3 data-text="category.name"></h3>
<template data-for__key.id="product in category.products">
<div data-text="product.name"></div>
</template>
</div>
</template>
</div>Filtered Lists
<div data-signals="{
products: [...],
searchQuery: ''
}">
<input data-bind="searchQuery" placeholder="Search products..." />
<template data-for__key.id="product in $products.filter(p =>
p.name.toLowerCase().includes($searchQuery.toLowerCase())
)">
<div data-text="product.name"></div>
</template>
</div>Learn More
- Display & Binding - Bind data inside loops
- Conditional - Show/hide list items
- Events - Handle user interactions in lists
- Fragments - Update lists from the server

