Skip to content

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 errors
  • data-for - Alpine.js-inspired iteration with efficient diffing
  • data-if - Conditional rendering with template elements
  • data-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.

blade
data-error="fieldName"

Parameters:

  • fieldName - The name of the field to display errors for

How it works:

  1. Reads the errors signal (automatically created by signals()->validate())
  2. Displays the first error message for the specified field
  3. Hides the element when no errors exist
  4. Updates reactively when errors change

Basic usage:

blade
<div @signals(['email' => '', 'errors' => []])>
    <input data-bind="email" type="email" />
    <div data-error="email"></div>
</div>

Controller:

php
public function submit()
{
    signals()->validate([
        'email' => 'required|email|unique:users'
    ]);

    // If validation fails, errors are automatically sent to frontend
}

Complete form example:

blade
<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:

json
{
    "errors": {
        "email": ["The email field is required."],
        "password": ["The password must be at least 8 characters."]
    }
}

Custom styling:

blade
<style>
    [data-error] {
        color: red;
        font-size: 0.875rem;
        margin-top: 0.25rem;
    }

    [data-error]:empty {
        display: none;
    }
</style>

Nested field errors:

blade
<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.

blade
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:

blade
<!-- 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 ​

blade
<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 ​

blade
<template data-for="item, index in $items">
    <div>
        <span data-text="`${index + 1}. ${item.name}`"></span>
    </div>
</template>

Array Destructuring ​

blade
<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 ​

blade
<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:

blade
<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:

  1. Checks for id property
  2. Checks for uuid property
  3. Checks for key property
  4. Falls back to array index

Custom key expressions:

blade
<!-- 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 ​

blade
<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 ​

blade
<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 ​

blade
<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 ​

blade
<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:

  1. Minimal DOM updates - Only changed elements are updated
  2. Element reuse - Existing elements are moved instead of recreated
  3. Batch operations - Changes are applied in optimal order (removes → moves → adds → updates)
  4. 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.

blade
data-if="expression"

Requirements:

  • Must be used on <template> elements
  • Template must contain exactly one root element

Basic usage:

blade
<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:

blade
<template data-if="$user && $user.isAdmin">
    <div>
        <h3>Admin Panel</h3>
        <p>Welcome, admin!</p>
    </div>
</template>

Multiple conditions:

blade
<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:

blade
<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:

blade
<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 CSS display property (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.

blade
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:

  1. Intercepts link clicks and form submissions
  2. Makes Hyper request with navigate headers
  3. Updates browser history
  4. Merges query parameters based on configuration

Basic Navigation ​

blade
<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

Target specific regions of the page:

blade
<!-- 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:

php
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):

blade
<!-- Current URL: /products?category=electronics&page=2 -->
<a href="/products?search=laptop" data-navigate="true">
    Search Laptops
</a>
<!-- Result: /products?search=laptop -->

Explicit merge:

blade
<a href="/products?search=laptop" data-navigate__merge="true">
    Search Laptops
</a>
<!-- Result: /products?category=electronics&page=2&search=laptop -->

Preserve only specific parameters:

blade
<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:

blade
<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):

blade
<a href="/page" data-navigate="true">Navigate</a>

Replace history:

blade
<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:

blade
<input data-bind="search"
       data-on:input="@get('/search')"
       data-navigate__merge__debounce.300ms="true" />

Throttle navigation:

blade
<button data-on:click="@get('/update')"
        data-navigate__throttle.500ms="true">
    Update (max once per 500ms)
</button>

Simple delay:

blade
<a href="/page" data-navigate__delay.1s="true">
    Navigate (1 second delay)
</a>

Form Navigation ​

Works automatically with GET forms:

blade
<form action="/search" method="GET" data-navigate__merge="true">
    <input name="q" placeholder="Search..." />
    <button type="submit">Search</button>
</form>

Combined Modifiers ​

blade
<!-- 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:

blade
<a href="/external" data-navigate-skip>External Link</a>

Automatically skipped:

  • External links (different origin)
  • Download links (download attribute)
  • POST/PUT/DELETE forms
  • Links with data-navigate-skip attribute

Listen to navigation events on the backend:

php
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 ​

blade
<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 ​

blade
<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 ​

blade
<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 ​

blade
<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 ​

blade
<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 errors signal 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-for diffing 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