I’m building a full page Livewire component that filters Meilisearch results.
When I check one of the filters the remaning checkboxes disappear, and I am struggling to understand why. I followed a tutorial to figure out how to build the bulk of the component and in the tutorial the behaviour didn’t occur. I’ve deconstructed the component to the bare minimum and the behaviour happens as soon as I wire up the checkboxes.
Here is the component:
<?php
namespace AppHttpLivewire;
use LivewireComponent;
use AppModelsCourseDate;
use AppModelsCourse;
class CourseBrowser extends Component
{
public $queryFilters = [];
public $priceRange = [
'min' => null,
'max' => null
];
public function mount()
{
$this->queryFilters = [
'venue' => [],
'type' => [],
'category' => [],
'days' => [],
'supplier' => []
];
}
public function render()
{
$search = CourseDate::search(
'',
function ($meilisearch, string $query, array $options) {
$filters = collect($this->queryFilters)
->filter(fn ($filter) => !empty($filter))
->recursive()
->map(function ($value, $key) {
return $value->map(fn ($value) => $key . ' = "' . $value . '"');
})
->flatten()
->join(' AND ');
$options['facets'] = ['venue', 'category', 'type', 'supplier', 'days'];
$options['filter'] = null;
if ($filters) {
$options['filter'] = $filters;
}
if ($this->priceRange['max']) {
$options['filter'] .= (isset($options['filter']) ? ' AND ' : '') . 'price <= ' . $this->priceRange['max'];
}
return $meilisearch->search($query, $options);
}
)->raw();
$coursedates = CourseDate::find(collect($search['hits'])->pluck('id'));
$minPrice = Course::all()->min('price');
$maxPrice = Course::all()->max('price');
$this->priceRange['min'] = $this->priceRange['min'] ?: $minPrice;
$this->priceRange['max'] = $this->priceRange['max'] ?: $maxPrice;
return view('livewire.course-browser')
->with(
[
'coursedates' => $coursedates,
'filters' => $search['facetDistribution'],
'minPrice' => $minPrice,
'maxPrice' => $maxPrice,
]
);
}
}
Here is the Blade view. I had originally posted an edited section showing just the checkboxes but I have edited this question to show the view in its entirety.
<div>
<div class="mb-4 text-gray-500">
<a href="{{ route('home') }}">Home</a> / <a href="{{ route('course.index') }}">Courses</a>
</div>
<div class="align-center mt-8 mb-10 flex items-center justify-between">
<div class="text-3xl font-bold">
All Courses
</div>
</div>
<div class="grid w-full grid-cols-4 gap-6">
<div class="col-span-1 rounded-sm bg-white shadow">
<div class="space-y-2 px-6 py-6">
<h6 class="text-sm font-bold uppercase">Filters</h6>
<hr class="mt-4 mb-6 border-gray-200 pb-2">
<h6 class="text-sm font-bold uppercase">Price </h6>
<div class="flex gap-1">
<input type="range"
min="{{ $minPrice }}"
max="{{ $maxPrice }}"
step="25"
class="accent-pink-700"
wire:model="priceRange.max" />
<span class="font-sm">
(£{{ $priceRange['max'] }})
</span>
</div>
@foreach ($filters as $title => $filter)
<h6 class="pt-2 text-sm font-bold uppercase">{{ Str::title($title) }}</h6>
@foreach ($filter as $option => $count)
<div wire:ignore.self
class="flex items-center space-x-2">
<input type="checkbox"
class="h-4 w-4 rounded border-gray-300 text-sm text-pink-600 focus:ring-0 focus:ring-offset-0"
wire:model="queryFilters.{{ $title }}"
id="{{ $title }}_{{ strtolower($option) }}"
value="{{ $option }}">
<label class="text-sm"
for="{{ $title }}_{{ strtolower($option) }}">{{ $option }} ({{ $count }})</label>
</div>
@endforeach
@endforeach
</div>
</div>
<div class="col-span-3">
<h6 class="text-md my-4 mb-4">{{ $coursedates->count() }} {{ Str::plural('course', $coursedates) }} matching your filters</h6>
@forelse ($coursedates as $coursedate)
<div wire:loading.class="opacity-50"
class="relative mb-6 w-full rounded-sm bg-white shadow">
<span class="absolute top-2 left-2 rounded bg-pink-800 px-3 py-1 font-bold text-white">{{ $coursedate->course->category->name }}</span>
<span class="absolute top-2 right-2 rounded bg-pink-600 px-3 py-1 font-bold text-white">{{ $coursedate->course->supplier->name }}</span>
<a href="{{ route('course.show', $coursedate->course->slug) }}">
<img src="{{ asset('storage/courses/images/' . $coursedate->course->title_image) }}"
alt="{{ $coursedate->course->slug }}"
class="h-40 w-full object-cover">
</a>
<div class="px-6 py-4">
<h6 class="mb-2 text-lg font-bold"><a href="{{ route('course.show', $coursedate->course->slug) }}">{{ $coursedate->course->name }} </a></h6>
<p class="mb-6 text-sm leading-tight">{{ $coursedate->course->tagline }}</p>
{{-- Pills Container --}}
<div class="flex">
<div class="mr-1 flex items-center rounded bg-pink-300 font-bold text-pink-800">
<div class="flex h-full items-center rounded-l bg-pink-400 px-2 py-1">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="h-4 w-4 text-pink-200">
<path d="M4.5 3.75a3 3 0 00-3 3v.75h21v-.75a3 3 0 00-3-3h-15z" />
<path fill-rule="evenodd"
d="M22.5 9.75h-21v7.5a3 3 0 003 3h15a3 3 0 003-3v-7.5zm-18 3.75a.75.75 0 01.75-.75h6a.75.75 0 010 1.5h-6a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex h-full items-center rounded bg-pink-300 px-3 py-1">
£{{ $coursedate->course->price }}
</div>
</div>
<div class="mr-1 flex items-center rounded bg-pink-500 font-bold text-white">
<div class="flex h-full items-center rounded-l bg-pink-600 px-2 py-1">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="h-4 w-4">
<path fill-rule="evenodd"
d="M11.54 22.351l.07.04.028.016a.76.76 0 00.723 0l.028-.015.071-.041a16.975 16.975 0 001.144-.742 19.58 19.58 0 002.683-2.282c1.944-1.99 3.963-4.98 3.963-8.827a8.25 8.25 0 00-16.5 0c0 3.846 2.02 6.837 3.963 8.827a19.58 19.58 0 002.682 2.282 16.975 16.975 0 001.145.742zM12 13.5a3 3 0 100-6 3 3 0 000 6z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex h-full items-center rounded bg-pink-500 px-3 py-1">
{{ $coursedate->venue->city }}
</div>
</div>
@foreach ($coursedate->actualdates as $dates)
@if ($coursedate->course->number_of_days > 1)
<div class="mr-1 flex items-center rounded bg-pink-200 font-bold text-pink-800">
<div class="flex h-full items-center gap-1 rounded-l bg-pink-200 px-2 py-1 text-xs">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="inline-flex h-5 w-5">
<path d="M12.75 12.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM7.5 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM8.25 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM9.75 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM10.5 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM12.75 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM14.25 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM15 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM16.5 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM15 12.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM16.5 13.5a.75.75 0 100-1.5.75.75 0 000 1.5z" />
<path fill-rule="evenodd"
d="M6.75 2.25A.75.75 0 017.5 3v1.5h9V3A.75.75 0 0118 3v1.5h.75a3 3 0 013 3v11.25a3 3 0 01-3 3H5.25a3 3 0 01-3-3V7.5a3 3 0 013-3H6V3a.75.75 0 01.75-.75zm13.5 9a1.5 1.5 0 00-1.5-1.5H5.25a1.5 1.5 0 00-1.5 1.5v7.5a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5v-7.5z"
clip-rule="evenodd" />
</svg>
{{ $dates->label }}
</div>
<div class="flex h-full items-center rounded-r bg-pink-700 px-2 py-1 text-pink-100">
{{ $dates->date->format('jS M Y') }}
</div>
</div>
@else
<div class="mr-1 flex items-center rounded bg-pink-200 text-sm font-bold text-pink-800">
<div class="flex h-full rounded-l bg-pink-200 py-1 px-2 text-sm font-bold text-pink-800">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="inline-flex h-5 w-5">
<path d="M12.75 12.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM7.5 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM8.25 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM9.75 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM10.5 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM12.75 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM14.25 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM15 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM16.5 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM15 12.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM16.5 13.5a.75.75 0 100-1.5.75.75 0 000 1.5z" />
<path fill-rule="evenodd"
d="M6.75 2.25A.75.75 0 017.5 3v1.5h9V3A.75.75 0 0118 3v1.5h.75a3 3 0 013 3v11.25a3 3 0 01-3 3H5.25a3 3 0 01-3-3V7.5a3 3 0 013-3H6V3a.75.75 0 01.75-.75zm13.5 9a1.5 1.5 0 00-1.5-1.5H5.25a1.5 1.5 0 00-1.5 1.5v7.5a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5v-7.5z"
clip-rule="evenodd" />
</svg>
</div>
<div class="flex h-full items-center rounded-r bg-pink-700 px-2 py-1 text-pink-100"> {{ $dates->date->format('jS M Y') }} </div>
</div>
@endif
@endforeach
</div>
<div class="mt-6 mb-4">
</div>
</div>
</div>
@empty
No Course Dates to show
@endforelse
</div>
</div>
</div>
The component in itself works fine. But as you can see in the gif below, the non selected checkboxes disappear, so I cannot apply multiple filters.
I suspect the problem lies in the way I’m storing the filters in the array but I can’t see the woods for the trees at the moment. The filtering itself currently works, and any attempt to change the format of the array results in it not working at all.
Thanks in advance.
3
Answers
I've dumped the contents of the array as it's being filled up on the page
Each time you check a box it adds the value into the array. Obviously In the case of the Venue group of checkboxes I can't add a second value because the unselected ones disappear. But what I'd expect is an array that looks like this for example:
I'd then map through the values and build the filter string for Mielesearch, which happens in the closure futher down.
There are certain bugs in Livewire, and this is also one of them.
Try enclosing the each checkbox in a div.
If the issue still presist, add wire:ignore.self to container
After some perusal of the Meilisearch docs:
That suggests that the behaviour is normal 😳
On updating the search results, the
facetDistribution
array is updated with the filters belonging to the results that have been returned, and doesn’t retain the ones with a zero value.Before filtering:
After filtering:
To be honest, I’d expected to retain the filters with a value of zero so I could re-filter and find courses that were present in Colchester OR Norwich, for example. In fact the tutorial I learned the process from appeared to show this behaviour.
I’m going to rewatch it again in case I misunderstood something!
Or perhaps the older version of Meilisearch being used in the tutorial allowed this?