Skip to content

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:

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

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

  1. Creates an errors signal containing Laravel's error messages
  2. Throws a HyperValidationException
  3. 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:

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

blade
<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 errors signal that's created during validation
  • Updates reactively when validation runs again

Error Message Structure

The errors signal contains Laravel's error bag structure:

javascript
{
    "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:

php
return hyper()->signals([
    'errors' => [],
    'message' => 'Saved successfully'
]);

You can also clear errors for specific fields:

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

php
signals()->validate([
    'avatar' => 'required|b64image|b64max:2048',
    'document' => 'nullable|b64file|b64mimes:pdf,doc,docx',
]);

Available rules:

  • b64file - Validates any base64-encoded file
  • b64image - Validates base64-encoded image files only
  • b64max:size - Maximum file size in kilobytes
  • b64min:size - Minimum file size in kilobytes
  • b64size:size - Exact file size in kilobytes
  • b64mimes: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:

php
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 pixels
  • max_width=1000 - Maximum width in pixels
  • min_height=100 - Minimum height in pixels
  • max_height=1000 - Maximum height in pixels
  • width=500 - Exact width in pixels
  • height=500 - Exact height in pixels
  • ratio=16/9 - Aspect ratio (width divided by height)

Complete File Upload Example

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

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

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

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

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

php
signals()->validate([
    'phone_numbers' => 'required|array|min:1|max:3',
    'phone_numbers.*' => 'required|string|max:20',
]);
blade
<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:

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