Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Multiselect Filter Laravel Package

klepak/nova-multiselect-filter

A Laravel Nova filter package that adds a multiselect filter, letting users filter resource listings by multiple values in one control. Useful for narrowing index views by several categories, statuses, or related options without adding separate filters.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require klepak/nova-multiselect-filter

There is no asset publishing step in the typical Nova filter workflow — the filter renders its own Vue component via Nova's auto-registration. Verify by inspecting vendor/klepak/nova-multiselect-filter/src/ and resources/js/ after install; the base class name (likely Klepak\NovaMultiselectFilter\MultiselectFilter) is what you extend.

Check Nova compatibility first. Last release is December 2020, so this was built and tested against Nova 2/3. On Nova 4+ the Vue component may fail to mount, and the Filter contract changed (notably apply(Request $request, $query, $value) semantics and the default()/key() plumbing). Do a smoke test on your Nova version before wiring it into a resource.

First use case — a Resource with a status column you want to filter by one or many values, without building a custom Vue filter yourself:

// app/Nova/Filters/StatusFilter.php
use Klepak\NovaMultiselectFilter\MultiselectFilter;

class StatusFilter extends MultiselectFilter
{
    public function name() { return 'Status'; }

    public function options() {
        return [
            'draft'     => 'Draft',
            'published' => 'Published',
            'archived'  => 'Archived',
        ];
    }

    public function apply(Request $request, $query, $value)
    {
        $value = array_filter((array) $value);
        return $value ? $query->whereIn('status', $value) : $query;
    }
}

Register it in NovaServiceProvider::filters() (global) or in the Resource's filters(Request $request) (per-resource). Start with the per-resource form so you can iterate without affecting other resources.

Implementation Patterns

Global vs. resource filter. Put cross-cutting filters (tenant, region, publish-state) in NovaServiceProvider::filters(). Put column-specific ones (e.g., user_id) in the Resource's filters() method — this keeps the dropdown scoped to where it makes sense and avoids "why is this filter showing on Orders?" confusion.

One filter class per column, not per value. The temptation is to add one filter per status. Resist it — a single multiselect filter with an options() map is one component and one query clause, whereas N single-select filters produce N round-trips and N where bindings.

Handle the "no selection" case explicitly. On Nova filter reset, $value may be null, [], or [''] depending on the Nova version. Normalize with array_filter((array) $value) before deciding whether to apply a whereIn. Returning the query untouched is the correct behavior — never whereIn('status', []) (which yields zero rows on MySQL).

Compose with relationship filters. When filtering on a related model, join eagerly and reuse the same whereIn idiom:

public function apply(Request $request, $query, $value)
{
    $value = array_filter((array) $value);
    return $value
        ? $query->whereHas('author', fn ($q) => $q->whereIn('team_id', $value))
        : $query;
}

Combine with default() for dashboard UX. If you want the filter pre-selected (e.g., default to "active + published"), override default() in your subclass to return the array of keys. This is far cleaner than post-processing the request in the Resource.

Reach for the package's select shape, not a Select field. A common misstep is to assume you can drop a Laravel\Nova\Fields\Select into filters() — you can't, it needs a filter's key() / apply() contract. The multiselect package gives you that contract while keeping the multi-value UI.

Testing tip. Unit-test apply() directly: instantiate the filter, call apply($request, Model::query(), ['a','b']), assert toSql() or bindings. This is faster than exercising the Vue component through Dusk and catches the empty-array bug before QA does.

Gotchas and Tips

  • Abandoned since Dec 2020 — treat it as a fork candidate. With 14 stars and a 2.09 score, expect no Nova 4 fixes upstream. If your app targets Nova 4/5, budget an afternoon to vendor the resources/js/components/Filter.vue, publish it (nova:publish or manual copy to nova-components/), and rebuild with the current Nova mix config. The PHP side is small; the Vue side is where breakage lives.
  • Class name discovery. The namespace is not the repo name — grep the src/ directory for namespace before guessing. A quick grep -R "class .*Filter" vendor/klepak/nova-multiselect-filter/src saves a stack trace.
  • options() must return an associative array (key => label), not a list. If you pass ['Draft', 'Published'], the stored value will be the numeric index and your whereIn will silently match column values 0, 1, etc. Always use key => label.
  • Debugging a filter that "does nothing": check Nova::filters() order — filters are applied in sequence and a later filter that returns an unscoped $query can clobber an earlier one. Log $query->toSql() and getBindings() in apply() (guarded by app()->isLocal()) when results look wrong.
  • Reset button wipes the whole filter bar. If users complain that clearing the multiselect nukes other filters, that's Nova's behavior, not this package's — but on older Nova it manifests only with this filter's component, so confirm before filing a bug upstream.
  • URL encoding in the query string. Nova serializes filter state as ?status[0]=draft&status[1]=published (or similar). If you write a custom bookmark/deep-link feature, don't hand-roll the encoding — reuse Nova's filter serializer, or you'll get single-item arrays where the component expects keys.
  • Extension point worth knowing: because apply() is a public method, you can subclass a shared base filter to centralize tenant scoping and only override options() per resource. This is the cleanest way to reuse the package across 20 resources without copying apply() 20 times.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment