Features
Third-party libraries
Select boxes, date pickers, rich editors and charts written for plain JavaScript work in a Nitro component. Write the library's binding once, as an attribute of your own, and Nitro starts it, keeps its value in step with a property, and tears it down, wherever the element comes and goes.
On this page
A library such as select2 or Flatpickr takes over an element: it hides it, builds markup of its own beside it, and reports changes in its own way. Three things make it work with a component that renders again:
- The library owns its markup. Wrap it in
nitro:ignore, so a render never touches what it built. - The value goes both ways. When the visitor chooses, the library sets the property; when anything else changes the property, the library shows it.
- It is torn down. When its element leaves the page, the library's instance goes with it.
A nitro: attribute of your own does all three in one place. Nitro runs it when its element
appears and cleans it up when the element goes, so the same binding works after a visit, in an
@if, in a new row of a list, and after the back button, with no event to listen for.
Binding a library
-
Name the attribute
List it in
config/nitro.php, so the compiler accepts it:PHP// config/nitro.php 'attributes' => ['select2', 'flatpickr', 'tom-select', 'choices'], -
Write the binding
Register a directive of the same name in a script your layout loads, with the library. It receives the element, the attribute's value (here, the property's name) and the component. The last two lines register it whenever the script runs: before Nitro starts, on a page's first load, or after, when a visit brings the layout that loads it:
JavaScript// resources/js/libraries.js: your layout loads it, with the libraries const bindings = (Nitro) => { Nitro.directive('select2', ({ el, value, component }) => { const $el = $(el); // 1. Start it with the property's value. $el.val(component.get(value)).select2(); // 2. The visitor chose: set the property. $el.on('change.nitro', () => component.set(value, $el.val())); // 3. Anything else changed the property: show it (the namespaced event tells select2 only). const stop = component.watch(value, (now) => $el.val(now).trigger('change.select2')); // 4. Leaving the page, an @if, a removed row: tear it down. return () => { stop(); $el.off('change.nitro').select2('destroy'); }; }); // ... the other libraries' bindings }; // Loaded before Nitro (a page's first load): when it starts. Loaded by a visit, after it: now. if (window.Nitro) bindings(window.Nitro); else document.addEventListener('nitro:starting', ({ detail: { Nitro } }) => bindings(Nitro)); -
Use it
Blade<div nitro:ignore> <select nitro:select2="category"> @foreach ($categories as $category) <option value="{{ $category->id }}">{{ $category->name }}</option> @endforeach </select> </div> {{-- A row of a list: bound to its own row. --}} @foreach ($lines as $i => $line) <div nitro:key="line-{{ $i }}" nitro:ignore> <select nitro:select2="lines.{{ $i }}.product">...</select> </div> @endforeach
| The component | Does |
|---|---|
component.get(path)
|
The property's value (
lines.2.product
: a key under it), as plain data
|
component.set(path, value)
|
Sets it, as
nitro:model
does: its
updated*()
hooks run, and the view renders the change
|
component.watch(path, fn)
|
Calls
fn(value)
whenever the value changes from anywhere else: a browser method, the server's answer, a reset, another library on the same property. Not for this binding's own
set()
, so the value never echoes back. Returns a function that stops watching
|
component.call(method, ...args)
|
Runs a method: a browser one returns its value, a
#[Server]
one a promise of it
|
component.dispatch(event, ...)
|
Sends an event, as
$this->dispatch()
does
|
Why the namespaced event
select2 reports a choice with jQuery's change event, and so does your own .trigger(). Listening to change.nitro and triggering change.select2 keeps the two apart: what the visitor did reaches the property, what the property did reaches select2 only. Most libraries have the same: a quiet way to set the value (Flatpickr's setDate(value, false), Tom Select's setValue(value, true)).
What Nitro takes care of
Write the binding once, and these work with no more code:
- A visit.
nitro:navigateto another page tears every library down; back, each starts again, once, with the page's values. There is nonavigatedevent to listen for, and nothing starts twice. The back button too. - Arriving by a visit. From a page of the same layout, the libraries start as the page shows. From a page without them, into a page whose layout loads them, the libraries and your bindings load with the visit, after the page's components started: Nitro runs a binding registered then on the elements already there.
- An
@if. When the block goes, its libraries are torn down; when it comes back, they start with the property's value at that moment. - A list. A new row's library starts, bound to its own row; a removed row's goes.
- Renders. A render of anything else leaves the library and its markup as they are.
- The server. A
#[Server]method that changes the property reaches the library with its answer. - An obfuscated build. The binding uses your property names; Nitro translates them.
Nitro's own browser tests run select2, Flatpickr, Tom Select, Choices, Trix, Quill, SortableJS, noUiSlider and IMask through each of these, every way into the page, with the bindings on this page.
Recipes
Flatpickr
Nitro.directive('flatpickr', ({ el, value, component }) => {
const picker = flatpickr(el, {
dateFormat: 'Y-m-d',
defaultDate: component.get(value),
onChange: (dates, text) => component.set(value, text),
});
const stop = component.watch(value, (now) => picker.setDate(now, false)); // false: no onChange
return () => {
stop();
picker.destroy();
};
});
Tom Select
Nitro.directive('tom-select', ({ el, value, component }) => {
const select = new TomSelect(el, {});
select.setValue(component.get(value), true); // true: silent
select.on('change', (now) => component.set(value, now));
const stop = component.watch(value, (now) => select.setValue(now, true));
return () => {
stop();
select.destroy();
};
});
Choices
Nitro.directive('choices', ({ el, value, component }) => {
const choices = new Choices(el, { shouldSort: false });
choices.setChoiceByValue(component.get(value));
const changed = () => component.set(value, choices.getValue(true));
el.addEventListener('change', changed);
const stop = component.watch(value, (now) => choices.setChoiceByValue(now));
return () => {
stop();
el.removeEventListener('change', changed);
choices.destroy();
};
});
Trix
Trix needs a hidden input beside its editor; the binding makes both, so the view has one element:
// <div nitro:ignore><div nitro:trix="body"></div></div>
Nitro.directive('trix', ({ el, value, component }) => {
const input = Object.assign(document.createElement('input'), { type: 'hidden', id: `trix-${Math.random().toString(36).slice(2)}`, value: component.get(value) ?? '' });
const editor = document.createElement('trix-editor');
editor.setAttribute('input', input.id);
el.append(input, editor);
let loading = false;
const changed = () => loading || component.set(value, input.value);
editor.addEventListener('trix-change', changed);
const stop = component.watch(value, (now) => {
loading = true;
editor.editor.loadHTML(now ?? '');
loading = false;
});
return () => {
stop();
editor.removeEventListener('trix-change', changed);
el.innerHTML = '';
};
});
Quill
Nitro.directive('quill', ({ el, value, component }) => {
const quill = new Quill(el, { theme: 'snow' });
const show = (html) => quill.setContents(quill.clipboard.convert({ html: html ?? '' }), 'silent');
show(component.get(value));
quill.on('text-change', (delta, old, source) => source === 'user' && component.set(value, quill.root.innerHTML));
const stop = component.watch(value, show);
return () => {
stop();
el.innerHTML = '';
};
});
SortableJS
The library moves the list's items itself, so the binding draws them from the property, and the property keeps the order:
// <div nitro:ignore><ul nitro:sortable="order"></ul></div>: the list is drawn here, from the property
Nitro.directive('sortable', ({ el, value, component }) => {
el.innerHTML = component.get(value).map((id) => `<li data-id="${id}">${id}</li>`).join('');
const sortable = Sortable.create(el, { onEnd: () => component.set(value, sortable.toArray()) });
const stop = component.watch(value, (order) => sortable.sort(order, false));
return () => {
stop();
sortable.destroy();
};
});
noUiSlider and IMask
Nitro.directive('slider', ({ el, value, component }) => {
noUiSlider.create(el, { start: component.get(value), step: 1, range: { min: 0, max: 100 } });
el.noUiSlider.on('set', ([now]) => Number(now) !== component.get(value) && component.set(value, Math.round(Number(now))));
const stop = component.watch(value, (now) => el.noUiSlider.set(now));
return () => {
stop();
el.noUiSlider.destroy();
};
});
Nitro.directive('mask', ({ el, value, component }) => {
const mask = IMask(el, { mask: '0000 0000 0000 0000' });
mask.unmaskedValue = component.get(value) ?? ''; // the property holds the digits
mask.on('accept', () => mask.unmaskedValue !== component.get(value) && component.set(value, mask.unmaskedValue));
const stop = component.watch(value, (now) => { mask.unmaskedValue = now ?? ''; });
return () => {
stop();
mask.destroy();
};
});
One component's script: @nitroScript
When a library belongs to one component only, such as a chart, write its code in the component's view.
@nitroScript runs once for each instance of the component, as it goes live:
<div>
<canvas nitro:ref="chart"></canvas>
@nitroScript
<script>
const chart = new Chart($refs.chart, {
type: 'line',
data: { labels: $component.get('months'), datasets: [{ data: $component.get('views') }] },
});
const stop = $component.watch('views', (views) => {
chart.data.datasets[0].data = views;
chart.update();
});
return () => {
stop();
chart.destroy();
};
</script>
@endNitroScript
</div>
- What it can use.
$refs(elements markednitro:ref),$el(the component's root element),$component(the sameget,set,watchas above) and$nitro(the component as$this->js()has it:$nitro.views,$nitro.save()). - Once for each instance. Two charts on a page run it twice, each with its own
$refs. A render never runs it again; a new instance (after a visit, in an@ifthat comes back) does. - Cleaning up. A function it returns runs when the component leaves the page.
- In the root element, outside blocks. Inside an
@ifor a@foreachit is a build error: whether it ran would depend on the block. - Never on the server. The server renders nothing of it, and Blade never reads it, so
{{ }}and@@in your JavaScript are just JavaScript. The<script>tag is optional, for your editor.
Loading the library itself
Load the library (jQuery, select2, Flatpickr), then your bindings, once, in your layout or your Vite bundle. A binding or a @nitroScript only uses the library. A layout of its own can load them in its <head>: a visit into it loads them, in their order.