render is called after any change if your fields are live. You need a way to indicate that the search is ready to be performed, maybe with a search button. If you know search has been pressed then you can do the search so that the render method can show the results, and not query on every render.
Dec 20, 2024
4
Level 1
Livewire Search
Hi everyone,
I have a search form that displays the search results when I click the search button. However, I have an issue: when I add a city, the search is triggered. I can add multiple cities to the search at the same time. If I remove the added city, the search is also triggered. How can I prevent this from happening?
Here is the source code:
app\Livewire\PropertyList.php
<?php
namespace App\Livewire;
use App\Models\Property\Property;
use App\Models\List\PropertyType;
use App\Models\List\PropertyCategory;
use App\Models\List\LocationCity;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Title('Property List')]
class PropertyList extends Component
{
use WithPagination;
#[Url]
public $selected_types;
#[Url]
public $selected_categories;
#[Url]
public $selected_location_cities = [];
#[Url]
public $price_range_min;
#[Url]
public $price_range_max;
#[Url]
public $locationCityOptions = [];
public $showDropdown = false;
public function mount()
{
$this->locationCityOptions = LocationCity::visible()->get(['id', 'name'])->toArray();
}
public function applyFilters()
{
$this->resetPage();
}
public function selectCity($id)
{
if (!in_array($id, $this->selected_location_cities)) {
$this->selected_location_cities[] = $id;
}
}
public function deselectCity($id)
{
$this->selected_location_cities = array_values(array_filter(
$this->selected_location_cities,
fn($cityId) => (int) $cityId !== (int) $id
));
}
public function deselectAllCities()
{
$this->selected_location_cities = [];
}
public function getSelectedCityNames()
{
return LocationCity::whereIn('id', $this->selected_location_cities)
->pluck('name')
->toArray();
}
public function updated($propertyName)
{
$this->validateOnly($propertyName, [
'price_range_min' => 'nullable|numeric|min:0',
'price_range_max' => 'nullable|numeric|gte:price_range_min',
]);
}
public function render()
{
return view('livewire.property-list', [
'properties' => $this->filteredPropertyQuery()->paginate(12),
'property_types' => PropertyType::visible()->get(['id', 'name']),
'property_categories' => PropertyCategory::visible()->get(['id', 'name']),
'property_location_cities_names' => $this->getSelectedCityNames(),
]);
}
private function filteredPropertyQuery()
{
$query = Property::query()->where('is_visible', 1);
if ($this->selected_types) {
$query->where('type_id', $this->selected_types);
}
if ($this->selected_categories) {
$query->where('category_id', $this->selected_categories);
}
if ($this->selected_location_cities) {
$query->whereIn('city_id', $this->selected_location_cities);
}
if ($this->price_range_min || $this->price_range_max) {
$priceField = $this->selected_types === 1 ? 'price_sale' : 'price_rent';
$query->whereBetween($priceField, [$this->price_range_min ?? 0, $this->price_range_max ?? PHP_INT_MAX]);
}
return $query;
}
}
resources\views\livewire\property-list.blade.php
<form wire:submit.prevent="applyFilters" class="grid grid-cols-12 gap-4">
<div class="col-span-2">
<select id="listingType" wire:model.defer="selected_types"
class="border border-default-200 rounded-lg w-full px-3 py-2 text-sm focus:border-default-300 focus:ring-transparent">
@foreach ($property_types as $type)
<option value="{{ $type->id }}">{{ $type->name }}</option>
@endforeach
</select>
</div>
<div class="col-span-2">
<select id="propertyType" wire:model.defer="selected_categories"
class="border border-default-200 rounded-lg w-full px-3 py-2 text-sm focus:border-default-300 focus:ring-transparent">
@foreach ($property_categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
</div>
<div class="col-span-3">
<div class="relative" x-data="{ showDropdown: @entangle('showDropdown') }">
<div class="border border-default-200 rounded-lg w-full px-3 py-2 text-sm focus:border-default-300 focus:ring-transparent cursor-pointer flex flex-wrap items-center gap-2"
@click="showDropdown = true">
@if (!empty($property_location_cities_names))
@foreach ($property_location_cities_names as $index => $name)
<span class="bg-blue-500 text-white text-sm rounded-md px-2 py-1 flex items-center gap-1">
{{ $name }}
<button wire:click.prevent="deselectCity({{ $selected_location_cities[$index] }})"
class="text-white hover:text-red-300 focus:outline-none" @click.stop>
x
</button>
</span>
@endforeach
@else
<span class="text-gray-500" @click.stop="showDropdown = true">
Selectes Cities..
</span>
@endif
@if (!empty($property_location_cities_names))
<button wire:click.prevent="deselectAllCities" class="ml-auto text-red-500 hover:text-red-700 text-sm"
@click.stop>
Delete
</button>
@endif
</div>
<div class="absolute z-10 mt-1 w-full bg-white border rounded-lg shadow-lg" x-show="showDropdown"
@click.away="showDropdown = false" x-transition>
@foreach ($locationCityOptions as $option)
<div wire:click="selectCity({{ $option['id'] }})" class="px-4 py-2 hover:bg-gray-100 cursor-pointer">
{{ $option['name'] }}
</div>
@endforeach
</div>
</div>
</div>
<div class="col-span-2">
<div class="flex">
<input type="text" placeholder="Min"
class="text-sm w-full px-3 py-2 border-r-0 border-gray-200 rounded-l-md focus:border-default-300 focus:ring-transparent">
<span class="inline-flex items-center px-1 text-sm text-gray-900 border-0 border-y border-gray-200">
-
</span>
<input type="text" placeholder="Max"
class="text-sm w-full px-3 py-2 border-solid border-0 border-y border-gray-200 rounded-none focus:border-default-300 focus:ring-transparent">
<span
class="inline-flex items-center px-3 py-2 text-sm text-gray-900 bg-gray-200 border border-l-0 border-gray-200 rounded-r-md">
EUR
</span>
</div>
</div>
<div class="col-span-1">
<button
class="w-full bg-blue-500 text-white p-2 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 relative h-10">
<div class="absolute inset-0 flex items-center justify-center">
<span wire:loading.remove>Search</span>
<svg wire:loading class="h-5 w-5 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 2.28.805 4.367 2.144 6.029l1.856-1.738z">
</path>
</svg>
</div>
</button>
</div>
</form>
Level 3
@Fzoltan87 I think this should work, because this would trigger a query only when needed:
public $shouldSearch = false;
public $searchResults; // Query properties
public function applyFilters()
{
$this->shouldSearch = true; // User presses search button
$this->resetPage(); //Not sure if this is the correct order for this to execute here, do tests if something isn't refreshing right
$this->searchResults = $this->filteredPropertyQuery()->paginate(12); // Load query results here
}
public function render()
{
return view('livewire.property-list', [
'properties' => $this->shouldSearch // Only run the query if $shouldSearch is true
? $this->searchResults
: collect(), // or empty collection
'property_types' => PropertyType::visible()->get(['id', 'name']),
'property_categories' => PropertyCategory::visible()->get(['id', 'name']),
'property_location_cities_names' => $this->getSelectedCityNames(),
]);
}
Then to display them
@if ($properties->count())
@foreach($properties as $property)
{{-- --}}
@endforeach
@else
<p>No properties</p>
@endif
1 like
Please or to participate in this conversation.