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:
when I was trying to give product model command this error is given ErrorException
include(C:\Users\HP\Documents\Project\project_one\vendor\composer/../../app/Http/Livewire/ProductList.php): Failed to open s stream: No such file or directory
at C:\Users\HP\Documents\Project\project_one\vendor\composer\ClassLoader.php:578 574▕ * @param string $file 575▕ * @return void 576▕ */ 577▕ self::$includeFile = static function($file) { ➜ 578▕ include $file; 579▕ }; 580▕ } 581▕ } 582▕
1 C:\Users\HP\Documents\Project\project_one\vendor\composer\ClassLoader.php:578 include()
2 C:\Users\HP\Documents\Project\project_one\vendor\composer\ClassLoader.php:432 Composer\Autoload\ClassLoader::Composer\Autoload{closure}("C:\Users\HP\Documents\Project\project_one\vendor\composer/.../../app/Http/Livewire/ProductList.php")
What do you mean by "give product model command"? It seems like the file of ProductList.php is not found, can you double check it actually exists? Sorry, I can't really debug it for you, the error message is pretty clear, I won't help you more than that.
sorry I figure it out bug was because I did not give the data of product country in right sequence in database migration thats why i could not migrate it
To seed database in this step, open
CategoryFactory.php
and amend:Create
ProductFactory.php
and amend:And amend in
DatabaseSeeder.php
We are seeding
products
with belongs to many relationship. Execute to seed the data:Correct me please, if I'm wrong but It generates a unique category/categories for each product, right?
my approach is :
@Eduardo, both your approach and mine are producing the same result. I am seeding at least 2 random categories with products, and you are seeding 3. I find my code to be cleaner compared to yours. As you can see in the screenshot:
Dear @Povilas Korop,
I'm afraid there are typos in your code. Shouldn't we type hint the relationship in pascal case like this?
Reference
Yeah, well spotted! Will fix. Thanks for taking the time.
Hello,
I have installed the code from the repository (laravel 10.45 but I encounter the same problem with laravel 11) and migrated/seeded the database on a brand new one.
On a listing page (products or orders), I have found a weird behavior concerning the checkboxes on the left when going from one page to another using the pagination below the table.
If I check some checkboxes on the first page for example and then go to the second page (or any other), the same rows are checked on the second page and if I uncheck one of them on the second page, all become unchecked.
Looking forward for a solution
Found the solution: One needs to add wire:key in the loop line 116 of resources/views/livewire/products-list.blade.php
And obviously the same correction line 136 of resources/views/livewire/orders-list.blade.php
That's a really great catch! Updated lessons and repo