Going further
Extending Nitro
Nitro has four ways for you to add to it: your own nitro: attributes, plugins in the browser, features that run around every component on the server, and attributes that work as features.
On this page
Each one is small on its own. Use your own attributes for behaviour in the page, such as a tooltip or a textarea that grows. Use plugins to listen to the runtime in the browser. Use features and attribute classes to run code around your components on the server.
Adding your own attributes
A nitro: attribute of your own takes two steps. First, list its name in
config/nitro.php, so the compiler accepts it:
// config/nitro.php
'attributes' => ['autosize'],
Then register a directive with the same name in your JavaScript. The directive receives the element, the attribute's value and its modifiers, and it can return a function that cleans up:
/**
* nitro:autosize: a textarea that grows with its text. Its value is the most lines it grows to,
* nitro:autosize="8"; past that, it scrolls.
*
* This file loads before Nitro does, so it registers the directive when Nitro is starting.
*/
document.addEventListener('nitro:starting', ({ detail: { Nitro } }) => {
Nitro.directive('autosize', ({ el, value }) => {
const style = getComputedStyle(el);
const line = parseFloat(style.lineHeight) || 20;
const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
const max = (Number(value) || Infinity) * line + padding;
const fit = () => {
el.style.height = 'auto';
el.style.height = `${Math.min(el.scrollHeight, max)}px`;
el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden';
};
fit();
el.addEventListener('input', fit);
return () => el.removeEventListener('input', fit);
});
});
<textarea nitro:model="reply" nitro:autosize="8"></textarea>
280 characters left
Type a few lines into the box. It grows with your text, up to eight lines, and then it scrolls.
- When it runs. The directive runs when its element appears. It runs again when the attribute's value or modifiers change, and not on other renders. The value is rendered like any attribute, so it can contain
{{ }}. - What it receives.
elis the element,valueis the attribute's value as a string, andmodifiersis a list of the words after its name, such as.topinnitro:tooltip.top. To pass data, use@json(...)in the value. - Cleaning up. The function the directive returns runs before the directive runs again, and when the element or its component leaves the page.
- Mistakes are caught. A
nitro:attribute that isn't listed fails the build. You can't list a name Nitro uses itself, such asclickormodel.
Registering a directive
A script that runs after @nitroScripts can call Nitro.directive() directly. A
script that runs before it, such as a Vite module in the page's <head>, should register in
a nitro:starting listener, as the example above does. Nitro fires that event on
document just before any component starts. A directive you register later still runs on the
elements already on the page.
Talking to the component
A directive also receives component, by your property and method names:
get() and set() a property, watch() it, call() a method,
or dispatch() an event, as $this->dispatch() does, for any #[On] method to
hear. Third-party libraries binds a library's value with them;
here, an event:
Nitro.directive('sortable', ({ el, component }) => {
const list = makeSortable(el, {
onEnd: (ids) => component.dispatch('reordered', ids),
});
return () => list.destroy();
});
#[On('reordered')]
public function reorder(array $ids): void
{
$this->order = $ids;
}
Writing plugins
A plugin listens to Nitro's runtime in the browser: its events, by name, with on(). Pass a function to Nitro.use(), and Nitro
calls it once with the plugin API:
Nitro.use(({ on }) => {
on('request', ({ headers }) => {
headers['X-Tenant'] = document.body.dataset.tenant;
});
on('response', ({ method, status }) => {
if (status >= 500) reportToMonitoring(method, status);
});
});
| Event | Called with |
|---|---|
component
|
A component started in the page:
{ name, el, dispatch(event, ...params) }
|
render
|
The same component, after each render |
store
|
A store was made in this tab:
{ name, persist, sync, get(property), set(property, value), onChange(listener) }
|
request
|
Before a component's request: a
#[Server]
call (
method
is its name), a partial reload (
$reload
) or an upload (
$upload
):
{ component, method, headers }
|
response
|
When the answer arrives:
{ component, method, status }
, with
0
when none arrived
|
- Removing a listener.
on()returns a function that removes the listener. - Late plugins catch up. A
componentorstorelistener also hears the components and stores already on the page. - Headers. A
requestlistener can add headers, but it can't change Nitro's own, such as the CSRF token. - Store values are JSON-safe, in the form the server sends them.
set()counts as the browser's own change, so the server checks it on the next call. - Errors are contained. A listener that throws is reported with
nitro:error, and the other listeners still run. - Names are the browser's. In an obfuscated build, methods and properties go by their aliases.
Nitro uses this API too
#[Persist] and #[Sync], which keep a store's state and share it between tabs, are plugins built only on this API.
Running code around every component
A feature runs on the server, around every component. Use one for things a single component's hooks can't
do, such as auditing every #[Server] call or setting a tenant on every component. Extend
Nitro\Feature and override the hooks you need:
namespace App\Nitro\Features;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Nitro\Component;
use Nitro\Feature;
class AuditCalls extends Feature
{
public function __construct(private readonly Request $request) {}
public function called(Component $component, string $method, mixed $result): void
{
Log::info('Nitro call', [
'component' => $component::class,
'method' => $method,
'user' => $this->request->user()?->id,
]);
}
}
Then list it in config/nitro.php:
// config/nitro.php
'features' => [App\Nitro\Features\AuditCalls::class],
| Hook | Runs |
|---|---|
boot($component)
|
On every request, when the component is created or restored |
mounting($component)
|
For a new component, before
mount()
|
hydrate($component)
|
On a
#[Server]
call, once the state is restored
|
updated($component, $changed)
|
On a
#[Server]
call, once the browser's changes are set. Return
false
to stop the method from running
|
calling($component, $method, $arguments)
|
Before a
#[Server]
method runs
|
called($component, $method, $result)
|
After a
#[Server]
method returns
|
dehydrate($component)
|
Before the state is sent to the browser, once per request |
- Order. Each hook runs after the component's own method of the same name, such as
boot(). Your features run after Nitro's own, soupdated()can see what#[Validate]found in$component->nitro()->errors. - Services. The container makes each feature once per request, so its constructor can take services.
- Stores run features too, since a store is a component.
- Stopping a method. Every feature's
updated()runs, even after one returnsfalse. An error a feature adds to$component->nitro()->errorsreaches the browser like a validation message.
Writing attributes that work as features
A feature can also be an attribute that runs only where you place it. Extend
Nitro\FeatureAttribute, and $this->target() tells you the property or
method it's on:
namespace App\Nitro\Attributes;
use Attribute;
use Illuminate\Support\Facades\RateLimiter;
use Nitro\Component;
use Nitro\FeatureAttribute;
#[Attribute(Attribute::TARGET_METHOD)]
class Throttle extends FeatureAttribute
{
public function __construct(public int $perMinute = 10) {}
public function calling(Component $component, string $method, array $arguments): void
{
$key = 'nitro:'.$component::class.':'.$method.':'.request()->ip();
abort_if(RateLimiter::tooManyAttempts($key, $this->perMinute), 429);
RateLimiter::hit($key);
}
}
use App\Nitro\Attributes\Throttle;
#[Server]
#[Throttle(perMinute: 3)]
public function send(): void
{
Mail::to($this->email)->send(new ContactMessage($this->message));
}
| Placed on | Its hooks |
|---|---|
A
#[Server]
method
|
calling()
and
called()
run for that method only
|
| A property |
updated()
runs only when the browser changed that property
|
| The class | Every hook runs |
- Browser methods are refused. A browser method never reaches the server, so an attribute class on one fails the build.
- Order. Attribute classes run after the component's own hooks, and before
#[Validate]and the features inconfig/nitro.php. So a property'supdated()can tidy a value before it's validated. - One per component. Each component gets its own instance, made from the attribute's arguments rather than by the container.
Nitro's #[Url] is one
#[Url] is itself an attribute class: its mounting() hook reads the property's value from the query string.