Last active
May 24, 2026 15:39
-
-
Save wsydney76/3ea76e02b1dc1ab325e859c049178473 to your computer and use it in GitHub Desktop.
Livewire search component for Craft, handcrafted, explained by AI...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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