Essentials
Browser classes and enums
Browser classes let your components share code, such as formatting money or a pricing rule. Mark a class or an enum with #[Browser], and your browser methods and views can use it just as your PHP does.
On this page
Code that several components need doesn't have to be copied into each of them, or moved into a
#[Server] method that costs a request. Put it in a class of its own, mark the class with
#[Browser], and Nitro compiles it for the browser too. On the server, it stays an ordinary PHP
class.
- 2 × Notebook £9.00
- 1 × Fountain pen £24.99
- Total £33.99
Next: Packed · Not saved yet
The order's status is a #[Browser] enum, and its prices are formatted by a
#[Browser] class. Pick a status or click Next step: the label and the next step
change in the browser, with no request. Save on the server sends the status to a
#[Server] method. Reload the page, and the status you saved is still there.
Creating a browser class
Add the #[Browser] attribute to a class. Its static methods and its constants are then available
in the browser:
<?php
namespace App\Support;
use Nitro\Attributes\Browser;
#[Browser]
final class Money
{
public const CURRENCY = '£';
/** Formats an amount in pence, such as 1250 as "£12.50". */
public static function format(int $pence): string
{
return self::CURRENCY.number_format($pence / 100, 2);
}
}
You can call it from any browser method, as you would in PHP:
use App\Support\Money;
public function label(): string
{
return Money::format($this->total);
}
- The same rules as browser methods. A browser class can't use the database, the clock or anything else that needs the server. If one of its methods does, the build fails and tells you the file and line.
- Only what you use is compiled. Nitro compiles the methods your browser code calls, and the methods they call in turn. A method that nothing calls in the browser is never compiled, so it can use anything.
- Constants become their values.
Money::CURRENCYis compiled as'£'. - Private methods and constants work inside their own class, with
self::. - Static methods only. You can't create instances (
new Money(...)) or use static properties in the browser.
Using a class in a view
A view has no use statements, so import the classes it needs with Blade's @use
directive. Nitro reads @use exactly as Blade does:
{{-- One class --}}
@use('App\Support\Money')
{{-- One class, under another name --}}
@use('App\Support\Money', 'Cash')
{{-- Several classes from one namespace --}}
@use('App\Enums\{OrderStatus, Priority}')
<p>{{ Money::format($total) }}</p>
An import applies to the whole view, as a PHP use statement does, and an included view can
import its own. Without @use, write the class's full name, such as
\App\Support\Money::format($total).
Using enums
An enum marked with #[Browser] works in the browser just as it does in PHP. You can use its
cases, cases(), from(), tryFrom(), its methods and its static methods:
<?php
namespace App\Enums;
use Nitro\Attributes\Browser;
#[Browser]
enum OrderStatus: string
{
case Placed = 'placed';
case Packed = 'packed';
case Shipped = 'shipped';
case Delivered = 'delivered';
public function label(): string
{
return match ($this) {
self::Placed => 'Placed',
self::Packed => 'Packed',
self::Shipped => 'On its way',
self::Delivered => 'Delivered',
};
}
public function next(): self
{
return match ($this) {
self::Placed => self::Packed,
self::Packed => self::Shipped,
default => self::Delivered,
};
}
public function isFinal(): bool
{
return $this === self::Delivered;
}
}
OrderStatus::Shipped->label(); // "On its way"
OrderStatus::from('packed'); // OrderStatus::Packed
OrderStatus::tryFrom('lost'); // null
count(OrderStatus::cases()); // 4
$status === OrderStatus::Delivered; // compared as in PHP
Each case is a single object in the browser, so ===, match and
in_array(..., true) compare cases the way PHP does, and ->name and
->value work as usual.
Using an enum as a property
A component property can hold a backed enum:
use App\Enums\OrderStatus;
public OrderStatus $status = OrderStatus::Placed;
public function advance(): void
{
$this->status = $this->status->next();
}
@use('App\Enums\OrderStatus')
<select nitro:model="status">
@foreach (OrderStatus::cases() as $case)
<option value="{{ $case->value }}">{{ $case->label() }}</option>
@endforeach
</select>
<p>{{ $status->label() }}</p>
nitro:modelworks as usual. A<select>sets the property from the selected option's value, and your code reads it as a case.- The server receives a case. The property travels as its value, and the server turns it back into the case before your methods run.
- The browser can only send real cases. A value that isn't one of the enum's cases, or isn't of its backing type, is refused with a 403.
- The enum must be backed and marked
#[Browser]. If it isn't, the build tells you.
Methods on a value
When you call a method on a value, such as $case->label(), Nitro compiles that method for the #[Browser] enums your code uses. In the browser, only an enum case has methods. Data from the server arrives as arrays, so calling a method on it fails the build.
The component
<?php
namespace App\Nitro\Components;
use App\Enums\OrderStatus;
use Nitro\Attributes\Computed;
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
use Nitro\Component;
class OrderTracker extends Component
{
/** The order's lines, with prices in pence. The server sets them, so the browser can't change them. */
#[Locked]
public array $lines = [
['name' => 'Notebook', 'price' => 450, 'quantity' => 2],
['name' => 'Fountain pen', 'price' => 2499, 'quantity' => 1],
];
public OrderStatus $status = OrderStatus::Placed;
#[Locked]
public string $savedAt = '';
public function mount(): void
{
$this->status = OrderStatus::from(session('order-tracker.status', 'placed'));
$this->savedAt = session('order-tracker.saved_at', '');
}
#[Computed]
public function total(): int
{
return array_sum(array_map(fn (array $line) => $line['price'] * $line['quantity'], $this->lines));
}
public function advance(): void
{
$this->status = $this->status->next();
}
#[Server]
public function save(): void
{
$this->savedAt = now()->format('H:i:s');
// A demo: kept in the session. Your app would save to the database.
session(['order-tracker.status' => $this->status->value, 'order-tracker.saved_at' => $this->savedAt]);
}
}
@use('App\Enums\OrderStatus')
@use('App\Support\Money')
<div class="w-full max-w-md space-y-4 text-sm">
<ul class="divide-y divide-slate-200 dark:divide-white/10">
@foreach ($lines as $line)
<li class="flex justify-between py-1.5">
<span>{{ $line['quantity'] }} × {{ $line['name'] }}</span>
<span class="tabular-nums">{{ Money::format($line['price'] * $line['quantity']) }}</span>
</li>
@endforeach
<li class="flex justify-between py-1.5 font-semibold">
<span>Total</span>
<span class="tabular-nums">{{ Money::format($this->total) }}</span>
</li>
</ul>
<div class="flex flex-wrap items-center gap-3">
<select nitro:model="status" class="rounded-lg bg-white px-2.5 py-1.5 ring-1 ring-slate-300 dark:bg-slate-900 dark:ring-white/15" aria-label="Status">
@foreach (OrderStatus::cases() as $case)
<option value="{{ $case->value }}">{{ $case->label() }}</option>
@endforeach
</select>
<button type="button" nitro:click="advance" @disabled($status->isFinal()) class="rounded-lg px-3 py-1.5 font-semibold ring-1 ring-slate-300 disabled:opacity-40 dark:ring-white/15">
Next step
</button>
<button type="button" nitro:click="save" nitro:loading.attr="disabled" class="rounded-lg bg-slate-900 px-3 py-1.5 font-semibold text-white disabled:opacity-50 dark:bg-white dark:text-slate-900">
Save on the server
</button>
</div>
<p class="text-slate-500">
{{ $status->isFinal() ? 'This order is complete.' : 'Next: '.$status->next()->label() }}
· {{ $savedAt === '' ? 'Not saved yet' : 'Saved at '.$savedAt.': reload, it stays' }}
</p>
</div>
Compiling…
Saved in the session, for this demo
So the example works for every visitor without an account or a database, save() keeps the status in your session. Your app would save it to the database in the same way.