Command Palette with a Remote Search
This page builds a <x-wirekit::command-palette> whose results
come from a server.
A search that asks a server has two answers the empty state cannot give: the results are not here yet, or the request failed. And the controls that narrow such a search belong above the results, not beneath them. Three slots cover both:
filtersrenders between the input and the list. Tab moves from the input into it.loadingshows while your request is out.errorshows when it failed.
The palette cannot see your request, so you report where it stands with the
wirekit:command-palette-state event. Open the demo and type: the loading line shows until
the answer arrives. Switch the section to narrow the list, and type offline to see the
failure. The demo only pretends to ask a server; Show Code has the markup of a real search,
and the factory it calls follows below.
The Demo
The Search Factory
The factory holds the request. A newer query aborts the older one, so a slow answer never overwrites a fast one, and a failure is reported, so the error slot shows instead of "nothing matched". Register it before Alpine starts:
// 1. In a script loaded with `defer`, so it runs before Alpine starts.
document.addEventListener('alpine:init', () => {
Alpine.data('siteSearch', () => ({
section: 'all',
term: '',
results: [],
controller: null,
// 2. Ask the server for one query. A newer query aborts this one.
async search(term) {
this.term = term;
this.controller?.abort();
this.controller = new AbortController();
this.$dispatch('wirekit:command-palette-state', { state: 'loading' });
try {
const url = '/search?q=' + encodeURIComponent(term) + '§ion=' + this.section;
const response = await fetch(url, { signal: this.controller.signal });
if (! response.ok) {
throw new Error('Search answered ' + response.status);
}
this.results = await response.json();
this.$dispatch('wirekit:command-palette-state', { state: 'idle' });
} catch (error) {
// 3. A request a newer query aborted reports nothing.
if (error.name === 'AbortError') {
return;
}
this.results = [];
this.$dispatch('wirekit:command-palette-state', { state: 'error' });
}
},
// 4. A new section asks again for the same words.
narrow(section) {
this.section = section;
this.search(this.term);
},
}));
});
Reporting Where the Request Stands
The event takes one of three states:
state |
What the palette does |
|---|---|
loading |
Shows the loading slot, marks the list aria-busy="true", and hides the empty state |
error |
Shows the error slot and hides the empty state |
idle |
Shows neither. The empty state is back to showing whenever the list holds no option |
Any other value counts as idle, so a palette never stays in the loading state because of an
unexpected word. The event is page-wide, like wirekit-command-palette-show: dispatch it
without naming a palette. Opening the palette resets the state to idle before it announces
the empty query, so a request you start in answer to that query keeps its loading.
From a Livewire component, dispatch the same event with named arguments:
// 1. The request failed on the server — show the error slot.
$this->dispatch('wirekit:command-palette-state', state: 'error');
The Three Slots
A few things to know about them:
loadinganderrortake plain content. The palette announces both through arole="status"region of its own, which is always in the page so a change inside it is read out. Aspinneror anything else with its ownrole="status"inside it would be announced twice.- Put mutually exclusive filters in a
segmented-control. It is one Tab stop, and the arrow keys switch the section. - The list keys belong to the input. ArrowUp, ArrowDown, Home, End and Enter move through the results only while the input has focus. A control in the filter row or the footer keeps its own keys.
Further Reading
- Command Palette — every prop, slot and keyboard interaction of the component.
- Overlay Events — the palette's show, close, query and state events beside the other overlays.