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