Nitro

Features

Forms and validation

A Nitro form is a set of properties bound to inputs with nitro:model, a #[Server] method that saves them, and rules that the server checks before that method runs. The browser gives instant hints, and the server has the final say.

On this page

As you type, the component's properties change in the browser, so hints and computed values update on every keystroke without a request. Submitting the form calls a #[Server] method. The server checks the #[Validate] rules first. If they pass, it runs the method. If they fail, it sends the messages back as $errors.

A form live, in this page View source ↓

Submit the form empty, and the messages come from the server. Type an email, and the hint under it is worked out in the browser as you type. Fill in the form and submit it, and the server saves it and clears the fields.

The component

Browser + server
<?php

namespace App\Nitro\Components;

use Nitro\Attributes\Computed;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Attributes\Validate;
use Nitro\Component;

class SignupForm extends Component
{
    #[Validate('required|string|min:2|max:60', as: 'name')]
    public string $name = '';

    #[Validate('required|email|max:120', as: 'email')]
    public string $email = '';

    #[Validate('required|in:starter,team', as: 'plan')]
    public string $plan = 'starter';

    #[Validate('accepted', as: 'terms')]
    public bool $terms = false;

    #[Locked]
    public string $welcome = '';

    /** Runs in the browser, so the hint follows your typing before anything is sent. */
    #[Computed]
    public function emailLooksRight(): bool
    {
        return $this->email === '' || preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $this->email) === 1;
    }

    #[Server]
    public function submit(): void
    {
        $this->validate();

        // A demo: kept in the session. Your app would create the account.
        session()->push('docs-signups', $this->email);

        $this->welcome = "Welcome, {$this->name}: you are on the {$this->plan} plan.";
        $this->reset('name', 'email', 'terms');
    }
}

Saved in the session, for this demo

So that the example works for every visitor, submit() keeps the email in your session. Your app would create the account in the database the same way.

Binding inputs

nitro:model keeps an input and a property in sync, in both directions. It works with text fields, text areas, radio buttons, checkboxes and selects. Use a dotted path to bind a key of an array property.

Blade
<input nitro:model.trim="name">             {{-- Trimmed --}}
<input type="email" nitro:model.blur="email">     {{-- Set when the field loses focus --}}
<input type="radio" value="team" nitro:model="plan">
<input type="checkbox" nitro:model="terms">        {{-- true or false --}}
<select nitro:model="category">...</select>
<input nitro:model="form.email">                   {{-- A key of the $form array --}}

Add a modifier to change when and how the property is set:

Modifier Does
.blur Sets the property when the field loses focus, instead of on every keystroke
.change , .lazy Sets it when the field's value is committed (the change event), like .blur
.live Sets it on every keystroke. That is what nitro:model does already, so it changes nothing; it is accepted for code written for Livewire
.debounce.300ms Sets it once typing pauses
.trim Removes spaces around the text
.number Sets it as a number, or null when the field is empty
.boolean Sets it as true or false , from "true" or "1"

Validating input

Add rules to a property with #[Validate], or pass them to $this->validate([...]) in a method. On every #[Server] call, the server checks the rules of the properties the browser changed, before the method runs. If a rule fails, the method doesn't run. Calling $this->validate() without rules checks every property's rules, as submit() does before it saves.

PHP Server
#[Validate('required|email|max:120', as: 'email')]
public string $email = '';

#[Validate(['tags' => 'array|max:3', 'tags.*' => 'string|min:2'])]
public array $tags = [];

#[Server]
public function submit(): void
{
    $this->validate();                                    // Checks every property's #[Validate] rules.
    $this->validate(['coupon' => 'nullable|alpha_num']);  // Or checks rules of its own.
}

Checking part of the form

$this->validateOnly() checks the #[Validate] rules of the properties it names, and no others. A wizard's Next checks its own step; a draft saves with a title while the rest is still empty. A key under an array property names its rules and those below it:

PHP Server
#[Validate(['account.email' => 'required|email|unique:users,email', 'account.password' => 'required|min:12'])]
public array $account = ['email' => '', 'password' => ''];

#[Validate(['profile.name' => 'required|max:80', 'profile.company' => 'nullable|max:120'])]
public array $profile = ['name' => '', 'company' => ''];

public int $step = 1;

/** Next: this step's fields only; the profile is checked on the step that asks for it. */
#[Server]
public function next(): void
{
    $this->validateOnly($this->step === 1 ? 'account' : 'profile');
    $this->step++;
}

/** The last step: everything, before anything is saved. */
#[Server]
public function finish(): void
{
    $this->validate();
    User::register($this->account, $this->profile);
    $this->redirect('/welcome', navigate: true);
}

Hints in the browser, rules on the server

Anything the browser checks is only a convenience, because anyone can send a request without your page. The rules that protect what a method saves belong on the server, on the property or in the method.

Showing errors

Error messages arrive with the component's state, as $errors. @error works as it does in any Blade view. $errors has the has(), first(), get(), all(), any() and count() methods, in the browser too. Use as: on #[Validate] to name the field in its messages.

Blade
@error('email')
    <span class="error">{{ $message }}</span>
@enderror

<input @class(['input', 'is-invalid' => $errors->has('email')]) nitro:model="email">
<p>{{ $errors->first('email') }}</p>

Showing that the form is saving

nitro:loading shows an element while a #[Server] call runs. Add .attr="disabled" to disable a button, or .remove to hide its label (see Server methods). Add .delay so a quick save shows nothing.

Showing unsaved changes

Your browser methods and nitro:model change the state at once, without a request. So the page can hold changes that the server hasn't seen yet. nitro:dirty shows an element while it does, and hides it again once a #[Server] call has taken the changes:

Blade
<form nitro:submit="save">
    <input nitro:model="title" nitro:dirty.class="border-amber-500" nitro:target="title">
    <textarea nitro:model="body"></textarea>

    <p nitro:dirty>You have unsaved changes.</p>
    <p nitro:dirty.remove>All changes saved.</p>
    <button>Save</button>
</form>
  • It compares values. Type a field back to what the server has, and it is no longer a change.
  • Narrow it with nitro:target. Name properties or paths: nitro:target="title,form.email". Without it, any change counts.
  • The same forms as nitro:loading. .remove hides the element instead, .class="..." and .class.remove="..." change classes, .attr="..." sets an attribute.

Next steps