How to filter a list using Laravel query builder and checkboxes

Nav • May 18, 2020

laravel

Let's set the stage...

We have a list of products that we need to filter. For example, we have a storefront that sells sporting apparel. Each product has a brand (Nike, Adidas etc.) and a category (t-shirts, shoes etc.). We'll show brands and categories as a list of checkboxes. The user should be able to click on the desired brand and/or category (one or more), then click filter, which will refresh the page showing the filtered results.

Packages

The only package we'll be using is Spatie's Laravel query builder. It extends Laravel's default Eloquent builder by making filtering through large lists easier. Check it out on Github or on Spatie docs. I've started using it recently and it has a very fluent API.

Schemas

These models have typical one-to-one relationships, product-to-brand and product-to-category.

I've kept the columns to a bare minimum. Product table looks something like this:

$table->text('id');
$table->text('name');
$table->text('description');
$table->text('category_id');
$table->text('brand_id');
$table->text('thumbnail')->nullable();
$table->text('large_preview')->nullable();

Category table:

$table->text('id');
$table->text('name');

Brand table:

$table->text('id');
$table->text('name');

Layout

To get a feel of what this looks like on the front end, we have a column on the left side which lists brands and categories. Each item has a checkbox. The user can check the desired checkbox items and click the filter button.

laravel query builder checkboxes

Below is the snippet for the left panel blade partial. I'll skip the styling.

<p>Brands</p>
@foreach ($brands as $brand)
    <label class="m-checkbox">
        <input
            name="brand" type="checkbox" value="{{ $brand->id }}"
            @if (in_array($brand->id, explode(',', request()->input('filter.brand'))))
                checked
            @endif
        >
        {{ $brand->name }}
    </label>
@endforeach

<p>Categories</p>
@foreach ($categories as $category)
    <label>
        <input
            name="category" type="checkbox" value="{{ $category->id }}"
            @if (in_array($category->id, explode(',', request()->input('filter.category'))))
                checked
            @endif
        >
        {{ $category->name }}
    </label>
@endforeach

<button type="button" id="filter">Filter</button>

Everything is pretty straight forward. The @if conditional is there so once the form is submitted, we can catch the selected id's through request() object and use them to tick the checkboxes.

Product list renders something like this, again skipping the styles.

@foreach ($products as $product)
    <p>{{ $product->name }}</p>
    <p>{{ $product->description }}</p>
    <button>Add to Cart</button>
    <hr>
@endforeach

Javascript

Laravel query builder can filter models based on URL parameters. We'll use javascript to refresh the page with selected brands and categories. We're trying to stay away from submitting forms with arrays as names. I have not really found a good way to submit form arrays without swapping brackets with URL safe characters. The query string doesn't looking appealing when you swap it out. JS method will keep the URL pretty. This also keeps the request() object variable clean, plus it opens the door to other ideas, like submitting through Ajax or Livewire.

<script>
    function getIds(checkboxName) {
        let checkBoxes = document.getElementsByName(checkboxName);
        let ids = Array.prototype.slice.call(checkBoxes)
                        .filter(ch => ch.checked==true)
                        .map(ch => ch.value);
        return ids;
    }

    function filterResults () {
        let brandIds = getIds("brand");

        let catagoryIds = getIds("catagory");

        let href = 'products?';

        if(brandIds.length) {
            href += 'filter[brand]=' + brandIds;
        }

        if(catagoryIds.length) {
            href += '&filter[category]=' + catagoryIds;
        }

        document.location.href=href;
    }

    document.getElementById("filter").addEventListener("click", filterResults);
</script>

When the filter button is clicked, you can collect all the id's for brand and category. Internally, the getIds function collects all the checkboxes with brand or category names, then filters through, collecting the values of checked checkboxes.

document.location.href=href refreshes the page with the url containing the brand and categories to filter.

document.getElementById("filter").addEventListener("click", filterResults); adds the click event to the filter button.

Controller

The index controller below, without URL parameters, will show all the products. When url parameters are supplied, products will be filtered. Check out Spatie docs for more options.

public function index()
{
    $categories = Category::all();

    $brands = Brand::all();

    $products = QueryBuilder::for(Product::class)
        ->allowedFilters([
            AllowedFilter::exact('brand', 'brand_id')
            AllowedFilter::exact('category', 'category_id'),
            ])
        ->get();

    return view('products.index', compact('products', 'brands', 'categories'));
}

I hope this helps in your filtering with Laravel project. If you have a question, drop a comment and I'll be sure to get back to you.