Now we will move on to products. In this lesson, we will create a table of Products, powered by Livewire. For now, we will just show a list of products, and in other lessons, we will add features to it.
First, we will create a Livewire component.
php artisan make:livewire ProductsList
We use all Livewire components, as full-page, so we need to register the route using Livewire Component instead of Controller inside the middleware group, with the name of products.index
, and add a link to the navigation.
routes/web.php:
Route::middleware('auth')->group(function () { Route::get('categories', CategoriesList::class)->name('categories.index'); Route::get('products', ProductsList::class)->name('products.index'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');});
resources/views/layouts/navigation.blade.php:
<!-- Navigation Links --><div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> {{ __('Dashboard') }} </x-nav-link> <x-nav-link :href="route('categories.index')" :active="request()->routeIs('categories.index')"> {{ __('Categories') }} </x-nav-link> <x-nav-link :href="route('products.index')" :active="request()->routeIs('products.*')"> {{ __('Products') }} </x-nav-link> </div>
The product will have a country. For this, we will create a Model and Migration, but data will just be seeded.
php artisan make:model Country -m
database/migrations/xxxx_create_countries_table.php:
return new class extends Migration{ public function up() { Schema::create('countries', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('short_code'); $table->timestamps(); }); }}
app/Models/Country.php:
class Country extends Model{ protected $fillable = ['name', 'short_name'];}
Seeder can be found here, don't forget to call it in DatabaseSeeder
, so that after every migration you could seed it easier.
database/seeders/DatabaseSeeder.php:
public function run(): void{ $this->call([ CountriesSeeder::class, ]);}
Next, as always, Products Model and Migrations:
php artisan make:model Product -m
database/migrations/xxxx_create_products_table.php:
return new class extends Migration{ public function up() { Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->text('description'); $table->foreignId('country_id')->constrained(); $table->integer('price')->default(0); $table->timestamps(); }); }}
app/Models/Product.php:
class Product extends Model{ use HasFactory; protected $fillable = ['name', 'description', 'country_id', 'price']; public function country(): BelongsTo { return $this->belongsTo(Country::class); } public function categories(): BelongsToMany { return $this->belongsToMany(Category::class); }}
And, because Product has ManyToMany relation to Categories, we need to create migration for this relation.
php artisan make:migration "create category product table"
database/migrations/xxxx_create_category_product_table.php:
return new class extends Migration{ public function up() { Schema::create('category_product', function (Blueprint $table) { $table->foreignId('category_id')->constrained()->cascadeOnDelete(); $table->foreignId('product_id')->constrained()->cascadeOnDelete(); }); }}
Now, let's show the products in the table. First, in the component, we need to add the WithPagination
trait and query Products.
app/Livewire/ProductsList.php:
class ProductsList extends Component{ use WithPagination; public function render(): View { $products = Product::paginate(10); return view('livewire.products-list', [ 'products' => $products, ]); }}
And blade for now would look like below.
resources/views/livewire/products-list.blade.php:
<div> <x-slot name="header"> <h2 class="text-xl font-semibold leading-tight text-gray-800"> {{ __('Products') }} </h2> </x-slot> <div class="py-12"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="overflow-hidden bg-white shadow-sm sm:rounded-lg"> <div class="p-6 bg-white border-b border-gray-200"> <div class="mb-4"> <div class="mb-4"> <a class="inline-flex items-center px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase bg-gray-800 rounded-md border border-transparent hover:bg-gray-700"> Create Product </a> </div> </div> <div class="overflow-hidden overflow-x-auto mb-4 min-w-full align-middle sm:rounded-md"> <table class="min-w-full border divide-y divide-gray-200"> <thead> <tr> <th class="px-6 py-3 text-left bg-gray-50"> </th> <th class="px-6 py-3 text-left bg-gray-50"> <span class="text-xs font-medium tracking-wider leading-4 text-gray-500 uppercase">Name</span> </th> <th class="px-6 py-3 text-left bg-gray-50"> <span class="text-xs font-medium tracking-wider leading-4 text-gray-500 uppercase">Categories</span> </th> <th class="px-6 py-3 text-left bg-gray-50"> <span class="text-xs font-medium tracking-wider leading-4 text-gray-500 uppercase">Country</span> </th> <th class="px-6 py-3 w-32 text-left bg-gray-50"> <span class="text-xs font-medium tracking-wider leading-4 text-gray-500 uppercase">Price</span> </th> <th class="px-6 py-3 text-left bg-gray-50"> </th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200 divide-solid"> @foreach($products as $product) <tr class="bg-white" wire:key="product-{{ $product->id }}"> <td class="px-4 py-2 text-sm leading-5 text-gray-900 whitespace-no-wrap"> <input type="checkbox" value="{{ $product->id }}" wire:model.live="selected"> </td> <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> {{ $product->name }} </td> <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> @foreach($product->categories as $category) <span class="px-2 py-1 text-xs text-indigo-700 bg-indigo-200 rounded-md">{{ $category->name }}</span> @endforeach </td> <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> {{ $product->country->name }} </td> <td class="px-6 py-4 text-sm leading-5 text-gray-900 whitespace-no-wrap"> ${{ number_format($product->price / 100, 2) }} </td> <td> <a class="inline-flex items-center px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase bg-gray-800 rounded-md border border-transparent hover:bg-gray-700"> Edit </a> <button class="px-4 py-2 text-xs text-red-500 uppercase bg-red-200 rounded-md border border-transparent hover:text-red-700 hover:bg-red-300"> Delete </button> </td> </tr> @endforeach </tbody> </table> </div> {{ $products->links() }} </div> </div> </div> </div></div>
Yes, for now, we have an N+1 problem, we will fix that in other lessons. For now, after adding manually at least 11 products you should see a similar working table with pagination: