In this lesson, we will add live search functionality to the table.

First, we will get a list of categories and countries. For this, we will need public properties for both and we will assign them in the mount() method...
In this lesson, we will add live search functionality to the table.

First, we will get a list of categories and countries. For this, we will need public properties for both and we will assign them in the mount() method...
I found the query is bit complex. Can't we simplify it by using Eloquent ORM?
It is not really an improvement but my query looks like this. just got rid of the "foreach" loop. "to" is set to max price in products. If "to" is 0, query doesn't return any product. It is not bug but feature ;)
// ...
public function mount(): void
{
// ...
$this->searchColumns["price"] = [0, Product::max('price') / 100];
}
// ...
public function render()
{
$products = Product::query()
->select(['products.*', 'countries.id as countryId', 'countries.name as countryName',])
->join('countries', 'countries.id', '=', 'products.country_id')
->whereBetween(
'products.price',
[
floatval($this->searchColumns['price'][0]) * 100,
floatval($this->searchColumns['price'][1]) * 100
]
)
->with('categories')
->where('products.name', 'LIKE', '%' . $this->searchColumns['name'] . '%')
->where('products.description', 'LIKE', '%' . $this->searchColumns['description'] . '%')
->when($this->searchColumns['category_id'], function ($query) {
$query->whereRelation('categories', 'id', $this->searchColumns['category_id']);
})
->when($this->searchColumns['country_id'], function ($query) {
$query->whereRelation('country', 'id', $this->searchColumns['country_id']);
});
return view('livewire.products-list', [
'products' => $products->paginate(10),
]);
}
I simplified the code by moving the functionality to local scopes.
#[Layout('layouts.app')]
public function render()
{
$products = Product::query()
->select(['products.*', 'countries.id as countryId', 'countries.name as countryName'])
->join('countries', 'countries.id', '=', 'products.country_id')
->with('categories')
->filterByName($this->searchColumns['name'])
->filterByPrice($this->searchColumns['price'][0], $this->searchColumns['price'][1])
->filterByCategory($this->searchColumns['category_id'])
->filterByCountry($this->searchColumns['country_id'])
->paginate();
return view('livewire.products.products-lists', [
'products' => $products
]);
}
public function scopeFilterByName($query, $name)
{
return $query->when($name, fn($query) => $query->where('products.name', 'LIKE', '%' . $name . '%'));
}
public function scopeFilterByPrice($query, $min, $max)
{
return $query->when(is_numeric($min), fn($query) => $query->where('price', '>=', $min * 100))
->when(is_numeric($max), fn($query) => $query->where('price', '<=', $max * 100));
}
public function scopeFilterByCategory($query, $categoryId)
{
return $query->when($categoryId, fn($query) => $query->whereRelation('categories','id', $categoryId));
}
public function scopeFilterByCountry($query, $countryId)
{
return $query->when($countryId, fn($query) => $query->whereRelation('country','id', $countryId));
}
When I search by category I got the following error
select count(*) as aggregate from `products` inner join `countries` on `countries`.`id` = `products`.`country_id` where `products`.`name` LIKE %volup% and exists (select * from `categories` inner join `category_product` on `categories`.`id` = `category_product`.`category_id` where `products`.`id` = `category_product`.`product_id` and `id` = 7)
Integrity constraint violation: 1052 Column 'id' in where clause is ambiguous, I am on Laravel 10
Solution: replace the last 'id' in the following query with 'categories.id'
$products = Product::query()
->select(['products.*', 'countries.id as countryId', 'countries.name as countryName',])
->join('countries', 'countries.id', '=', 'products.country_id')
->with('categories');
foreach ($this->searchColumns as $column => $value) {
if (!empty($value)){
$products->when($column == 'name', fn($products) => $products->where('products.'.$column, 'LIKE', '%'.$value.'%' ));
$products->when($column == 'description', fn($products) => $products->where('products.'.$column, 'LIKE', '%'.$value.'%' ));
$products->when($column == 'country_id', fn($products) => $products->whereRelation('country', 'id', $value));
$products->when($column == 'category_id', fn($products) => $products->whereRelation('categories', 'id', $value));
}
}
Hello all! I not think it is a improvement, but I like to have a message when my filter result in no records found.
@forelse($products as $product)
<tr class="bg-white">
<td class="px-4 py-2 text-sm leading-5 text-gray-900 whitespace-no-wrap">
<input type="checkbox" value="{{ $product->id }}" wire:model="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->countryName }}
</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>
@empty
<tr class="bg-gray-300">
<td colspan="6" class="text-center">No results found!</td>
</tr>
@endforelse
</tbody>
A problem that I have is that when I type something in any search field the autofocus is gone (and so is the text coursor) as soon as the search updates, so I have to click the field again to be able to continue to type in that field. Does anyone experience the sam or have I missed something?
Hello...
Nothing happens when I select from the dropdown or type something on the search inputs... All previuos lessons are working as expected, but not this one...
Here is the code:
--- Component namespace App\Livewire;
use App\Models\Category; use App\Models\Country; use App\Models\Product; use Livewire\Component; use Livewire\WithPagination; use Illuminate\Contracts\View\View;
class ProductList extends Component { use WithPagination;
public array $categories = [];
public array $countries = [];
public array $searchColumns = [
'name' => '',
'price' => ['', ''],
'description' => '',
'category_id' => 0,
'country_id' => 0,
];
public function mount(): void
{
$this->categories = Category::pluck('name', 'id')->toArray();
$this->countries = Country::pluck('name', 'id')->toArray();
}
public function render(): View
{
$products = Product::query()
->select(['products.*', 'countries.id as countryId', 'countries.name as countryName',])
->join('countries', 'countries.id', '=', 'products.country_id')
->with('categories');
foreach ($this->searchColumns as $column => $value) {
if (!empty($value)) {
$products->when($column == 'price', function ($products) use ($value) {
if (is_numeric($value[0])) {
$products->where('products.price', '>=', $value[0] * 100);
}
if (is_numeric($value[1])) {
$products->where('products.price', '<=', $value[1] * 100);
}
})
->when($column == 'category_id', fn($products) => $products->whereRelation('categories', 'id', $value))
->when($column == 'country_id', fn($products) => $products->whereRelation('country', 'id', $value))
->when($column == 'name', fn($products) => $products->where('products.' . $column, 'LIKE', '%' . $value . '%'));
}
}
return view('livewire.product-list', [
'products' => $products->paginate(10),
]);
}
}
--- View
<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">
Crear un Producto
</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>
<tr>
<td></td>
<td class="px-2 py-2">
<input wire:model="searchColumns.name" type="text" placeholder="Search..."
class="w-full text-sm rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" />
</td>
<td class="px-2 py-1">
<select wire:model="searchColumns.category_id"
class="w-full text-sm rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="">-- choose category --</option>
@foreach($categories as $id => $category)
<option value="{{ $id }}">{{ $category }}</option>
@endforeach
</select>
</td>
<td class="px-2 py-1">
<select wire:model="searchColumns.country_id"
class="w-full text-sm rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50">
<option value="">-- choose country --</option>
@foreach($countries as $id => $country)
<option value="{{ $id }}">{{ $country }}</option>
@endforeach
</select>
</td>
<td class="px-2 py-1 text-sm">
<div>
From
<input wire:model="searchColumns.price.0" type="number"
class="mr-2 w-full text-sm rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" />
</div>
<div>
to
<input wire:model="searchColumns.price.1" type="number"
class="w-full text-sm rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" />
</div>
</td>
<td></td>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@foreach($products as $product)
<tr class="bg-white">
<td class="px-4 py-2 text-sm leading-5 text-gray-900 whitespace-no-wrap">
<input type="checkbox" value="{{ $product->id }}" wire:model="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->countryName }}
</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>
This Lesson does not seem to be working when using Livewire 3.4. I went through the docs but couldn't find something that helps
Not sure if adding the category_product pivot table happened when the course was updated to Livewire v3 but I had to modify the query to include that table in order to get the category search to work;
foreach ($this->searchColumns as $column => $value) {
if (!empty($value)) {
$products->when($column == 'price', function ($products) use ($value) {
if (is_numeric($value[0])) {
$products->where('products.price', '>=', $value[0] * 100);
}
if (is_numeric($value[1])) {
$products->where('products.price', '<=', $value[1] * 100);
}
})
->when($column == 'category_id', function ($products) use ($value) {
$products->join('category_product', 'products.id', '=', 'category_product.product_id')
->where('category_product.category_id', $value);
})
->when($column == 'country_id', fn($products) => $products->whereRelation('country', 'id', $value))
->when($column == 'name', fn($products) => $products->where('products.' . $column, 'LIKE', '%' . $value . '%'));
}
}
Can you further explain the need to multiply by 100 in the product price search? It is displaying the price stored in the DB divided by 100. The search works fine on the price that is displayed however because it is dividing by 100 in the search.
Not sure if I understand the question. We need to search by price in the DATABASE, which is in cents, that's why we need to multiply the search parameters by 100 to have them in cents, to match the database records.
Very nicen specialy the search parts
my five cents: there are lots of way to store money in the database, but perhaps the most common - convention adopted by famous examples such as Stripe - is storing in the database as cents, due to the problem around float-poiting numbers: compilers treat them differently and they've got thus no quite precision. Integers are more reliable