This example will show you how we built the system using Livewire. For this, we will have three Livewire components:
Before we code anything in Livewire, we need to install a few packages:
First, we have to install Breeze:
composer require laravel/breeze --dev
And then we need to run it's install:
php artisan breeze:install blade
Why "blade" stack and not "livewire"? Yes, Laravel Breeze has Livewire as one of the options, but personally, I don't like the Livewire Volt that comes automatically with it. I prefer to use Livewire with Components and without Volt, no need to learn another mini-language on top.
Next, we need to manually install Livewire:
composer require livewire/livewire
Once installed, we'll modify our Dashboard template to load our Components:
<x-app-layout> <x-slot name="header"> <div class="flex flex-grow justify-between items-center"> <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"> {{ __('Products List') }} </h2> <livewire:cart-counter/> </div> </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="flex p-6"> <div class="w-1/3"> <livewire:sidebar/> </div> <div class="w-2/3"> <livewire:products/> </div> </div> </div> </div> </div></x-app-layout>
Yes, those components don't exist yet, but I wanted to specifically show you the "overview", first.
Of course, the page will not load at this point, so let's create these components.
This component will be responsible for:
So, we run:
php artisan make:livewire Products
app/Livewire/Products.php
use App\Models\Cart;use App\Models\Product;use Illuminate\Contracts\View\View;use Livewire\Attributes\On;use Livewire\Attributes\Url;use Livewire\Component; class Products extends Component{ public array $cartProducts = []; #[Url] public array $selected = [ 'prices' => [], 'categories' => [], 'manufacturers' => [] ]; public function render(): View { $products = Product::withFilters( $this->selected['prices'], $this->selected['categories'], $this->selected['manufacturers'] )->get(); $this->cartProducts = Cart::pluck('product_id')->unique()->toArray(); return view('livewire.products', compact('products')); } #[On('updatedSidebar')] public function setSelected($selected): void { $this->selected = $selected; } public function addOrRemoveFromCart(int $productId): void { if (in_array($productId, $this->cartProducts, true)) { Cart::where('product_id', $productId)->delete(); unset($this->cartProducts[$productId]); } else { Cart::create(['product_id' => $productId]); $this->cartProducts[] = $productId; } $this->dispatch('updateCart'); }}
Here's the exact breakdown of what we did:
$cartProducts
property is used to know which products were already added to the Cart to show the Remove button$selected
property is used to retrieve all selected filters (more on them in our next Component)render()
just collects the data needed and returns a ViewsetSelected()
is an event listener that reacts to events from the Product Filter componentaddOrRemoveFromCart()
function handles the button click on each product.Next, we need to add the View code:
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="http://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 class="mt-4 border-t pt-6"> @if (! in_array($product->id, $cartProducts)) <a wire:click.prevent="addOrRemoveFromCart({{ $product->id }})" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" href="#"> Add to Cart </a> @else <a wire:click.prevent="addOrRemoveFromCart({{ $product->id }})" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded" href="#"> Remove from Cart </a> @endif </div> </div> </div> @endforeach</div>
In this case, it's simple to use foreach
to render our list. Then, we have a button to add/remove each product from the Cart.
Now, we need a way to filter the products:
php artisan make:livewire Sidebar
app/Livewire/Sidebar.php
use App\Models\Category;use App\Models\Manufacturer;use App\Services\PriceService;use Livewire\Attributes\Url;use Livewire\Component;use Illuminate\Contracts\View\View; class Sidebar extends Component{ #[Url] 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' => function ($query) { $query->withFilters( $this->selected['prices'], [], $this->selected['manufacturers'] ); }]) ->get(); $manufacturers = Manufacturer::withCount(['products' => function ($query) { $query->withFilters( $this->selected['prices'], $this->selected['categories'], [] ); }]) ->get(); return view('livewire.sidebar', compact('prices', 'categories', 'manufacturers')); } public function updatedSelected(): void { $this->dispatch('updatedSidebar', $this->selected); }}
We can break this down into the following:
$selected
property is used to store the filters (they will also be stored in the URL as Query param)render()
retrieves available filters and returns the ViewupdateSelected()
dispatches an event we catch in our Product List component.Last, we need a view for it:
resources/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.live="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 class="form-check"> <input type="checkbox" id="category{{ $index }}" value="{{ $category->id }}" wire:model.live="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 class="form-check"> <input type="checkbox" id="manufacturer{{ $index }}" value="{{ $manufacturer->id }}" wire:model.live="selected.manufacturers"> <label for="manufacturer{{ $index }}"> {{ $manufacturer['name'] }} ({{ $manufacturer['products_count'] }}) </label> </div> @endforeach</div>
In this case, we have a few simple foreach
loops that render different items.
These items have the wire:model.live
attribute to set filters immediately.
This component will be the simplest one - just a basic text with a count:
php artisan make:livewire CartCounter
app/Livewire/CartCounter.php
use App\Models\Cart;use Illuminate\Contracts\View\View;use Livewire\Attributes\On;use Livewire\Component; class CartCounter extends Component{ public function render(): View { return view('livewire.cartCounter', [ 'cartAmount' => Cart::count(), ]); } #[On('updateCart')] public function refreshComponent(): void { $this->dispatch('$refresh'); }}
The only thing worth noting is that we have a listener for the updateCart
event that comes from our Product List button.
And the view:
resources/views/livewire/cartCounter.blade.php
<div class="px-4 py-3 leading-normal text-blue-700 bg-blue-100 rounded-lg text-right w-1/3"> <i class="fa fa-shopping-cart"></i> Cart ({{ $cartAmount }})</div>
That's it for our counter.
And yeah, the whole application is now working! You can filter the products via the left sidebar and add any product to cart, looking at the Cart number going up!
Ok, time to compare the experience of building the same functionality with Vue.js and Inertia.
You can find this code for this lesson in a separate branch on GitHub