Validation
Validation in Hyper works seamlessly with Laravel's validation system. You validate signals on the server using Laravel's familiar validation rules, and errors are automatically sent back to the frontend for display. This approach keeps validation logic where it belongs—on the server—while providing instant feedback to users.
Validating Signals
Laravel's validation system works with request data. In Hyper, your reactive state lives in signals, so validation happens on signal data instead of request input.
Basic Validation
Use the signals()->validate() method to validate signal data:
public function store()
{
$validated = signals()->validate([
'email' => 'required|email|unique:users',
'name' => 'required|string|max:255',
'password' => 'required|min:8|confirmed',
]);
User::create($validated);
return hyper()->signals(['message' => 'User created successfully']);
}The validate() method uses Laravel's validator under the hood, so all Laravel validation rules work exactly as you'd expect.
How It Works
When validation passes, you get the validated data back, just like Laravel's $request->validate():
$validated = signals()->validate([
'title' => 'required|string|max:100',
'content' => 'required|string',
]);
// $validated contains only the fields that were validated
Todo::create($validated);When validation fails, Hyper automatically:
- Creates an
errorssignal containing Laravel's error messages - Throws a
HyperValidationException - Sends the errors back to the frontend
The frontend receives the error structure and can display messages using the data-error attribute.
Custom Messages and Attributes
Laravel's custom validation messages and attribute names work the same way:
signals()->validate(
[
'email' => 'required|email|unique:users',
'password' => 'required|min:8',
],
[
'email.unique' => 'This :attribute is already registered.',
'password.min' => ':attribute must be at least :min characters.',
],
[
'email' => 'email address',
'password' => 'Password',
]
);Displaying Validation Errors
On the frontend, Hyper provides the data-error attribute to display validation errors automatically.
Using data-error (Hyper)
The data-error attribute displays validation errors for a specific field:
<form @signals(['name' => '', 'email' => '', 'errors' => []])
data-on:submit__prevent="@postx('/users')">
<div>
<label>Name</label>
<input type="text" data-bind="name" />
<div data-error="name" class="text-red-500"></div>
</div>
<div>
<label>Email</label>
<input type="email" data-bind="email" />
<div data-error="email" class="text-red-500"></div>
</div>
<button type="submit">Create User</button>
</form>How data-error works:
- Shows the first error message for the specified field
- Automatically hidden when no errors exist for that field
- Syncs with the
errorssignal that's created during validation - Updates reactively when validation runs again
Error Message Structure
The errors signal contains Laravel's error bag structure:
{
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters."]
}Each field can have multiple error messages. The data-error attribute shows the first one by default.
Clearing Errors
When validation succeeds, clear the errors signal to hide all error messages:
return hyper()->signals([
'errors' => [],
'message' => 'Saved successfully'
]);You can also clear errors for specific fields:
$errors = signals('errors') ?? [];
// Remove error for specific field
unset($errors['email']);
return hyper()->signals(['errors' => $errors]);File Upload Validation
File inputs in Hyper send files as base64-encoded data within signals. Hyper provides special validation rules for validating these base64 files.
Base64 Validation Rules
Hyper adds validation rules specifically for base64-encoded files:
signals()->validate([
'avatar' => 'required|b64image|b64max:2048',
'document' => 'nullable|b64file|b64mimes:pdf,doc,docx',
]);Available rules:
b64file- Validates any base64-encoded fileb64image- Validates base64-encoded image files onlyb64max:size- Maximum file size in kilobytesb64min:size- Minimum file size in kilobytesb64size:size- Exact file size in kilobytesb64mimes:ext1,ext2- Allowed file extensions (jpg, png, pdf, etc.)b64dimensions:constraints- Image dimension validation
Image Dimension Validation
The b64dimensions rule validates image dimensions with the same syntax as Laravel's dimensions rule:
signals()->validate([
'avatar' => 'required|b64image|b64dimensions:min_width=100,min_height=100,max_width=1000,max_height=1000',
'banner' => 'nullable|b64image|b64dimensions:width=1200,height=400',
'thumbnail' => 'nullable|b64image|b64dimensions:ratio=16/9',
]);Supported constraints:
min_width=100- Minimum width in pixelsmax_width=1000- Maximum width in pixelsmin_height=100- Minimum height in pixelsmax_height=1000- Maximum height in pixelswidth=500- Exact width in pixelsheight=500- Exact height in pixelsratio=16/9- Aspect ratio (width divided by height)
Complete File Upload Example
<form @signals(['avatar' => null, 'errors' => []])
data-on:submit__prevent="@postx('/profile/avatar')">
<input type="file"
data-bind="avatar"
accept="image/*" />
<div data-error="avatar" class="text-red-500"></div>
<button type="submit">Upload Avatar</button>
</form>public function uploadAvatar()
{
signals()->validate([
'avatar' => 'required|b64image|b64max:2048|b64dimensions:min_width=200,min_height=200',
]);
$path = signals()->store('avatar', 'avatars', 'public');
auth()->user()->update(['avatar' => $path]);
return hyper()
->signals(['errors' => [], 'avatar' => null])
->js("showToast('Avatar uploaded successfully', 'success')");
}Real-Time Validation
You can validate fields as users type by triggering validation on input events with debouncing:
<form @signals(['email' => '', 'errors' => []])>
<input type="email"
data-bind="email"
data-on:input__debounce.500ms="@postx('/validate-email')" />
<div data-error="email" class="text-red-500"></div>
</form>public function validateEmail()
{
try {
signals()->validate([
'email' => 'required|email|unique:users',
]);
return hyper()->signals(['errors' => []]);
} catch (\Dancycodes\Hyper\Exceptions\HyperValidationException $e) {
// Errors are automatically sent to frontend
return hyper();
}
}Validation State Indicators
Show loading states while validation runs:
<div @signals(['email' => '', 'validating' => false, 'errors' => []])>
<input type="email"
data-bind="email"
data-on:input__debounce.500ms="$validating = true; @postx('/validate-email')" />
<span data-show="$validating" class="text-gray-500">Checking...</span>
<div data-error="email" class="text-red-500"></div>
</div>public function validateEmail()
{
try {
signals()->validate([
'email' => 'required|email|unique:users',
]);
return hyper()->signals(['errors' => [], 'validating' => false]);
} catch (\Dancycodes\Hyper\Exceptions\HyperValidationException $e) {
return hyper()->signals(['validating' => false]);
}
}Common Patterns
Form Validation with Error Display
<form @signals(['name' => '', 'email' => '', 'category' => '', 'errors' => []])
data-on:submit__prevent="@postx('/contacts')">
<div>
<label>Name</label>
<input type="text" data-bind="name" />
<div data-error="name" class="text-red-500"></div>
</div>
<div>
<label>Email</label>
<input type="email" data-bind="email" />
<div data-error="email" class="text-red-500"></div>
</div>
<div>
<label>Category</label>
<select data-bind="category">
<option value="">Select category</option>
<option value="work">Work</option>
<option value="personal">Personal</option>
</select>
<div data-error="category" class="text-red-500"></div>
</div>
<button type="submit">Save Contact</button>
</form>public function store()
{
$validated = signals()->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:contacts',
'category' => 'required|in:work,personal',
]);
Contact::create($validated);
return hyper()
->signals(['name' => '', 'email' => '', 'category' => '', 'errors' => []])
->js("showToast('Contact created successfully', 'success')");
}Conditional Validation
Validate different rules based on signal values:
public function update(Contact $contact)
{
$rules = [
'name' => 'required|string|max:255',
'category' => 'required|in:work,personal',
];
// Only validate email if it changed
if (signals('email') !== $contact->email) {
$rules['email'] = 'required|email|unique:contacts,email,' . $contact->id;
}
$validated = signals()->validate($rules);
$contact->update($validated);
return hyper()->signals(['errors' => []]);
}Validating Arrays
Validate array fields using Laravel's array validation syntax:
signals()->validate([
'phone_numbers' => 'required|array|min:1|max:3',
'phone_numbers.*' => 'required|string|max:20',
]);<div @signals(['phone_numbers' => [''], 'errors' => []])>
<template data-for="phone, index in $phone_numbers">
<div>
<input type="text"
data-bind="`phone_numbers[${index}]`"
placeholder="Phone number" />
<button type="button"
data-on:click="$phone_numbers.splice(index, 1)">
Remove
</button>
</div>
</template>
<div data-error="phone_numbers" class="text-red-500"></div>
<button type="button"
data-on:click="$phone_numbers.push('')">
Add Phone Number
</button>
</div>Clearing Specific Field Errors
Clear errors for fields that are being re-validated:
public function store()
{
// Get current errors
$errors = signals('errors') ?? [];
// Clear errors for fields we're about to validate
foreach (['name', 'email'] as $field) {
unset($errors[$field]);
}
// Update errors signal before validation
hyper()->signals(['errors' => $errors]);
$validated = signals()->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
// ... rest of logic
}The HyperSignal class automatically clears errors for fields being validated, so this pattern is already handled for you in most cases.

