Essentials
Methods
A component's methods run in the browser. A click changes the state and the page at once, without a request. Only the methods you mark #[Server] go to the server.
On this page
You write methods the same way you write any PHP method. Nitro compiles them to JavaScript, so a click, a keystroke or a form submission runs your code right there in the browser. The view then renders again with whatever changed. Everything in the cart below happens without the server.
- The cart is empty
- Subtotal
- $0
- Delivery
- free
- Total
- $0
The component
add() and remove() change $items. subtotal() and
delivery() are marked #[Computed]. updatedPostcode() runs after the
postcode field changes. Open Compiled to see the JavaScript that each one becomes.
The browser previews, the server decides
The totals here are only a preview for the visitor, because anything the browser sends can be changed. Work out any prices and totals that money depends on again on the server, from the database, in the #[Server] method that takes the order.
<?php
namespace App\Nitro\Components;
use Nitro\Attributes\Computed;
use Nitro\Component;
class Cart extends Component
{
public array $products = [
['name' => 'Notebook', 'price' => 4],
['name' => 'Pen', 'price' => 2],
['name' => 'Backpack', 'price' => 35],
];
public array $items = [];
public string $postcode = '';
public function add(int $index): void
{
$this->items[] = $this->products[$index];
}
public function remove(int $index): void
{
array_splice($this->items, $index, 1);
}
public function updatedPostcode(string $value): void
{
$this->postcode = strtoupper(preg_replace('/\s+/', ' ', trim($value)));
}
#[Computed]
public function subtotal(): int
{
return array_sum(array_column($this->items, 'price'));
}
#[Computed]
public function delivery(): int
{
return $this->subtotal === 0 || $this->subtotal >= 30 ? 0 : 5;
}
}
<div class="grid w-full max-w-lg gap-4 text-sm sm:grid-cols-2">
<div class="space-y-2">
@foreach ($products as $i => $product)
<button type="button" nitro:click="add($i)" class="flex w-full items-center justify-between rounded-lg bg-white px-3 py-2 ring-1 ring-slate-200 hover:ring-blue-400 dark:bg-slate-900 dark:ring-white/10">
<span>{{ $product['name'] }}</span>
<span class="text-slate-500">${{ $product['price'] }} +</span>
</button>
@endforeach
</div>
<div class="rounded-lg bg-white p-3 ring-1 ring-slate-200 dark:bg-slate-900 dark:ring-white/10">
<ul class="space-y-1">
@forelse ($items as $i => $item)
<li class="flex justify-between">
<span>{{ $item['name'] }}</span>
<button type="button" nitro:click="remove($i)" class="text-slate-400 hover:text-red-500">remove</button>
</li>
@empty
<li class="text-slate-400">The cart is empty</li>
@endforelse
</ul>
<dl class="mt-3 space-y-1 border-t border-slate-200 pt-3 dark:border-white/10">
<div class="flex justify-between"><dt>Subtotal</dt><dd>${{ $this->subtotal }}</dd></div>
<div class="flex justify-between"><dt>Delivery</dt><dd>{{ $this->delivery === 0 ? 'free' : '$'.$this->delivery }}</dd></div>
<div class="flex justify-between font-semibold text-slate-900 dark:text-white"><dt>Total</dt><dd>${{ $this->subtotal + $this->delivery }}</dd></div>
</dl>
@if ($this->subtotal > 0 && $this->subtotal < 30)
<p class="mt-2 text-xs text-slate-500">Free delivery from $30: ${{ 30 - $this->subtotal }} to go</p>
@endif
<input nitro:model.blur="postcode" placeholder="Postcode (try sw1a 1aa)" class="mt-3 w-full rounded-md bg-transparent px-2 py-1 ring-1 ring-slate-300 outline-none focus:ring-blue-500 dark:ring-white/15">
</div>
</div>
Compiling…
Calling methods from the view
An event attribute names a method, or calls it with arguments. The arguments are expressions from the view,
such as the loop's $i. Actions like $set and $toggle change a
property without a method of your own.
<button nitro:click="add">Add</button>
<button nitro:click="remove($i)">Remove</button>
<button nitro:click="$set('postcode', '')">Clear the postcode</button>
<button nitro:click="$toggle('open')">Details</button>
Any DOM event works, with modifiers such as .prevent and .enter. See
Directives for the full list.
Computing values
You read a #[Computed] method as if it were a property, as $this->subtotal, both
in the class and in the view. Nitro works it out once and keeps the result until the state changes. A
computed method takes no parameters. It isn't part of the state, so it's never sent anywhere.
Compute values instead of storing them
A total, a filtered list or a count is better computed than kept in a property. It always matches the state, and the state stays small.
Reacting to changes
updatedPostcode($value) runs in the browser after nitro:model or
$set changes $postcode. updated($property, $value) runs after any such
change. The cart uses it to tidy what was typed. Type sw1a 1aa in the postcode field and
leave it to see the result.
Helper methods
Every component has these methods:
| Call | What it does |
|---|---|
$this->reset('prop', ...)
|
Puts properties back to their defaults |
$this->js('...')
|
Runs JavaScript after the next render, with
$nitro
,
$refs
and
$el
|
$this->dispatch('event', ...)
|
Dispatches an event to other components (see Events) |
PHP in the browser
The compiled code behaves the way PHP does. Arrays are values, integers stay integers, and functions such as
array_splice(), preg_replace() and strtoupper() give PHP's results.
You can use operators, casts, and PHP's string, array, math, ctype, URL and preg_* functions, as
well as 54 of Laravel's Str:: helpers. All of them are tested against PHP and Laravel.
Code that can't run in a browser, such as a facade, a model query, new or a function the
browser doesn't have, fails the build. The error names the file and line and tells you what to do, so you
find out before your visitors do. Move that code to a
server method.
Paginating and sorting
The Nitro\Concerns\WithPagination and WithSorting traits page and sort an
array in the browser:
use Nitro\Concerns\WithPagination;
use Nitro\Concerns\WithSorting;
class Posts extends Component
{
use WithPagination, WithSorting;
public array $posts = [];
public string $search = '';
#[Computed]
public function rows(): array
{
$found = array_filter($this->posts, fn ($post) => str_contains(strtolower($post['title']), strtolower($this->search)));
return $this->paginate($this->sorted($found), 12);
}
public function updatedSearch(): void // A new search starts on the first page.
{
$this->resetPage();
}
}
<th nitro:click="sortBy('title')">Title {{ $this->sortIcon('title') }}</th>
...
@foreach ($this->rows['items'] as $post)
...
@endforeach
<p>{{ $this->rows['from'] }}–{{ $this->rows['to'] }} of {{ $this->rows['total'] }}</p>
<button nitro:click="previousPage">Previous</button>
<button nitro:click="nextPage">Next</button>
paginate()returnsitems,page,pages,total,fromandto. You move between pages withgotoPage(),previousPage(),nextPage()andresetPage(), and the page number is kept in the query string.sorted()works withsortBy('name'), which reverses the order when you call it again, and withsortIcon('name').- Call
resetPage()when a filter changes, so that a new search starts from the first page.