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.
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
<?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');
}
}
<form nitro:submit="submit" class="w-full max-w-sm space-y-3 text-sm">
@if ($welcome !== '')
<p class="rounded-lg bg-emerald-50 px-3 py-2 text-emerald-700 ring-1 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/20">{{ $welcome }}</p>
@endif
<label class="block space-y-1">
<span class="font-medium">Name</span>
<input nitro:model.trim="name" @class(['w-full rounded-lg bg-white px-3 py-2 ring-1 outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-900', 'ring-red-400' => $errors->has('name'), 'ring-slate-300 dark:ring-white/15' => ! $errors->has('name')])>
@error('name') <span class="text-xs text-red-600 dark:text-red-400">{{ $message }}</span> @enderror
</label>
<label class="block space-y-1">
<span class="font-medium">Email</span>
<input type="email" nitro:model.blur="email" @class(['w-full rounded-lg bg-white px-3 py-2 ring-1 outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-900', 'ring-red-400' => $errors->has('email'), 'ring-slate-300 dark:ring-white/15' => ! $errors->has('email')])>
@if (! $this->emailLooksRight)
<span class="text-xs text-amber-600 dark:text-amber-400">That does not look like an email address yet.</span>
@endif
@error('email') <span class="text-xs text-red-600 dark:text-red-400">{{ $message }}</span> @enderror
</label>
<div class="flex gap-4">
@foreach (['starter' => 'Starter', 'team' => 'Team'] as $value => $label)
<label class="flex items-center gap-2">
<input type="radio" value="{{ $value }}" nitro:model="plan"> {{ $label }}
</label>
@endforeach
</div>
<label class="flex items-center gap-2">
<input type="checkbox" nitro:model="terms"> I accept the terms
</label>
@error('terms') <span class="block text-xs text-red-600 dark:text-red-400">{{ $message }}</span> @enderror
<button class="rounded-lg bg-slate-900 px-3 py-2 font-semibold text-white disabled:opacity-50 dark:bg-white dark:text-slate-900" nitro:loading.attr="disabled">
<span nitro:loading.remove>Sign up</span>
<span nitro:loading>Signing up…</span>
</button>
</form>
Compiling…
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.
<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.
#[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:
#[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.
@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:
<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..removehides the element instead,.class="..."and.class.remove="..."change classes,.attr="..."sets an attribute.