Back to Course |
Practical Livewire 3: Order Management System Step-by-Step

Products Table Main Structure

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.

products table with pagination

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:

products table with pagination