digital-creative/nova-range-input-filter
A Laravel Nova filter that adds a numeric range input to resource indexes, letting users filter records by minimum and maximum values. Easy to configure, it provides a clean range UI for prices, quantities, scores, and other numeric fields, with no custom filter code required.
Verify the foundation before you write a line of code. This package is a Nova filter component — it has zero value outside a Nova admin panel. Confirm two things first:
composer.json constraints.composer require dcasia/nova-range-input-filter:^1.1
Pin it. There is effectively one meaningful tag (v1.1.0) whose entire changelog is "Upgrade Nova 4 / Fix various issues." Don't float the constraint — treat it as a fixed dependency and read the upstream diff whenever you bump.
Read the source before the README. The README is thin. Open vendor/dcasia/nova-range-input-filter/src and read two files:
$component it declares, and any constructor or config setters for min/max/step/placeholders.apply(). That method is the real specification for what the filter does to your query.That source read is your first hour of work and saves you a day of guessing later. The single most important thing to confirm: the exact shape of $value when apply() is called (array keys, nulls, partial payloads).
First use case: a numeric band on a resource index. The canonical shape is a filter class in app/Nova/Filters that subclasses the package filter and owns the query semantics.
// app/Nova/Filters/OrderTotalRange.php
namespace App\Nova\Filters;
use DigitalCreative\NovaRangeInputFilter\RangeInputFilter; // confirm exact namespace in src
use Illuminate\Http\Request;
class OrderTotalRange extends RangeInputFilter
{
public $name = 'Order total';
public function apply(Request $request, $query, $value)
{
$min = data_get($value, 'min');
$max = data_get($value, 'max');
if ($min === null && $max === null) {
return $query;
}
if ($min !== null && $max !== null) {
return $query->whereBetween('total', [(float) $min, (float) $max]);
}
return $min !== null
? $query->where('total', '>=', (float) $min)
: $query->where('total', '<=', (float) $max);
}
}
Register it on the resource:
public function filters(Request $request)
{
return [new OrderTotalRange];
}
That subclass-with-overridden-apply() shape is where you'll live day to day. The base class contributes the Vue range control; you contribute the query semantics.
Treat apply() as the only source of truth. The range control is presentational. Every correctness concern — inclusivity, null handling, casting, unbounded sides — lives in that one method. Write the branches explicitly (min-only, max-only, both, neither) so each case is individually readable and testable. Never inherit a default apply() and assume it matches your product's definition of "in range."
Pick one query idiom per codebase and stick to it. Use whereBetween for fully-bounded ranges and paired >= / <= for half-open ranges. Mixing idioms across resources makes inclusivity and null behavior subtly inconsistent — which surfaces months later as "why did this filter hide that row?"
Cast deliberately for decimal or numeric-string columns. Cast $value to float/int inside apply() before building the constraint. Passing raw strings into whereBetween usually works but can produce surprising ordering on some engines. It's a one-line fix versus an hour of debugging.
Prefer config setters over subclassing for the simple majority. For commodity price/quantity screens you often don't need a subclass at all — instantiate the package filter with its min/max/step options and let the default behavior run. Reserve subclassing for custom null handling, casts, or operators. Less code means fewer places to inherit a Nova upgrade surprise.
Seed a sensible opening window with ->default([...]). Nova's ->default([...]) gives the filter an initial value so the panel opens on a sane band rather than an empty control. Useful on ops screens where users almost always narrow.
Compose freely with other filters. Nova runs every active filter's apply() sequentially against the same builder. This package touches nothing outside apply(), so stacking it with a search filter, a status filter, or a second range filter on a different column (price + stock) is conflict-free. Just ensure each targets a distinct column.
Keep filter classes in app/Nova/Filters and test them like PHP classes. A cheap feature test — POST to the Nova index endpoint with the filter active and assert on returned IDs — catches most regressions, including behavior changes introduced by a version bump. Run it against all four value branches.
Formatting lives outside the query path. $name and ->withMeta() control what the panel displays. Leave numeric formatting to the Vue component rather than fighting it in PHP — it's cosmetic and doesn't affect filtering.
--ignore-platform-reqs — plan to fork. The code is small (a PHP class plus precompiled Vue), so a day's work is realistic. Smoke-test against your exact Nova 4 minor, not just "Nova 4."apply() should decide deliberately. Test min-only, max-only, both, and neither — those four cases are where filtered listings go wrong.whereBetween is inclusive on both ends and drops NULL rows. Correct for "price between $50 and $200"; wrong if NULL should render as "unknown, show anyway." Handle NULLs explicitly when the product cares.EXPLAIN once on a realistic dataset. On large or revenue-adjacent tables this is not optional, and the filter will never warn you.$value is always a two-key array. A cleared filter, a partial submit, or a programmatic call can hand you null or a partial payload. Use data_get() with defaults at every read.How can I help you explore Laravel packages today?