Skip to content

Instantly share code, notes, and snippets.

@wsydney76
Last active May 24, 2026 15:39
Show Gist options
  • Select an option

  • Save wsydney76/3ea76e02b1dc1ab325e859c049178473 to your computer and use it in GitHub Desktop.

Select an option

Save wsydney76/3ea76e02b1dc1ab325e859c049178473 to your computer and use it in GitHub Desktop.
Livewire search component for Craft, handcrafted, explained by AI...
<?php
/**
* Full-Page Livewire Component: Craft Search Demo
*
* In Livewire v4, you can define a component's PHP class and its Blade template
* in a single file — this is called a "Single-File Component" (or "Volt" style).
*
* The anonymous class below IS the Livewire component. Livewire reads this class,
* keeps it alive on the server between requests, and automatically syncs its
* public properties with the browser — no boilerplate controller needed.
*
* How a Livewire component request cycle works:
* 1. On the initial page load, Livewire renders the component to HTML and sends it.
* 2. When the user interacts (types, clicks, etc.), Livewire sends an AJAX request
* to the server with the updated data.
* 3. The server re-runs the relevant methods,re-renders the component and sends back updated HTML.
* 4. Livewire then morphs the existing DOM in the browser, updating only the elements that changed — without a full page reload."
*/
// Standard PHP use-statements so we can reference these classes by their short names.
use CraftCms\Cms\Entry\Elements\Entry;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\Attributes\Url;
use Livewire\WithPagination;
/**
* #[Title('Craft Search Demo')]
*
* This PHP attribute sets the <title> tag of the page automatically.
* Livewire handles the document title update when the component is rendered
* as a full-page component (i.e. returned directly from a route or controller).
*/
new #[Title('Craft Search Demo')] class extends Component {
/**
* WithPagination
*
* This trait adds pagination support to the component. It provides:
* - A `$page` property that tracks the current page number.
* - A `resetPage()` method to jump back to page 1.
* - Integration with Livewire so pagination links work reactively
* without a full page reload.
*/
use WithPagination;
/**
* #[Url] — Syncs this property with the browser's URL query string.
*
* When $search changes, Livewire automatically updates the URL to
* e.g. ?search=craft so the user can share or bookmark the search.
* On page load, if ?search=... is present in the URL, Livewire will
* pre-populate this property with that value.
*
* #[Validate(...)] — Declares a validation rule for this property.
*
* The rule 'not_regex:/ or /' rejects any search string containing
* the lowercase word " or " — reminding the user to use uppercase "OR"
* (as required by Craft CMS's search query syntax).
*
* The `message` key provides the human-readable error message shown
* in the template when validation fails.
*
* Public properties in Livewire are reactive: whenever $search changes
* (because the user typed into the bound input), Livewire automatically
* schedules a re-render of the component on the server.
*/
#[Url]
#[Validate('not_regex:/ or /', message: 'The operator "or" must be uppercase.')]
public string $search = '';
/**
* mount() — Livewire's lifecycle hook, called once when the component
* is first initialised (equivalent to a constructor for the request).
*
* Here we call $this->validate() immediately so that if the page is
* loaded with an invalid ?search= value in the URL, the validation
* error is shown straight away rather than waiting for user interaction.
*/
public function mount(): void
{
$this->validate();
}
/**
* #[Computed] — Marks this method as a "computed property".
*
* Computed properties work like regular properties in the template
* ($this->entries), but their value is derived by running this method.
* Livewire caches the result for the duration of a single render cycle,
* so the database query only runs once per request even if the template
* references $this->entries multiple times.
*
* The query itself uses Craft CMS's Element Query API:
* - ->uri(':notempty:') — only return entries that have a URL (i.e. are live pages).
* - ->search($this->search) — filters results using Craft's full-text search engine.
* - ->orderBy(...) — when a search term is present, sort by relevance score;
* otherwise fall back to alphabetical title order.
* - ->paginate(8) — return a Laravel-compatible paginator with 8 items per page.
* The WithPagination trait reads the current page from the URL
* automatically.
*/
#[Computed]
public function entries()
{
return Entry::find()
->uri(':notempty:')
->search($this->search)
->orderBy($this->search ? 'score' : 'title')
->paginate(8);
}
/**
* updatedSearch() — A Livewire lifecycle hook that fires automatically
* whenever the $search property is updated by the user.
*
* Naming convention: updated{PropertyName}() — Livewire detects the
* property name from the method name and calls it after each update.
*
* Here we reset the paginator back to page 1 so that a new search
* doesn't land the user in the middle of stale results from a previous query.
*/
public function updatedSearch(): void
{
$this->resetPage();
}
};
?>
{{--
Everything below this point is the Blade template for this component.
Livewire requires a single root element (the outer <div>) wrapping the
entire template so it can track and morph the DOM correctly.
--}}
<div class="space-y-4">
{{--
flux:input — A Flux UI component (a Livewire-friendly component library).
wire:model.live.debounce.300ms="search"
Binds this input to the $search property.
- "live" → send updates to the server on every keystroke (not just on blur).
- "debounce.300ms"→ wait 300 ms after the user stops typing before firing the
request, reducing unnecessary server round-trips.
wire:keyup.esc="$set('search', '')"
Listens for the Escape key and resets $search to an empty string.
$set() is a Livewire magic method callable directly from the template —
no extra PHP method needed.
label, autofocus, clearable — standard Flux/HTML attributes for UX.
--}}
<flux:input
wire:model.live.debounce.300ms="search"
wire:keyup.esc="$set('search', '')"
label="Search for:"
autofocus
clearable
/>
{{--
$this->entries accesses the computed property defined above.
Because it is computed (and cached), calling it here and again below
does NOT run two separate database queries.
--}}
@if ($this->entries->isNotEmpty())
<ul class="space-y-2">
@foreach ($this->entries as $entry)
{{--
wire:key gives Livewire a stable identifier for each list item.
This helps Livewire's DOM-diffing algorithm figure out which
elements were added, removed, or moved — without it, Livewire
may re-render items unnecessarily or animate them incorrectly.
--}}
<li wire:key="{{ $entry->id }}">
{{-- $entry->link renders an <a href="..."> tag using the entry's URL and title. --}}
{{ $entry->link }}
</li>
@endforeach
</ul>
{{--
->links() renders the pagination controls using the specified Blade partial.
Because Livewire intercepts the pagination link clicks, navigating between
pages triggers a Livewire round-trip instead of a full page reload.
--}}
{{ $this->entries->links('components.partials.paginate') }}
@else
<p>No results found.</p>
@endif
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment