Livewire Sidebar Filters for E-Shop Products: Step-by-Step

Livewire Sidebar Filters for E-Shop Products: Step-by-Step
Admin
Tuesday, May 2, 2023 7 mins to read
Share
Livewire Sidebar Filters for E-Shop Products: Step-by-Step

Have you ever needed a checkbox-based filter for a list, like in e-shop sidebars? In this tutorial, we will use Livewire to build this step-by-step and update the products list without page refresh.

filtered products

Notice: the link to the repository will be at the end of the tutorial.

We will use Livewire features like Events and Lifecycle Hooks to achieve the end result.

So let's dive in!


Laravel Project Preparation

For this demo, we'll use our own Laravel Breeze Pages Skeleton which will give us a Breeze-like layout but with a public page for the products list.

As you saw in the screenshot above, we will filter by three criteria:

  • Price (within the Product itself)
  • Category (a belongsTo relationship)
  • Manufacturer (a belongsTo relationship)

So first, we need Models and Migrations for all of them.

php artisan make:model Category -m
php artisan make:model Manufacturer -m
php artisan make:model Product -m

database/migrations/xxxx_create_categories_table.php:

public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

app/Models/Category.php:

class Category extends Model
{
protected $fillable = [
'name',
];
 
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}

database/migrations/xxxx_create_manufacturers_table.php:

public function up(): void
{
Schema::create('manufacturers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}

app/Models/Manufacturer.php:

class Manufacturer extends Model
{
protected $fillable = [
'name',
];
 
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
}

database/migrations/xxxx_create_products_table.php:

public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->decimal('price');
$table->foreignId('category_id')->constrained();
$table->foreignId('manufacturer_id')->constrained();
$table->timestamps();
});
}

app/Models/Product.php:

class Product extends Model
{
protected $fillable = [
'name',
'description',
'price',
'category_id',
'manufacturer_id',
];
 
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}

Livewire Components

Next, after the installation of Livewire itself, we will need two Livewire components:

  • One to show the products
  • And another one for a sidebar with filters

When we click on any filter in the sidebar, the event will be sent to the Products component.

php artisan make:livewire Products
php artisan make:livewire Sidebar

First, let's just show filters and products, and later we will actually make filters work.

Here's how to add Livewire components in the Blade View: with @livewire('sidebar') and @livewire('products').

resources/views/home.blade.php:

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Home page') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
This is home page!
<div class="flex">
<div class="w-1/3">
@livewire('sidebar')
</div>
<div class="w-2/3">
@livewire('products')
</div>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

Next, showing products is straightforward. We just need to get them and show them.

app/Http/Livewire/Products.php:

class Products extends Component
{
public function render(): View
{
$products = Product::all();
 
return view('livewire.products', compact('products'));
}
}

resources/views/livewire/products.blade.php:

<div class="flex flex-wrap">
@foreach($products as $product)
<div class="w-1/3 p-3">
<div class="rounded-md">
<a href="#"><img src="https://placehold.it/700x400" alt=""></a>
<div class="mt-3">
<a href="#" class="text-2xl text-indigo-500 hover:underline">{{ $product->name }}</a>
</div>
<h5 class="mt-3">${{ number_format($product->price, 2) }}</h5>
<p class="mt-3">{{ $product->description }}</p>
</div>
</div>
@endforeach
</div>

For the sidebar, it's a bit more complicated.

We will need to do calculations when filters are applied, we will need to get prices, and also to make calculations for them.

For this, we will create a PriceService class and a method, passing the selected prices, categories, and manufacturers to it.

Prices options will be set as a constant in the Product model.

app/Models/Product.php:

class Product extends Model
{
// ...
const PRICES = [
'Less than 50',
'From 50 to 100',
'From 100 to 500',
'More than 500',
];
// ...
}

Now the service.

app/Services/PriceService.php:

use App\Models\Product;
 
class PriceService
{
private array $prices;
private array $categories;
private array $manufactures;
 
public function getPrices($prices, $categories, $manufactures): array
{
$this->manufactures = $manufactures;
$this->categories = $categories;
$this->prices = $prices;
$formattedPrices = [];
 
foreach (Product::PRICES as $index => $name) {
$formattedPrices[] = [
'name' => $name,
'products_count' => $this->getProductCount($index),
];
}
 
return $formattedPrices;
}
 
private function getProductCount($index): int
{
return Product::when($index == 0, function (Builder $query) {
$query->where('price', '<', '50');
})
->when($index == 1, function (Builder $query) {
$query->whereBetween('price', ['50', '100']);
})
->when($index == 2, function (Builder $query) {
$query->whereBetween('price', ['100', '500']);
})
->when($index == 3, function (Builder $query) {
$query->where('price', '>', '500');
})
->count();
}
}

For now, in this Service, we accept the required parameters and return price option with the products count. Later, we will add filters in the getProductsCount() to count the filtered products.

Now, we can call this Service in the Livewire component and show all filters in the Blade View.

app/Http/Livewire/Sidebar.php:

class Sidebar extends Component
{
public array $selected = [
'prices' => [],
'categories' => [],
'manufacturers' => [],
];
 
public function render(PriceService $priceService): View
{
$prices = $priceService->getPrices(
[],
$this->selected['categories'],
$this->selected['manufacturers']
);
 
$categories = Category::withCount(['products'])
->get();
 
$manufacturers = Manufacturer::withCount(['products'])
->get();
 
return view('livewire.sidebar', compact('prices', 'categories', 'manufacturers'));
}
}

We will bing every checkbox input to the $selected property, like wire:model="selected.prices".

And the same as with the prices, for now, we show the count of all products. When we add filters, we will calculate them on-the-fly by the selected filter.

Here's the Livewire Blade file to show the filters.

resouces/views/livewire/sidebar.blade.php:

<div class="col-lg-3 mb-4">
<h1 class="mt-4 text-4xl">Filters</h1>
 
<h3 class="mt-2 mb-1 text-3xl">Price</h3>
@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
 
<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
 
<h3 class="mt-2 mb-1 text-3xl">Manufacturers</h3>
@foreach($manufacturers as $index => $manufacturer)
<div>
<input type="checkbox" id="manufacturer{{ $index }}" value="{{ $manufacturer->id }}" wire:model="selected.manufacturers">
<label for="manufacturer{{ $index }}">
{{ $manufacturer['name'] }} ({{ $manufacturer['products_count'] }})
</label>
</div>
@endforeach
</div>

Now you should see a similar result to the one below:

sidebar with filters


Apply Filters

Because we will apply product filters in a few places, it is logical to use Eloquent Local Scope so that code wouldn't repeat. This scope needs to accept prices, categories, and manufacturers.

app/Models/Product.php:

use Illuminate\Database\Eloquent\Builder;
 
class Product extends Model
{
// ...
public function scopeWithFilters(Builder $query, array $prices, array $categories, array $manufacturers)
{
return $query->when(count($manufacturers), function (Builder $query) use ($manufacturers) {
$query->whereIn('manufacturer_id', $manufacturers);
})
->when(count($categories), function (Builder $query) use ($categories) {
$query->whereIn('category_id', $categories);
})
->when(count($prices), function (Builder $query) use ($prices){
$query->where(function (Builder $query) use ($prices) {
$query->when(in_array(0, $prices), function (Builder $query) {
$query->orWhere('price', '<', '50');
})
->when(in_array(1, $prices), function (Builder $query) {
$query->orWhereBetween('price', ['50', '100']);
})
->when(in_array(2, $prices), function (Builder $query) {
$query->orWhereBetween('price', ['100', '500']);
})
->when(in_array(3, $prices), function (Builder $query) {
$query->orWhere('price', '>', '500');
});
});
});
}
}

In this scope, we are using Conditional Clauses to check if any of the filters are selected.

Now, we need to use this Scope where the filter needs to be applied. In our case, this is in the PriceService, Products, and Sidebar Livewire components, we will add withFilters() there.

app/Services/PriceService.php:

use Illuminate\Database\Eloquent\Builder;
 
class PriceService
{
// ...
private function getProductCount($index): int
{
return Product::withFilters($this->prices, $this->categories, $this->manufacturers)
->when($index == 0, function (Builder $query) {
$query->where('price', '<', '50');
})
->when($index == 1, function (Builder $query) {
$query->whereBetween('price', ['50', '100']);
})
->when($index == 2, function (Builder $query) {
$query->whereBetween('price', ['100', '500']);
})
->when($index == 3, function (Builder $query) {
$query->where('price', '>', '500');
})
->count();
}
}

app/Http/Livewire/Sidebar.php:

use Illuminate\Database\Eloquent\Builder;
 
class Sidebar extends Component
{
// ...
public function render(PriceService $priceService): View
{
$prices = $priceService->getPrices(
[],
$this->selected['categories'],
$this->selected['manufacturers']
);
 
$categories = Category::withCount(['products' => function (Builder $query) {
$query->withFilters(
$this->selected['prices'],
[],
$this->selected['manufacturers']
);
}])
->get();
 
$manufacturers = Manufacturer::withCount(['products' => function (Builder $query) {
$query->withFilters(
$this->selected['prices'],
$this->selected['categories'],
[]
);
}])
->get();
 
return view('livewire.sidebar', compact('prices', 'categories', 'manufacturers'));
}
}

In every filter parameter, we pass one empty array. This is because, for example, if you select any category it won't calculate the count of products for the categories. It will only calculate the count of products for prices and manufacturers.

To filter products, we first need to have selected products in the Products Livewire component. We will set them by sending an event with $this->emit() from the sidebar Livewire component and set them in the $selected property. The event needs to be triggered when Livewire Lifecycle Hook gets triggered.

app/Http/Livewire/Sidebar.php:

class Sidebar extends Component
{
// ...
public function updatedSelected(): void
{
$this->emit('updatedSidebar', $this->selected);
}
// ...
}

app/Http/Livewire/Products.php:

class Products extends Component
{
protected $selected = [
'prices' => [],
'categories' => [],
'manufacturers' => []
];
 
protected $listeners = ['updatedSidebar' => 'setSelected'];
 
public function setSelected($selected): void
{
$this->selected = $selected;
}
 
public function render(): View
{
$products = Product::withFilters(
$this->selected['prices'],
$this->selected['categories'],
$this->selected['manufacturers']
)->get();
 
return view('livewire.products', compact('products'));
}
}

That's it. When you click any filter in the sidebar, products should be updated accordingly.

filtered products

The link to the repository of this project: LaravelDaily/Livewire-eshop-filters-in-sidebar