Skip to main content
WireKit
Copy for LLM

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:

  • filters renders between the input and the list. Tab moves from the input into it.
  • loading shows while your request is out.
  • error shows 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

A remote search with a filter row, a loading state and an error state

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) + '&section=' + 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:

  • loading and error take plain content. The palette announces both through a role="status" region of its own, which is always in the page so a change inside it is read out. A spinner or anything else with its own role="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.

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store