Hyper Attributes
Laravel Hyper extends Datastar with four custom attributes that provide enhanced functionality for Laravel applications. These attributes integrate seamlessly with Laravel's validation system, enable powerful iteration patterns, and provide client-side navigation with intelligent query parameter management.
Overview
Hyper provides the following custom attributes:
data-error- Display Laravel validation errorsdata-for- Alpine.js-inspired iteration with efficient diffingdata-if- Conditional rendering with template elementsdata-navigate- Client-side navigation with merge control
These attributes work alongside Datastar's standard attributes and follow Datastar's modifier conventions for consistency.
Validation Errors
data-error
Automatically display validation errors from Laravel's validation system.
data-error="fieldName"Parameters:
fieldName- The name of the field to display errors for
How it works:
- Reads the
errorssignal (automatically created bysignals()->validate()) - Displays the first error message for the specified field
- Hides the element when no errors exist
- Updates reactively when errors change
Basic usage:
<div @signals(['email' => '', 'errors' => []])>
<input data-bind="email" type="email" />
<div data-error="email"></div>
</div>Controller:
public function submit()
{
signals()->validate([
'email' => 'required|email|unique:users'
]);
// If validation fails, errors are automatically sent to frontend
}Complete form example:
<div @signals([
'name' => '',
'email' => '',
'password' => '',
'errors' => []
])>
<form data-on:submit__prevent="@postx('/register')">
<div>
<label>Name</label>
<input data-bind="name" />
<div data-error="name" class="error-message"></div>
</div>
<div>
<label>Email</label>
<input data-bind="email" type="email" />
<div data-error="email" class="error-message"></div>
</div>
<div>
<label>Password</label>
<input data-bind="password" type="password" />
<div data-error="password" class="error-message"></div>
</div>
<button type="submit">Register</button>
</form>
</div>Error signal structure:
Laravel's validation errors are sent as:
{
"errors": {
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters."]
}
}Custom styling:
<style>
[data-error] {
color: red;
font-size: 0.875rem;
margin-top: 0.25rem;
}
[data-error]:empty {
display: none;
}
</style>Nested field errors:
<div @signals(['contacts' => [['name' => '', 'email' => '']], 'errors' => []])>
<template data-for="contact, index in $contacts">
<div>
<input data-bind="`contacts.${index}.name`" />
<div data-error="`contacts.${index}.name`"></div>
<input data-bind="`contacts.${index}.email`" />
<div data-error="`contacts.${index}.email`"></div>
</div>
</template>
</div>Iteration
data-for
Loop over arrays or objects with efficient DOM diffing and element reuse.
data-for="expression"
data-for__key="keyExpression"Inspired by Alpine.js x-for, this attribute provides the same familiar syntax with Datastar's reactive foundation.
Requirements:
- Must be used on
<template>elements - Template must contain exactly one root element
Expression formats:
<!-- Basic iteration -->
data-for="item in $items"
<!-- With index -->
data-for="item, index in $items"
<!-- With index and collection -->
data-for="item, index, collection in $items"
<!-- Array destructuring -->
data-for="[name, email] in $users"
<!-- Object destructuring -->
data-for="{id, name} in $users"Basic Iteration
<div @signals(['todos' => [
['id' => 1, 'title' => 'Buy groceries', 'done' => false],
['id' => 2, 'title' => 'Walk dog', 'done' => true]
]])>
<template data-for="todo in $todos">
<div>
<input type="checkbox" data-bind="todo.done" />
<span data-text="todo.title"></span>
</div>
</template>
</div>Iteration with Index
<template data-for="item, index in $items">
<div>
<span data-text="`${index + 1}. ${item.name}`"></span>
</div>
</template>Array Destructuring
<div @signals(['users' => [
['John Doe', 'john@example.com'],
['Jane Smith', 'jane@example.com']
]])>
<template data-for="[name, email] in $users">
<div>
<strong data-text="name"></strong>
<span data-text="email"></span>
</div>
</template>
</div>Object Destructuring
<template data-for="{id, name, email} in $users">
<div>
<span data-text="id"></span>:
<strong data-text="name"></strong> -
<span data-text="email"></span>
</div>
</template>Key Modifier
Use data-for__key to specify which property to use for efficient diffing:
<template data-for__key.id="user in $users">
<div data-text="user.name"></div>
</template>Default key behavior:
If no key is specified, Hyper uses smart default key generation:
- Checks for
idproperty - Checks for
uuidproperty - Checks for
keyproperty - Falls back to array index
Custom key expressions:
<!-- Use specific property -->
<template data-for__key.sku="product in $products">
<div data-text="product.name"></div>
</template>
<!-- Use index explicitly -->
<template data-for__key.index="item in $items">
<div data-text="item"></div>
</template>Iterating Numbers
<div @signals(['count' => 5])>
<template data-for="i in $count">
<div data-text="i"></div>
</template>
<!-- Renders: 1, 2, 3, 4, 5 -->
</div>Iterating Objects
<div @signals(['user' => ['name' => 'John', 'email' => 'john@example.com']])>
<template data-for="[key, value] in $user">
<div>
<strong data-text="key"></strong>: <span data-text="value"></span>
</div>
</template>
</div>Nested Loops
<template data-for="category in $categories">
<div>
<h3 data-text="category.name"></h3>
<template data-for="product in category.products">
<div data-text="product.name"></div>
</template>
</div>
</template>Dynamic Lists
<div @signals(['items' => [], 'newItem' => ''])>
<input data-bind="newItem" placeholder="Add item" />
<button data-on:click="$items.push($newItem); $newItem = ''">
Add
</button>
<template data-for="item, index in $items">
<div>
<span data-text="item"></span>
<button data-on:click="$items.splice(index, 1)">Remove</button>
</div>
</template>
</div>Efficient Diffing
The data-for attribute uses a sophisticated diffing algorithm adapted from Alpine.js:
- Minimal DOM updates - Only changed elements are updated
- Element reuse - Existing elements are moved instead of recreated
- Batch operations - Changes are applied in optimal order (removes → moves → adds → updates)
- Smart key handling - Automatic duplicate key detection and resolution
Performance characteristics:
- Adding item at end: O(1)
- Removing item: O(1)
- Moving item: O(1)
- Full list replacement: O(n)
Conditional Rendering
data-if
Conditionally render elements based on a boolean expression.
data-if="expression"Requirements:
- Must be used on
<template>elements - Template must contain exactly one root element
Basic usage:
<div @signals(['showDetails' => false])>
<button data-on:click="$showDetails = !$showDetails">
Toggle Details
</button>
<template data-if="$showDetails">
<div class="details">
<p>These are the details.</p>
</div>
</template>
</div>With complex conditions:
<template data-if="$user && $user.isAdmin">
<div>
<h3>Admin Panel</h3>
<p>Welcome, admin!</p>
</div>
</template>Multiple conditions:
<template data-if="$status === 'loading'">
<div>Loading...</div>
</template>
<template data-if="$status === 'error'">
<div class="error">Error occurred!</div>
</template>
<template data-if="$status === 'success'">
<div class="success">Success!</div>
</template>Combining with data-for:
<template data-for="user in $users">
<div>
<span data-text="user.name"></span>
<template data-if="user.isVerified">
<span class="badge">✓ Verified</span>
</template>
</div>
</template>Toggle patterns:
<div @signals(['expanded' => false])>
<button data-on:click="$expanded = !$expanded">
<span data-show="!$expanded">Show More</span>
<span data-show="$expanded">Show Less</span>
</button>
<template data-if="$expanded">
<div class="content">
<p>Expanded content here...</p>
</div>
</template>
</div>Difference from data-show:
data-show- Toggles CSSdisplayproperty (element stays in DOM)data-if- Adds/removes element from DOM completely
Use data-if when:
- Element is expensive to render
- You want to avoid running JavaScript in hidden elements
- You need true conditional rendering
Use data-show when:
- Toggling visibility frequently
- Element is simple
- You want CSS transitions
Client-Side Navigation
data-navigate
Enable client-side navigation with intelligent query parameter management.
data-navigate="true"
data-navigate__key="navigationKey"
data-navigate__merge="true"
data-navigate__only="param1,param2"
data-navigate__except="param1"
data-navigate__replace="true"
data-navigate__debounce="300ms"How it works:
- Intercepts link clicks and form submissions
- Makes Hyper request with navigate headers
- Updates browser history
- Merges query parameters based on configuration
Basic Navigation
<a href="/dashboard" data-navigate="true">Dashboard</a>Converted to:
- Hyper AJAX request to
/dashboard - Updates
#content(or specified navigation key) - Pushes URL to browser history
Navigation Keys
Target specific regions of the page:
<!-- Update sidebar only -->
<a href="/sidebar" data-navigate__key.sidebar="true">
Update Sidebar
</a>
<!-- Update main content -->
<a href="/content" data-navigate__key.main="true">
Update Content
</a>Backend handling:
public function page()
{
if (request()->isHyperNavigate('sidebar')) {
return hyper()->fragment('layout', 'sidebar', $sidebarData);
}
if (request()->isHyperNavigate('main')) {
return hyper()->fragment('layout', 'main', $mainData);
}
return view('page', $data);
}Query Parameter Merging
No merge (default):
<!-- Current URL: /products?category=electronics&page=2 -->
<a href="/products?search=laptop" data-navigate="true">
Search Laptops
</a>
<!-- Result: /products?search=laptop -->Explicit merge:
<a href="/products?search=laptop" data-navigate__merge="true">
Search Laptops
</a>
<!-- Result: /products?category=electronics&page=2&search=laptop -->Preserve only specific parameters:
<a href="/products?page=1" data-navigate__only.search,category="true">
Reset Page
</a>
<!-- Result: /products?search=laptop&category=electronics&page=1 -->Preserve all except specific parameters:
<a href="/products" data-navigate__except.page="true">
Keep Filters, Reset Page
</a>
<!-- Result: /products?category=electronics&search=laptop -->History Management
Push to history (default):
<a href="/page" data-navigate="true">Navigate</a>Replace history:
<a href="/page" data-navigate__replace="true">Navigate</a>Use __replace for:
- Pagination
- Search results
- Filter changes
- Any navigation that shouldn't add to browser history
Timing Modifiers
Debounce navigation:
<input data-bind="search"
data-on:input="@get('/search')"
data-navigate__merge__debounce.300ms="true" />Throttle navigation:
<button data-on:click="@get('/update')"
data-navigate__throttle.500ms="true">
Update (max once per 500ms)
</button>Simple delay:
<a href="/page" data-navigate__delay.1s="true">
Navigate (1 second delay)
</a>Form Navigation
Works automatically with GET forms:
<form action="/search" method="GET" data-navigate__merge="true">
<input name="q" placeholder="Search..." />
<button type="submit">Search</button>
</form>Combined Modifiers
<!-- Comprehensive navigation control -->
<a href="/products?page=1"
data-navigate__merge__except.page__debounce.300ms="true">
Next Page
</a>
<!-- Targeted update with granular parameter control -->
<a href="/filters"
data-navigate__key.filters__only.search,category="true">
Apply Filters
</a>Opt-Out
Skip navigation for specific links:
<a href="/external" data-navigate-skip>External Link</a>Automatically skipped:
- External links (different origin)
- Download links (
downloadattribute) - POST/PUT/DELETE forms
- Links with
data-navigate-skipattribute
Navigation Events
Listen to navigation events on the backend:
if (request()->isHyperNavigate()) {
// Any navigation request
}
if (request()->isHyperNavigate('sidebar')) {
// Specific navigation key
}
$key = request()->hyperNavigateKey();
// Returns: "sidebar" or null
$keys = request()->hyperNavigateKeys();
// Returns: ["sidebar", "main"] or []Common Patterns
Search with Filters
<div @signals([
'search' => request('search', ''),
'category' => request('category', ''),
'minPrice' => request('minPrice', ''),
'products' => $products
])>
<form action="/products" method="GET" data-navigate__merge="true">
<input data-bind="search" name="search" placeholder="Search..." />
<select data-bind="category" name="category">
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
<input data-bind="minPrice" name="minPrice" type="number" placeholder="Min Price" />
<button type="submit">Search</button>
<a href="/products" data-navigate__except.search,category,minPrice="true">
Clear Filters
</a>
</form>
@fragment('product-list')
<div id="product-list">
<template data-for__key.id="product in $products">
<div>
<h3 data-text="product.name"></h3>
<p data-text="`$${product.price}`"></p>
</div>
</template>
</div>
@endfragment
</div>Pagination with Filters
<div @signals(['page' => {{ request('page', 1) }}, 'items' => $items])>
<!-- Filters preserved, page changes -->
<a href="/items?page={{ $items->currentPage() - 1 }}"
data-navigate__merge__replace="true"
data-show="$page > 1">
Previous
</a>
<a href="/items?page={{ $items->currentPage() + 1 }}"
data-navigate__merge__replace="true"
data-show="$page < {{ $items->lastPage() }}">
Next
</a>
@fragment('item-list')
<div id="item-list">
<template data-for__key.id="item in $items">
<div data-text="item.name"></div>
</template>
</div>
@endfragment
</div>Todo List with Validation
<div @signals([
'todos' => $todos,
'newTodo' => '',
'editingId' => null,
'errors' => []
])>
<form data-on:submit__prevent="@postx('/todos')">
<input data-bind="newTodo" placeholder="Add todo..." />
<div data-error="title"></div>
<button type="submit">Add</button>
</form>
@fragment('todo-list')
<div id="todo-list">
<template data-for__key.id="todo in $todos">
<div>
<template data-if="$editingId === todo.id">
<div>
<input data-bind="todo.title" />
<button data-on:click="@putx(`/todos/${todo.id}`); $editingId = null">
Save
</button>
</div>
</template>
<template data-if="$editingId !== todo.id">
<div>
<input type="checkbox"
data-bind="todo.done"
data-on:change="@patchx(`/todos/${todo.id}`)" />
<span data-text="todo.title"></span>
<button data-on:click="$editingId = todo.id">Edit</button>
<button data-on:click="@deletex(`/todos/${todo.id}`)">Delete</button>
</div>
</template>
</div>
</template>
</div>
@endfragment
</div>Multi-Step Form
<div @signals([
'step' => 1,
'name' => '',
'email' => '',
'password' => '',
'errors' => []
])>
<template data-if="$step === 1">
<div>
<h2>Step 1: Personal Info</h2>
<input data-bind="name" />
<div data-error="name"></div>
<button data-on:click="$step = 2">Next</button>
</div>
</template>
<template data-if="$step === 2">
<div>
<h2>Step 2: Account</h2>
<input data-bind="email" type="email" />
<div data-error="email"></div>
<input data-bind="password" type="password" />
<div data-error="password"></div>
<button data-on:click="$step = 1">Back</button>
<button data-on:click="@postx('/register')">Register</button>
</div>
</template>
</div>Tabbed Interface with Navigation
<div @signals(['activeTab' => request('tab', 'profile')])>
<div class="tabs">
<a href="?tab=profile"
data-navigate__key.content__merge="true"
data-class:active="$activeTab === 'profile'">
Profile
</a>
<a href="?tab=settings"
data-navigate__key.content__merge="true"
data-class:active="$activeTab === 'settings'">
Settings
</a>
</div>
@fragment('content')
<div id="content">
<template data-if="$activeTab === 'profile'">
<div>Profile content...</div>
</template>
<template data-if="$activeTab === 'settings'">
<div>Settings content...</div>
</template>
</div>
@endfragment
</div>Technical Details
data-error Implementation
- Creates
errorssignal if missing - Uses Datastar's
computed()for reactive error reading - Updates DOM with
effect()when errors change - Handles both array and string error formats
data-for Implementation
- Based on Alpine.js
x-fordiffing algorithm - Uses Datastar's signal system for data tracking
- Implements sophisticated diff calculation: O(n) complexity
- Applies changes in optimal order for minimal DOM updates
- Supports direct signal path integration for nested signals
data-if Implementation
- Requires single root element (Alpine/Vue pattern)
- Uses Datastar's reactive
effect()for condition evaluation - Applies Datastar processing to rendered elements
- Cleans up properly when element is removed
data-navigate Implementation
- Follows Datastar modifier conventions
- Provides explicit merge control (no magic defaults)
- Supports debounce, throttle, and delay timing
- Automatically handles browser history
- Falls back to standard navigation if Hyper unavailable
Related Documentation
- Blade Directives -
@signalsfor initializing signals - Signals Helper - Server-side signal validation
- Request Macros - Navigation detection methods
- HyperResponse - Fragment rendering
- Datastar Attributes - Standard Datastar attributes reference

