Getting started
Quickstart
In this guide you will build a todo list in a few minutes, with a class, a view, a tag to place it, and one method that runs on the server. The finished component is running below, on this page.
On this page
1. Write the class
Create app/Nitro/Components/Todos.php. The component's public properties are its state.
Here they hold the list, the title being typed, and the time the list was last saved.
<?php
namespace App\Nitro\Components;
use Nitro\Attributes\Computed;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Component;
class Todos extends Component
{
public array $todos = [];
public string $title = '';
#[Locked]
public string $savedAt = '';
public function mount(): void
{
$this->todos = session('todos.list', []);
$this->savedAt = session('todos.saved_at', '');
}
public function add(): void
{
if (trim($this->title) === '') {
return;
}
$this->todos[] = ['title' => trim($this->title), 'done' => false];
$this->title = '';
}
public function toggle(int $index): void
{
$this->todos[$index]['done'] = ! $this->todos[$index]['done'];
}
#[Computed]
public function remaining(): int
{
return count(array_filter($this->todos, fn ($todo) => ! $todo['done']));
}
#[Server]
public function save(): void
{
$this->validate([
'todos' => 'array|max:20',
'todos.*.title' => 'required|string|max:100',
'todos.*.done' => 'boolean',
]);
$this->savedAt = now()->format('H:i:s');
// This demo keeps the list in the session. Your app would save it to the database.
session(['todos.list' => $this->todos, 'todos.saved_at' => $this->savedAt]);
}
}
mount()runs on the server when the component is created. It loads the saved list.add()andtoggle()are plain methods. They run in the browser, so adding and ticking off todos doesn't send anything to the server.remaining()is marked#[Computed]. You read it as$this->remaining, and Nitro works it out once and keeps it until the list changes.save()is marked#[Server], so it runs on the server. The list comes from the browser, sosave()validates it before storing it.$savedAtis marked#[Locked]. The browser can read it, but only the server can set it.
This demo saves to the session
So that the example works for every visitor, without an account or a database, save() keeps the list in your session. Your application would save it to the database in the same way. The call to the server, the validation and the new state that comes back all stay the same.
2. Write the view
Create resources/views/nitro/components/todos.blade.php. The view is ordinary Blade, so
@foreach, @class, @checked and echoes work as they always do.
The nitro: attributes connect the view to the class.
<div class="w-full max-w-sm space-y-3">
<form nitro:submit="add" class="flex gap-2">
<input nitro:model="title" maxlength="100" placeholder="What needs doing?" class="flex-1 rounded-lg bg-white px-3 py-2 text-sm ring-1 ring-slate-300 outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-900 dark:ring-white/15">
<button class="rounded-lg bg-slate-900 px-3 py-2 text-sm font-semibold text-white dark:bg-white dark:text-slate-900">Add</button>
</form>
<ul class="space-y-1">
@foreach ($todos as $i => $todo)
<li>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" nitro:click="toggle($i)" @checked($todo['done'])>
<span @class(['line-through text-slate-400' => $todo['done']])>{{ $todo['title'] }}</span>
</label>
</li>
@endforeach
</ul>
<div class="flex items-center gap-3 text-sm text-slate-500">
<span>{{ $this->remaining }} left</span>
@if ($savedAt !== '')
<span class="ml-auto">Saved at {{ $savedAt }}. Reload the page and it stays.</span>
@endif
<button nitro:click="save" nitro:loading.attr="disabled" @class(['font-semibold text-slate-900 disabled:opacity-50 dark:text-white', 'ml-auto' => $savedAt === ''])>Save</button>
</div>
</div>
| Attribute | What it does |
|---|---|
nitro:submit="add"
|
Calls
add()
when the form is submitted, without reloading the page
|
nitro:model="title"
|
Keeps the input and
$title
in sync
|
nitro:click="toggle($i)"
|
Calls
toggle()
with the row's index
|
nitro:loading.attr="disabled"
|
Disables the button while
save()
runs
|
3. Place the component
You place a component in any Blade view with its tag. The tag is the class name in kebab case, so
Todos becomes todos, and Posts\Table would become
posts.table.
<x-layouts.app>
<h1>My todos</h1>
<nitro:todos />
</x-layouts.app>
Route::view('/todos', 'todos');
<x-layouts.app> is the layout you made in
Installation, with @nitroScripts in it.
If clicking does nothing, that line is the first thing to check.
Now open /todos. The server renders the list with Blade, so the page arrives complete. The
browser then takes over. Type a todo, press enter, and tick it off. Nothing is sent to the server until
you press Save. If you reload after saving, the list is still there, because
mount() loaded it.
Watch the network tab
Adding and ticking off todos makes no request. Saving makes exactly one. The component's signed state goes to the server, save() runs, and the new state comes back.
4. Turn it into a page (optional)
A component is one part of a page. When it fills the whole screen, you can turn it into a
page instead. Move the class to app/Nitro/Pages and its view to
nitro/pages, then give it a route. A page has its own layout and title, and loads its data
in mount().
use App\Nitro\Pages\Todos;
Route::nitro('/todos', Todos::class)->name('todos');
The whole component
Here are the class and the view side by side, along with the JavaScript that Nitro compiles them to.
<?php
namespace App\Nitro\Components;
use Nitro\Attributes\Computed;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Component;
class Todos extends Component
{
public array $todos = [];
public string $title = '';
#[Locked]
public string $savedAt = '';
public function mount(): void
{
$this->todos = session('todos.list', []);
$this->savedAt = session('todos.saved_at', '');
}
public function add(): void
{
if (trim($this->title) === '') {
return;
}
$this->todos[] = ['title' => trim($this->title), 'done' => false];
$this->title = '';
}
public function toggle(int $index): void
{
$this->todos[$index]['done'] = ! $this->todos[$index]['done'];
}
#[Computed]
public function remaining(): int
{
return count(array_filter($this->todos, fn ($todo) => ! $todo['done']));
}
#[Server]
public function save(): void
{
$this->validate([
'todos' => 'array|max:20',
'todos.*.title' => 'required|string|max:100',
'todos.*.done' => 'boolean',
]);
$this->savedAt = now()->format('H:i:s');
// This demo keeps the list in the session. Your app would save it to the database.
session(['todos.list' => $this->todos, 'todos.saved_at' => $this->savedAt]);
}
}
<div class="w-full max-w-sm space-y-3">
<form nitro:submit="add" class="flex gap-2">
<input nitro:model="title" maxlength="100" placeholder="What needs doing?" class="flex-1 rounded-lg bg-white px-3 py-2 text-sm ring-1 ring-slate-300 outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-900 dark:ring-white/15">
<button class="rounded-lg bg-slate-900 px-3 py-2 text-sm font-semibold text-white dark:bg-white dark:text-slate-900">Add</button>
</form>
<ul class="space-y-1">
@foreach ($todos as $i => $todo)
<li>
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" nitro:click="toggle($i)" @checked($todo['done'])>
<span @class(['line-through text-slate-400' => $todo['done']])>{{ $todo['title'] }}</span>
</label>
</li>
@endforeach
</ul>
<div class="flex items-center gap-3 text-sm text-slate-500">
<span>{{ $this->remaining }} left</span>
@if ($savedAt !== '')
<span class="ml-auto">Saved at {{ $savedAt }}. Reload the page and it stays.</span>
@endif
<button nitro:click="save" nitro:loading.attr="disabled" @class(['font-semibold text-slate-900 disabled:opacity-50 dark:text-white', 'ml-auto' => $savedAt === ''])>Save</button>
</div>
</div>
Compiling…