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:
<button data-on:click="$count = $count + 1">
Click Me
</button>When the button is clicked, the expression executes and count is incremented.
Common Events
<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:
<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:
<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:
<button data-on:click__once="$initialized = true">
Initialize (runs once)
</button>outside
Triggers when clicking outside the element:
<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:
<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:
<div data-on:scroll__throttle.200ms="$scrollPosition = window.scrollY">
<!-- Handler runs at most once per 200ms -->
</div>delay
Waits a specified time before executing:
<button data-on:click__delay.1000ms="$message = 'Delayed message'">
Click (1 second delay)
</button>Combining Modifiers
You can chain multiple modifiers:
<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:
<!-- 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:
<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
<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
<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
<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
<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
<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
<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:
<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:
<!-- 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:
<!-- 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:
<!-- 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:
public function likePost(Post $post)
{
$post->increment('likes');
return hyper()
->signals('liked', true)
->dispatch('post-liked', ['id' => $post->id]);
}With Notifications:
public function saveSettings(Request $request)
{
$request->user()->update($request->validated());
return hyper()->dispatch('notification', [
'message' => 'Settings saved!',
'type' => 'success'
]);
}Targeted Updates:
// Update specific component
return hyper()->dispatch('update-stats',
['count' => 100],
['selector' => '#sidebar']
);Chaining:
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.
<!-- 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:
// 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'
]);
}<!-- 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:
<!-- 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:
public function destroy(Post $post)
{
$post->delete();
return hyper()->dispatch('post-deleted', ['id' => $post->id]);
}Targeted Dispatch (Selector):
<!-- 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):
| Option | Type | Default | Description |
|---|---|---|---|
selector | string | null | CSS selector for targeted dispatch |
window | boolean | true | Dispatch to window object |
bubbles | boolean | true | Event bubbles up DOM |
cancelable | boolean | true | Event can be canceled |
composed | boolean | true | Event composes through shadow DOM |
Backend hyper()->dispatch(eventName, data, options):
Same options as frontend. Automatically handles JSON encoding and XSS protection.
Learn More
- Display & Binding - Update UI in response to events
- Actions - Complete reference for all HTTP actions
- Forms - Build forms with event handling
- API Reference - HyperResponse methods
- Datastar Documentation - Complete event reference

