gekich/nova-range-filter
A Laravel Nova filter package that adds range-based filtering to Nova resources, letting users narrow records by numeric or date ranges from the filter panel. A small, low-adoption add-on for simple interval filtering in admin panels.
composer require gekich/nova-range-filter
php artisan vendor:publish --tag=nova-range-filter-assets
php artisan nova:publish
php artisan nova:filter PriceRange
use Gekich\NovaRangeFilter\RangeFilter; // verify exact namespace in src/
class PriceRange extends RangeFilter
{
public $name = 'Price Range';
public function column(): string
{
return 'price';
}
}
public function filters(Request $request)
{
return [new PriceRange];
}
Where to look first: src/ for the base filter class (this is the entire API surface — typically a $column, an apply() doing whereBetween, and a key()/options() stub) and resources/js/ for the range component that defines how the min/max value is serialized. Read the base class before writing any filter — there is almost no documentation to fall back on.
First use case: a numeric min/max on a single indexed column (price, quantity, age). Dates are the second use case but need more care (see Gotchas).
Numeric range on a local column — the intended happy path:
class PriceRange extends RangeFilter
{
public $name = 'Price';
public function column(): string { return 'price'; }
}
Date range — override apply() rather than relying on whereBetween against a datetime column, so the upper bound includes the whole day:
public function apply(Request $request, $query, $value)
{
[$from, $to] = [$value['min'] ?? null, $value['max'] ?? null];
return $query
->when($from, fn ($q) => $q->whereDate('created_at', '>=', $from))
->when($to, fn ($q) => $q->whereDate('created_at', '<=', $to));
}
Relation / qualified columns — the base class usually passes the column straight into whereBetween, so give it the qualified name to avoid ambiguity:
public function column(): string { return 'orders.total'; }
and ensure the relationship is joined before the filter runs.
Default range — implement default() to pre-fill the inputs so the filter is visible in the URL and active on first load:
public function default()
{
return ['min' => null, 'max' => null];
}
Reuse across resources — because the column is the only real variable, build one generic filter with a constructor argument and instantiate it per resource:
class NumericRange extends RangeFilter
{
public function __construct(public string $column, public ?string $label = null) {}
public function column(): string { return $this->column; }
}
// filters(): [new NumericRange('price', 'Price'), new NumericRange('stock', 'Stock')]
Stacking with other filters — nothing special is required; Nova applies each filter's apply() in sequence. Keep the range filter narrow (one column) so it composes cleanly with search and relationship filters.
Testing — Nova filters are plain classes, so unit-test apply() directly against a query builder with a fake Request; you don't need HTTP tests or a browser.
apply() into your own Filter subclass. There will be no fixes for Nova version drift.Nova::script() registration, and the Vue component API all changed after v3. Check composer.json constraints and the component's mixins/Form usage before installing on anything newer than v3.nova:publish + npm run dev/prod in the host app.$value shape is an array with possible nulls. Both min and max may be empty strings or null when only one side is filled. Always guard before whereBetween — passing [null, null] silently returns zero rows on some drivers.whereBetween('created_at', ['2024-01-01', '2024-01-31']) excludes everything after 00:00:00 on the 31st. Use whereDate, or append 23:59:59, or use an exclusive < nextDay bound.key(); renaming the filter class or changing the value shape breaks bookmarked URLs and silently drops the filter. Keep the class name stable.dd($value) at the top of apply() is the fastest way to see what the component actually sends. If apply() never fires, the issue is asset registration; if it fires with the wrong data, the issue is the Vue component's value handling.apply(), default(), key(), and column(). Anything more elaborate (presets, "last 30 days" shortcuts) belongs in your own filter class rather than in a subclass of this one.How can I help you explore Laravel packages today?