Given Ncube
Posted on June 10, 2023
In the previous post, we covered the ability to create new products, add images with filepond.js and store the images on Cloudinary. This the part 6.2 of the on going tutorial series on building an ecommerce website in Laravel from start to deployment. If you missed previous tutorials, checkout part 1 here.
In this post, we will add the ability to list all products, in short we will implement the index action of our products resource. This is going to be a shorter post than most, let's dive in
To begin, head over to your Admin\ProductController
and edit the index
action and tell laravel to return the admin.products.index
view with a list of recently created products paginated.
/**
* Display a listing of the resource.
*
* @return Renderable
*/
public function index()
{
$products = Product::paginate();
return view('admin.products.index', [
'products' => $products,
]);
}
Now head over to the admin.products.index
view and add the following snippet
@extends('layouts.app')
@section('title')
Products
@endsection
@section('content')
<section class="section">
<div class="section-header">
<h1>Products</h1>
<div class="section-header-button">
<a href="{{ route('admin.products.create') }}"
class="btn btn-primary">Add New</a>
</div>
<div class="section-header-breadcrumb">
<div class="breadcrumb-item active"><a href="{{ route('admin.home.index') }}">Dashboard</a></div>
<div class="breadcrumb-item"><a href="{{ route('admin.products.index') }}">Products</a></div>
<div class="breadcrumb-item">All Posts</div>
</div>
</div>
<div class="section-body">
<h2 class="section-title">Products</h2>
<p class="section-lead">
You can manage all products, such as editing, deleting and more.
</p>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4>Products</h4>
<div class="card-header-form">
<form>
<div class="input-group">
<input type="text"
class="form-control"
placeholder="Search">
<div class="input-group-btn">
<button class="btn btn-primary"><i class="fas fa-search"></i></button>
</div>
</div>
</form>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-borderless table-invoice rounded">
<tr>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
<th>Created At</th>
<th>Status</th>
</tr>
@foreach ($products as $product)
<tr>
<td data-controller="obliterate"
data-obliterate-url-value="{{ route('admin.products.destroy', $product) }}">
{{ $product->name }}
</td>
<td>
{{ $product->category->name }}
</td>
<td>
{{ $product->quantity ?? 0 }} left
</td>
<td>{{ $product->created_at->diffForHumans() }}</td>
<td>
@if ($product->status == 'active')
<div class="badge badge-primary">Active</div>
@else
<div class="badge badge-danger">Draft</div>
@endif
</td>
<td>
<div>
<a href='{{ route('admin.products.edit', $product) }}'
class='btn btn-primary'>Edit</a>
<a data-turbo-method='delete'
href='{{ route('admin.products.destroy', $product) }}'
class='btn btn-danger'>Delete</a>
</div>
</td>
</tr>
@endforeach
</table>
</div>
<div class="float-right">
{{ $products->links('vendor.pagination.bootstrap-5') }}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@endsection
This snippet just displays a table with all products, action links to edit and delete, and pagination links.
Notice the search form at the top right of the table, we want to be able to type something and just get the matched results as we type in semi realtime.
To do that, we first modify the index
action a bit and add a query filter
/**
* Display a listing of the resource.
*
* @return Renderable
*/
public function index(Request $request)
{
$products = QueryBuilder::for(Product::class)
->allowedFilters([AllowedFilter::scope('search', 'whereScout')])
->paginate()
->appends($request->query());
return view('admin.products.index', [
'products' => $products,
]);
}
This piece of code calss the whereScout
scope to filter the results, let's configure scout for the model starting by adding the trait
use Laravel\Scout\Searchable;
class Product extends Model
{
use Searchable;
...
Then configure the toSearchableArray
method to tell scout where to search
/*
* Get the indexable data array for the model.
*
* @return array
*/
public function toSearchableArray(): array
{
return [
'name' => $this->name,
'description' => $this->description,
'slug' => $this->slug,
];
}
To make it work let's add the whereScout
scope
/**
* Scope a query to only include listings that are returned by scout
*
* @param Builder $query
* @param string $search
* @return Builder
*/
public function scopeWhereScout(Builder $query, string $search): Builder
{
return $query->whereIn(
'id',
self::search($search)
->get()
->pluck('id'),
);
}
On the frontend we use Turbo and Stimulus to simulate a real time effect, we already created the Stimulus controllers needed in the previous tutorials and we will just reuse those
In the admin.products.index
view replace the search input
with the one below
<input type="text"
class="form-control"
placeholder="Search"
{{ stimulus_controller('filter', [
'route' => 'admin.products.index',
'filter' => 'search',
]) }}
{{ stimulus_action('filter', 'change', 'input') }}>
We are connecting the filter controller to this input which listens for a change event in the input fires a filter change event to another controller which handles page reloading.
Let's connect the reloading controller by wrapping the table inside a turbo frame. Wrap everything inside the div with class .card-body
inside the turbo frame tag like this
<turbo-frame class='w-full'
id='categories'
target="_top"
{{ stimulus_controller('reload') }}
{{ stimulus_actions([
[
'reload' => ['filterChange', 'filter:change@document'],
],
[
'reload' => ['sortChange', 'sort:change@document'],
],
]) }}>
<!-- Everything that was inside the .card-body goes here -->
<!-- Everything inside this frame will be reloaded when the controller reloads -->
</turbo-frame>
Now if you start typing in the search fields you start getting results instantly.
While were on this page let's add the ability to delete products. Edit the destory
action in your controller as follows
/**
* Remove the specified resource from storage.
*
* @param Product $product
* @return RedirectResponse
*/
public function destroy(Product $product)
{
$product->delete();
return to_route('admin.products.index')->with(
'success',
'Product was successfully deleted',
);
}
If you click the delete button in the products index page it should delete the product. Magic, right!? Well, not quite.
Here is what the button looks like
<a data-turbo-method='delete'
href='{{ route('admin.products.destroy', $product) }}'
class='btn btn-danger'>
Delete
</a>
The data-turbo-method="delete"
attribute tells Turbo, which is responsible for navigating to links, to make a DELETE request.
And for this tutorial we are done. In the next tutorial we will add the ability to edit products and like always to make sure you don't miss it when it comes out subscribe to the newsletter below and get an email when it's ready.
If you any questions reach out to me on Twitter @ncubegiven_
In the meantime Happy Coding!
Posted on June 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.