Skip to content

Events

Datastar provides the data-on:* attribute for handling user interactions and DOM events. Combined with Hyper's CSRF-protected actions, this creates a complete event handling system that keeps your application logic on the server while providing instant feedback to users.

Event Handling

data-on:* (Datastar)

The data-on:* attribute attaches event listeners to elements. Replace * with any standard DOM event name:

blade
<button data-on:click="$count = $count + 1">
    Click Me
</button>

When the button is clicked, the expression executes and count is incremented.

Common Events

blade
 <div data-signals="{message: ''}">
    <!-- Click event -->
    <button data-on:click="$message = 'Clicked!'">
        Click
    </button>

    <!-- Input event (fires as user types) -->
    <input data-on:input="$message = 'Typing...'" />

    <!-- Change event (fires when input loses focus) -->
    <select data-on:change="$message = 'Selection changed'">
        <option>Option 1</option>
        <option>Option 2</option>
    </select>

    <!-- Form submit event -->
    <form data-on:submit="$message = 'Form submitted'">
        <button type="submit">Submit</button>
    </form>
    
    <div data-text="$message"></div>
</div>

Any standard DOM event works: click, dblclick, mouseenter, mouseleave, focus, blur, keydown, keyup, scroll, etc.

Event Modifiers

Standard Modifiers (Datastar)

Modifiers change how event listeners behave. Add them with double underscores:

prevent

Calls preventDefault() on the event:

blade
<form data-on:submit__prevent="@postx('/save')">
    <button type="submit">Save</button>
</form>

Prevents the default form submission, allowing your action to handle it instead.

stop

Calls stopPropagation() to prevent event bubbling:

blade
<div data-on:click="alert('Parent clicked')">
    <button data-on:click__stop="alert('Button clicked')">
        Click Me
    </button>
</div>

Only the button's handler runs when you click it.

once

Runs the handler only once, then removes the event listener:

blade
<button data-on:click__once="$initialized = true">
    Initialize (runs once)
</button>

outside

Triggers when clicking outside the element:

blade
<div data-signals="{dropdownOpen: false}">
    <div data-on:click__outside="$dropdownOpen = false">
        <button data-on:click="$dropdownOpen = !$dropdownOpen">
            Toggle Dropdown
        </button>
        <div data-show="$dropdownOpen">
            Dropdown content
        </div>
    </div>
</div>

Clicking anywhere outside the dropdown closes it.

Timing Modifiers (Datastar)

Control when and how often event handlers execute:

debounce

Delays execution until the user stops triggering the event:

blade
<input
    data-bind="searchQuery"
    data-on:input__debounce.300ms="@get('/search?q=' + $searchQuery)" />

The search only fires 300ms after the user stops typing. This prevents sending a request on every keystroke.

throttle

Limits how frequently the handler can run:

blade
<div data-on:scroll__throttle.200ms="$scrollPosition = window.scrollY">
    <!-- Handler runs at most once per 200ms -->
</div>

delay

Waits a specified time before executing:

blade
<button data-on:click__delay.1000ms="$message = 'Delayed message'">
    Click (1 second delay)
</button>

Combining Modifiers

You can chain multiple modifiers:

blade
<form data-on:submit__prevent__debounce.500ms="@postx('/save')">
    <!-- Prevents default, then debounces by 500ms -->
</form>

<button data-on:click__stop__once="initializeComponent()">
    <!-- Stops propagation and only runs once -->
</button>

Server Actions

Datastar HTTP Actions

Datastar provides actions for making HTTP requests:

blade
<!-- GET request -->
<button data-on:click="@get('/data')">
    Fetch Data
</button>

<!-- POST request (no CSRF token) -->
<button data-on:click="@post('/endpoint')">
    Submit
</button>

These work for simple GET requests or APIs that don't require CSRF protection.

Hyper CSRF-Protected Actions

For Laravel routes that require CSRF tokens, use Hyper's protected actions:

blade
<div data-signals="{title: ''}">
    <!-- POST with CSRF token -->
    <button data-on:click="@postx('/todos')">
        Create Todo
    </button>

    <!-- PUT with CSRF token -->
    <button data-on:click="@putx('/todos/1')">
        Update Todo
    </button>

    <!-- PATCH with CSRF token -->
    <button data-on:click="@patchx('/todos/1')">
        Patch Todo
    </button>

    <!-- DELETE with CSRF token -->
    <button data-on:click="@deletex('/todos/1')">
        Delete Todo
    </button>
</div>

Hyper automatically includes Laravel's CSRF token in these requests.

Common Patterns

Form Submission

blade
<div data-signals="{
    email: '',
    password: '',
    errors: {}
}">
    <form data-on:submit__prevent="@postx('/login')">
        <input
            type="email"
            data-bind="email"
            placeholder="Email" />
        <div data-error="email"></div>

        <input
            type="password"
            data-bind="password"
            placeholder="Password" />
        <div data-error="password"></div>

        <button type="submit">Login</button>
    </form>
</div>

The __prevent modifier stops the default form submission, allowing your action to handle it.

Search as You Type

blade
<div data-signals="{searchQuery: '', results: []}">
    <input
        data-bind="searchQuery"
        data-on:input__debounce.300ms="@get('/search?q=' + $searchQuery)"
        placeholder="Search..." />

    <template data-for="result in $results" data-for__key="id">
        <div data-text="result.title"></div>
    </template>
</div>

Debouncing prevents excessive requests while the user types.

Keyboard Shortcuts

blade
<div data-signals="{showHelp: false}">
    <div data-on:keydown__window="$showHelp = (evt.key === '?' ? !$showHelp : $showHelp)">
        Press '?' for help
    </div>

    <div data-show="$showHelp" class="help-modal">
        Help content here
    </div>
</div>

Confirmation Before Delete

blade
<div data-signals="{confirmDelete: false}">
    <button
        data-on:click="$confirmDelete = true"
        class="text-red-500">
        Delete
    </button>

    <div data-show="$confirmDelete" class="confirmation-dialog">
        <p>Are you sure you want to delete this item?</p>
        <button data-on:click="@deletex('/items/1')">
            Yes, Delete
        </button>
        <button data-on:click="$confirmDelete = false">
            Cancel
        </button>
    </div>
</div>

Auto-save on Input

blade
<div data-signals="{draft: ''}">
    <textarea
        data-bind="draft"
        data-on:input__debounce.2s="@patchx('/drafts/1')"
        placeholder="Your draft auto-saves as you type..."
        rows="10">
    </textarea>
</div>

The draft saves automatically 2 seconds after the user stops typing.

Double-Click to Edit

blade
<div data-signals="{editing: false, name: 'John Doe'}">
    <div data-show="!$editing">
        <span data-text="$name" data-on:dblclick="$editing = true"></span>
    </div>

    <div data-show="$editing">
        <input
            data-bind="name"
            data-on:blur="$editing = false; @putx('/update-name')"
            data-on:keydown="if (evt.key === 'Enter') { $editing = false; @putx('/update-name') }" />
    </div>
</div>

Double-click to edit, blur or press Enter to save.

Event Object

Within event handlers, you can access the event object via evt:

blade
<input data-on:keydown="if (evt.key === 'Enter') alert('Enter pressed!')" />

<button data-on:click="console.log(evt.target)">
    Log Target
</button>

<form data-on:submit="evt.preventDefault(); alert('Form submitted')">
    <button type="submit">Submit</button>
</form>

The evt variable contains the standard DOM event with properties like target, key, preventDefault(), etc.

Event Dispatch System

Hyper provides a Livewire-style event dispatch system for component communication, enabling decoupled, event-driven reactive patterns across your application.

@dispatch Action (Frontend)

Dispatch custom events from the frontend using native browser CustomEvent API.

Basic Global Dispatch:

blade
<!-- Dispatch to window (global) -->
<button data-on:click="@dispatch('post-liked')">
    Like
</button>

<!-- Listen anywhere in your app with __window modifier -->
<div data-on:post-liked__window="$liked++">
    Likes: <span data-text="$liked"></span>
</div>

With Event Data:

blade
<!-- Dispatch with data accessible via event.detail -->
<button data-on:click="@dispatch('notification', {
    message: 'Post saved!',
    type: 'success'
})">
    Save
</button>

<!-- Access data in listener with __window modifier -->
<div data-on:notification__window="
    $message = event.detail.message;
    $type = event.detail.type;
">
    <div data-text="$message"></div>
</div>

Multiple Targets:

blade
<!-- All matching elements receive the event -->
<button data-on:click="@dispatch('highlight', {}, {selector: '.card'})">
    Highlight All
</button>

<div class="card" data-on:highlight="el.classList.add('highlighted')">Card 1</div>
<div class="card" data-on:highlight="el.classList.add('highlighted')">Card 2</div>

hyper()->dispatch() (Backend)

Dispatch events from your Laravel controllers to trigger frontend behavior.

Basic Usage:

php
public function likePost(Post $post)
{
    $post->increment('likes');

    return hyper()
        ->signals('liked', true)
        ->dispatch('post-liked', ['id' => $post->id]);
}

With Notifications:

php
public function saveSettings(Request $request)
{
    $request->user()->update($request->validated());

    return hyper()->dispatch('notification', [
        'message' => 'Settings saved!',
        'type' => 'success'
    ]);
}

Targeted Updates:

php
// Update specific component
return hyper()->dispatch('update-stats',
    ['count' => 100],
    ['selector' => '#sidebar']
);

Chaining:

php
return hyper()
    ->view('posts.list', ['posts' => $posts])
    ->dispatch('posts-updated', ['count' => $posts->count()])
    ->dispatch('notification', ['message' => 'Refreshed']);

@dispatch Blade Directive

Dispatch events on initial page load, perfect for initialization or flash messages.

blade
<!-- Dispatch on page load -->
@dispatch('dashboard-loaded', ['timestamp' => now()->toString()])

<!-- Show flash messages -->
@if(session('success'))
    @dispatch('notification', [
        'message' => session('success'),
        'type' => 'success'
    ])
@endif

<!-- Initialize components -->
@dispatch('init-charts', ['data' => $chartData])

<!-- Targeted dispatch -->
@dispatch('init-widget', ['config' => $config], ['selector' => '#widget'])

Real-World Examples

Notification System:

php
// Backend
public function store(Request $request)
{
    $post = Post::create($request->validated());

    return hyper()
        ->view('posts.index', ['posts' => Post::latest()->get()])
        ->dispatch('notification', [
            'message' => 'Post created!',
            'type' => 'success'
        ]);
}
blade
<!-- Frontend -->
<div id="notifications"
     data-signals="{'notifications': []}"
     data-on:notification__window="
         $notifications.unshift({
             id: Date.now(),
             message: event.detail.message,
             type: event.detail.type
         });
         setTimeout(() => {
             $notifications = $notifications.filter(n => n.id !== Date.now())
         }, 3000);
     ">

    <template data-for="notification in $notifications">
        <div class="alert" data-bind__class.success="notification.type === 'success'">
            <span data-text="notification.message"></span>
        </div>
    </template>
</div>

Multi-Component Communication:

blade
<!-- Component A: Post List -->
<div data-on:post-deleted__window="$posts = $posts.filter(p => p.id !== event.detail.id)">
    <template data-for="post in $posts">
        <button data-on:click="@postx(`/posts/${post.id}/delete`)">Delete</button>
    </template>
</div>

<!-- Component B: Post Count -->
<div data-signals="{'count': {{ $posts->count() }}}"
     data-on:post-deleted__window="$count--">
    Total: <span data-text="$count"></span>
</div>

<!-- Component C: Activity Feed -->
<div data-on:post-deleted__window="
    $activities.unshift({type: 'deleted', id: event.detail.id, time: Date.now()});
">
    <!-- Activity list -->
</div>

Backend:

php
public function destroy(Post $post)
{
    $post->delete();
    return hyper()->dispatch('post-deleted', ['id' => $post->id]);
}

Targeted Dispatch (Selector):

blade
<!-- Dispatch only to elements matching selector -->
<button data-on:click="@dispatch('update-count',
    {value: 5},
    {selector: '#dashboard'}
)">
    Update
</button>

<!-- Only this element receives the event (no __window needed for targeted) -->
<div id="dashboard" data-on:update-count="$count = event.detail.value">
    Count: <span data-text="$count"></span>
</div>

API Reference

Frontend @dispatch(eventName, data, options):

OptionTypeDefaultDescription
selectorstringnullCSS selector for targeted dispatch
windowbooleantrueDispatch to window object
bubblesbooleantrueEvent bubbles up DOM
cancelablebooleantrueEvent can be canceled
composedbooleantrueEvent composes through shadow DOM

Backend hyper()->dispatch(eventName, data, options):

Same options as frontend. Automatically handles JSON encoding and XSS protection.

Learn More