Essentials
Arrays and models
Most state is arrays or Eloquent models. You read an array with square brackets and a model with an arrow, exactly as in PHP, and both work the same in the browser and on the server.
On this page
A view runs twice: as Blade on the server, and compiled in the browser. So the rules are PHP's rules. An array
is read with $task['title']. A model, or any other object, is read with
$post->title. Nitro keeps track of which values are models, so the browser reads them just
as Laravel does.
Reading arrays and models
Use the syntax that fits the value:
{{-- An array: square brackets --}}
<p>{{ $task['title'] }}</p>
<p>{{ $task['owner']['name'] ?? 'Nobody' }}</p>
{{-- A model: an arrow, and its loaded relations the same way --}}
<p>{{ $post->title }}</p>
<p>{{ $post->author->name }}</p>
<p>{{ $post->editor?->name ?? 'Not edited' }}</p>
- Arrays use square brackets. Nested keys work the same way, as deep as you need.
- Models use an arrow, and so do their loaded relations.
$post['title']also works on a model, as it does in Laravel. ?->and??handle a missing relation or key, as they do in PHP.
An arrow on an array fails
$task->title on an array fails with Attempt to read property "title" on array.
It fails in the browser just as it fails in a Laravel view, so you see the mistake at once, not on some
later render. Use $task['title'] instead.
Working with arrays
Arrays suit data that the visitor changes in the page: form rows, a cart, a draft, a list you filter. Your browser methods change them with the usual PHP, without a request:
#[Validate([
'lines' => 'required|array|max:50',
'lines.*.description' => 'required|max:200',
'lines.*.quantity' => 'integer|min:1|max:999',
'lines.*.price' => 'integer|min:0', // In pence, typed in by the user.
])]
public array $lines = [
['description' => '', 'quantity' => 1, 'price' => 0],
];
public function addLine(): void
{
$this->lines[] = ['description' => '', 'quantity' => 1, 'price' => 0];
}
public function removeLine(int $i): void
{
array_splice($this->lines, $i, 1);
}
#[Computed]
public function total(): int
{
return array_sum(array_map(fn (array $line) => $line['quantity'] * $line['price'], $this->lines));
}
#[Server]
public function save(): void
{
$this->validate();
auth()->user()->invoices()->create([
'lines' => $this->lines,
'total' => $this->total, // Worked out again here, from the validated lines.
]);
}
@foreach ($lines as $i => $line)
<div>
<input nitro:model="lines.{{ $i }}.description">
<input type="number" nitro:model.number="lines.{{ $i }}.quantity">
<span>{{ number_format($line['quantity'] * $line['price'] / 100, 2) }}</span>
<button nitro:click="removeLine($i)">Remove</button>
</div>
@endforeach
<button nitro:click="addLine">Add a line</button>
<p>Total: {{ number_format($this->total / 100, 2) }}</p>
- Write to any depth.
$this->lines[$i]['quantity']++and$this->lines[] = [...]work in browser methods, as in PHP. - Bind any depth.
nitro:model="lines.{{ $i }}.quantity"binds an input to one row's field, on an input or on a child component's tag. - Use PHP's array functions.
array_map(),array_filter(),usort(),array_splice()and the rest run in the browser. - Arrays are values.
$copy = $this->linesmakes a copy, so changing$copynever changes the property. - Keys and their order survive. An array keyed by id stays keyed by id, in its order, on the way to the browser and back.
Validate what the browser changed
The browser can set an array to anything. Give it #[Validate] rules with * for each row, and call $this->validate() before you save. Work out totals again on the server: the browser's total is a preview.
Working with models
Models suit records that live in the database, which your #[Server] methods act on. Load them in
mount(), with the relations the view needs:
use App\Models\Post;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
public Collection $posts;
public function mount(): void
{
$this->posts = Post::with('author')->latest()->take(20)->get();
}
#[Server]
public function publish(Post $post): void
{
Gate::authorize('update', $post);
$post->update(['published_at' => now()]);
}
@foreach ($posts as $post)
<article nitro:key="{{ $post->id }}">
<h3>{{ $post->title }}</h3>
<p>By {{ $post->author->name }}</p>
@if ($post->published_at === null)
<button nitro:click="publish($post)">Publish</button>
@endif
</article>
@endforeach
- The browser gets their visible attributes. Hidden attributes (
$hidden) are never sent. - Load relations with
with(). The browser can't query the database, so a relation you didn't load isn't there. Nitro loads the same relations again on every#[Server]call. - The server reads them again on every call. A model property always comes from the database, never from what the browser sends back.
- Change them on the server. Update a record in a
#[Server]method. The next state the browser gets has the new values.
Passing them to server methods
Pass an array's id, or the model itself. A parameter typed as a model receives the record:
| In the view | The method | Receives |
|---|---|---|
remove($task['id'])
|
remove(int $id)
|
The id. Look the record up yourself |
remove($post->id)
|
remove(Post $post)
|
The record the key names |
remove($post)
|
remove(Post $post)
|
The same record: the browser sends its key |
The key comes from the browser, so authorize the record before you act on it. See Passing arguments.
Choosing between them
Pick by who owns the data:
| When | Use | For example |
|---|---|---|
| The visitor changes it in the page | An array | Form rows, a cart, a draft, a filter |
| It lives in the database and the server acts on it | A model or a collection | Posts to publish, orders to ship |
| A long list the view only shows | An array of the fields it shows | Smaller state, and nothing extra sent |
Turning models into arrays takes one call:
// Models: read again from the database on every #[Server] call.
$this->posts = Post::with('author')->latest()->get();
// Arrays: only the fields the view needs, and the browser may change them.
$this->posts = Post::latest()->get(['id', 'title'])->toArray();