Nitro

Going further

JavaScript API

Your components need no JavaScript of your own. When the rest of your page does, such as analytics, an error tracker or a widget, the Nitro object and the runtime's browser events let it work with your components.

On this page

The runtime puts one object on the page, window.Nitro, and dispatches browser events as things happen. This page lists both. To add your own nitro: attributes or listen to every component, see Extending Nitro.

Running code before components start

A script loaded before @nitroScripts can't use Nitro yet. Listen for nitro:starting on the document instead. It fires once, after every script has loaded and before any component starts:

JavaScript
// resources/js/app.js, loaded before @nitroScripts
document.addEventListener('nitro:starting', ({ detail: { Nitro } }) => {
    Nitro.directive('autosize', autosize);    // Before any component starts.
    Nitro.use(analytics);
});

The Nitro object

Does
Nitro.dispatch(event, params) Sends an event to every #[On] method that listens for it, and to the window, as $this->dispatch() does. params is a list
Nitro.visit(url) Goes to a page the way a nitro:navigate link does, without reloading
Nitro.components() The components on the page
Nitro.find(id) One component, by its nitro:id , or null
Nitro.directive(name, fn) Registers your own nitro: attribute
Nitro.use(plugin) Adds a plugin that runs around every component
Nitro.engine(name, { load, mount }) Adds a view engine: components whose view is written for it. Vue, React, Solid and Svelte have theirs; see "A view engine of your own" below

Registering a directive and Writing plugins cover the last two.

JavaScript
// Send an event to every #[On('cart-updated')] method, and to the window.
Nitro.dispatch('cart-updated', [3]);

// Go to a page the way a nitro:navigate link does.
Nitro.visit('/orders');

// The components on the page.
for (const component of Nitro.components()) {
    console.log(component.name, component.el, component.unsaved());
}

A component

What Nitro.components() and Nitro.find() return has these members:

Member What it is
id Its id, the element's nitro:id
name Its name, such as counter or pages::orders . An obfuscated build uses short aliases instead
el Its root element
dispatch(event, params) Sends an event from it, as its $this->dispatch() does
unsaved(paths) The properties (or paths) changed in the browser that the server hasn't seen yet, as nitro:dirty shows them. Without paths , all of them

Listening for events

The runtime dispatches these events on the window. Each one carries its details in event.detail:

Event Detail When
nitro:request component , method A #[Server] call starts
nitro:response component , method , status Its answer arrives
nitro:error component , message , and status and body for a failed request Something failed: a call, a render, a handler
nitro:expired component , status The session or CSRF token expired (419). Nitro reloads the page, unless you call preventDefault()
nitro:navigating url A visit starts
nitro:navigated url A visit has shown the new page
nitro:flash flash Flash data arrived, as Nitro::flash() sent it
Your own events params , component A component called $this->dispatch() without ->to() or ->self()
JavaScript
// A component's $this->dispatch('cart-updated', count: 3) reaches the window too.
window.addEventListener('cart-updated', (event) => {
    badge.textContent = event.detail.params[0];
});

// Report failed server calls to your error tracker.
window.addEventListener('nitro:error', ({ detail }) => {
    Sentry.captureMessage(`${detail.component}: ${detail.message}`);
});

// Count page views after each visit.
window.addEventListener('nitro:navigated', ({ detail }) => {
    plausible('pageview', { u: detail.url });
});

// Ask before reloading when the session has expired.
window.addEventListener('nitro:expired', (event) => {
    event.preventDefault();
    showSessionExpiredDialog();
});

Upload events

A file input with nitro:model dispatches its own events, which bubble up from the input: nitro-upload-start, nitro-upload-progress (with progress, from 0 to 100), nitro-upload-finish and nitro-upload-error (with status and message).

JavaScript
photoInput.addEventListener('nitro-upload-progress', (event) => {
    bar.style.width = `${event.detail.progress}%`;
});

A view engine of your own

Vue, React, Solid and Svelte views run on engines Nitro provides. For anything else, register your own: a charting library, a map, a canvas game, a framework Nitro has no adapter for. The component keeps its PHP class as always. Only its view is yours to render.

  1. Name the engine in the config

    An entry in nitro.views: where its views are and their extensions. Without an adapter, its views hold no PHP.

    // config/nitro.php
    'views' => [
        // Vue, React, Solid and Svelte, as published...
        'chart' => ['path' => resource_path('js/charts'), 'extensions' => ['js']],
    ],
    
  2. Name the view on the class

    #[View('views', engine: 'chart')]
    class ViewsChart extends Component
    {
        /** @var list<array{label: string, views: int}> */
        #[Locked]
        public array $months = [];
    
        public function mount(): void
        {
            $this->months = Post::monthlyViews(now()->year);
        }
    }
    
  3. Register the engine before components start

    load(view) gives the view's module, by its name. mount(module, el, handle) renders it in el, and returns what the component tells it as it changes.

    // resources/js/app.js, loaded before @nitroScripts
    const charts = import.meta.glob('./charts/*.js');
    
    document.addEventListener('nitro:starting', ({ detail: { Nitro } }) => {
        Nitro.engine('chart', {
            load: (view) => charts[`./charts/${view}.js`]().then((module) => module.default),
            mount(draw, el, handle) {
                const chart = draw(el, handle);
    
                return {
                    update: () => chart.redraw(),
                    unmount: () => chart.destroy(),
                };
            },
        });
    });
    
resources/js/charts/revenue.js
import Chart from 'chart.js/auto';

/** The revenue chart: drawn from the component's months, and again when they change. */
export default function revenue(el, nitro) {
    const canvas = el.appendChild(document.createElement('canvas'));
    const data = () => {
        const months = nitro.get('months');

        return {
            labels: months.map((month) => month.label),
            datasets: [{ label: 'Revenue', data: months.map((month) => month.cents / 100) }],
        };
    };
    const chart = new Chart(canvas, { type: 'bar', data: data() });

    return {
        redraw() {
            chart.data = data();
            chart.update();
        },
        destroy() {
            chart.destroy();
        },
    };
}

update(names) is called with the names of what changed: the properties, "$errors" for the validation messages, "$unsaved" when the server has taken the changes, or "*" for everything. unmount() is called when the component leaves the page.

The handle

A view reads and changes its component through the handle, by the PHP's names, in plain JavaScript values:

Member Does
get(path) A property's value, a key under it ( "form.email" ), or a #[Computed] value
set(path, value) Writes a property, or a key under it, as nitro:model does
watch(path, fn) Calls fn(value) whenever the value changes from anywhere but this handle's own set() ; returns a function that stops watching
call(method, ...args) Runs a method. A browser method returns its value; a #[Server] one, a promise of it
errors() The validation messages, by field
unsaved() The properties changed here that the server hasn't seen yet
dispatch(event, ...params) Sends an event, as $this->dispatch() does
properties , methods , server , computed The names
id , component The component's id, and the component itself, as Nitro.find() gives it

With server rendering on, an engine of your own still works: the render service skips it, and its views render in the browser.

Next steps