Nitro

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:

PHP
// 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:

resources/js/autosize.js
/**
 * 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);
    });
});
Blade
<textarea nitro:model="reply" nitro:autosize="8"></textarea>
nitro:autosize live, in this page View source ↓

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. el is the element, value is the attribute's value as a string, and modifiers is a list of the words after its name, such as .top in nitro: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 as click or model.

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:

JavaScript
Nitro.directive('sortable', ({ el, component }) => {
    const list = makeSortable(el, {
        onEnd: (ids) => component.dispatch('reordered', ids),
    });

    return () => list.destroy();
});
PHP Browser
#[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:

JavaScript
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 component or store listener also hears the components and stores already on the page.
  • Headers. A request listener 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:

app/Nitro/Features/AuditCalls.php Server
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:

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, so updated() 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 returns false. An error a feature adds to $component->nitro()->errors reaches 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:

app/Nitro/Attributes/Throttle.php Server
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);
    }
}
app/Nitro/Components/Contact.php Server
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 in config/nitro.php. So a property's updated() 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.

Next steps