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:
// 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.
// 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()
|
// 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).
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.
-
Name the engine in the config
An entry in
nitro.views: where its views are and their extensions. Without anadapter, its views hold no PHP.// config/nitro.php 'views' => [ // Vue, React, Solid and Svelte, as published... 'chart' => ['path' => resource_path('js/charts'), 'extensions' => ['js']], ], -
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); } } -
Register the engine before components start
load(view)gives the view's module, by its name.mount(module, el, handle)renders it inel, 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(), }; }, }); });
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.