This tutorial is a Part 2 follow-up to the Livewire Sidebar Filters for E-Shop Products: Step-by-Step article. We decided to improve that Sidebar Filter component with new features using Alpine.js.
We will add:
- Collapsable filter options
- Search for every filter
- Show more/less for the filter options list
At the end of this tutorial, you will find a link to the repository with both the original Livewire component, and these Alpine.js improvements in the form of Pull Request.
So let's dive in!
Livewire Component Changes
First, we need to make changes to the Livewire component. We need to add public properties for categories and manufacturers and make them as an array.
app/Http/Livewire/Sidebar.php:
class Sidebar extends Component{ public array $categories = []; public array $manufacturers = []; // ... public function render(PriceService $priceService): View { $prices = $priceService->getPrices( [], $this->selected['categories'], $this->selected['manufacturers'] ); $categories = Category::withCount(['products' => function (Builder $query) { $this->categories = Category::withCount(['products' => function (Builder $query) { $query->withFilters( $this->selected['prices'], [], $this->selected['manufacturers'] ); }]) ->get() ->toArray(); $manufacturers = Manufacturer::withCount(['products' => function (Builder $query) { $this->manufacturers = Manufacturer::withCount(['products' => function (Builder $query) { $query->withFilters( $this->selected['prices'], $this->selected['categories'], [] ); }]) ->get() ->toArray(); return view('livewire.sidebar', compact('prices', 'categories', 'manufacturers')); return view('livewire.sidebar', compact('prices')); }}
This way we will be able to share the state between Livewire and Alpine.js.
Now, let's go to the resources/views/livewire/sidebar.blade.php
and add more features.
Collapse
First, we will add a collapse feature.
We will add all features only for categories now, and later for other blocks.
Everything in Alpine.js starts with the x-data
directive. So first thing, we need to wrap the block with a div and add x-data
with the reactive data open
which by default will be true.
<div x-data="{ open: true }"> <h3 class="mt-2 mb-1 text-3xl">Categories</h3> @foreach($categories as $index => $category) <div> <input type="checkbox" id="category{{ $index }}" value="{{ $category->id }}" wire:model="selected.categories"> <label for="category{{ $index }}"> {{ $category['name'] }} ({{ $category['products_count'] }}) </label> </div> @endforeach</div>
Next, the title, in our case Categories, needs to be transformed into a button.
<div x-data="{ open: true }"> <h3 class="mt-2 mb-1 text-3xl">Categories</h3> <button @click="open = !open" class="flex w-full items-center justify-between text-left"> Categories <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6 transform duration-300" :class="{'rotate-180': open}"> <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" /> </svg> </button> @foreach($categories as $index => $category) <div> <input type="checkbox" id="category{{ $index }}" value="{{ $category->id }}" wire:model="selected.categories"> <label for="category{{ $index }}"> {{ $category['name'] }} ({{ $category['products_count'] }}) </label> </div> @endforeach</div>
When the button is pressed, open
will be set to the opposite value. Also, we bind :class
to rotate the arrow.
Now we need to hide the list when the button is pressed. For this we will wrap the @foreach
blade directive into a div
and will show or hide the list using x-show
directive. Also, we will add some transition animation.
<div x-data="{ open: true }"> <h3 class="mt-2 mb-1 text-3xl">Categories</h3> <button @click="open = !open" class="flex w-full items-center justify-between text-left"> Categories <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6 transform duration-300" :class="{'rotate-180': open}"> <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" /> </svg> </button> <div x-show="open" x-transition.scale.origin.top x-transition:enter.duration.500ms x-transition:leave.duration.500ms > @foreach($categories as $index => $category) <div> <input type="checkbox" id="category{{ $index }}" value="{{ $category->id }}" wire:model="selected.categories"> <label for="category{{ $index }}"> {{ $category['name'] }} ({{ $category['products_count'] }}) </label> </div> @endforeach </div> </div>
After all this, we have a similar result to the one below. The left part is showing everything, and on the right it's collapsed.
For the prices block, this will be the only feature. So we need to do the same. Wrap in a div with x-data
, adding a button to show/hide, and wrapping foreach into a div with the x-show
.
resources/views/livewire/sidebar.blade.php:
<div x-data="{ open: true }"> <h3 class="mt-2 mb-1 text-2xl"> <button @click="open = !open" class="flex w-full items-center justify-between text-left"> Price <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-6 w-6 transform duration-300" :class="{'rotate-180': open}"> <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" /> </svg> </button> </h3> <div x-show="open" x-transition.scale.origin.top x-transition:enter.duration.500ms x-transition:leave.duration.500ms > @foreach($prices as $index => $price) <div> <input type="checkbox" id="price{{ $index }}" value="{{ $index }}" wire:model="selected.prices"> <label for="price{{ $index }}"> {{ $price['name'] }} ({{ $price['products_count'] }}) </label> </div> @endforeach </div> </div>
Search
Now let's add the search functionality.
First, we need an input and it has to be...