juniora/nova-range-input-filter
Adds a range filter to Laravel Nova resource indexes, letting users narrow records by a numeric or date range through min and max inputs. Drop it into your filters array to filter listings by price, quantity, dates, and similar range-based columns.
Install and verify compatibility first. The package targets Laravel Nova, so confirm the Nova major version supported before anything else:
composer require juniora/nova-range-input-filter
# Check the constraints before you commit
cat vendor/juniora/nova-range-input-filter/composer.json | grep -A3 '"require"'
ls vendor/juniora/nova-range-input-filter/src
cat vendor/juniora/nova-range-input-filter/README.md
At zero stars and a single 2024-12-21 release, treat the README as advisory and the source as authoritative. Skim src/ for the base class name and its apply() / options() signatures — that is your real API.
First use case: a numeric column on an index screen where two separate Number filters (min/max) feel clunky — e.g. order totals, price, age, stock counts.
Generate a filter and swap the parent class for the package's one:
php artisan nova:filter OrderTotalRange
<?php
namespace App\Nova\Filters;
use Juniora\RangeInputFilter\RangeInputFilter; // confirm namespace in src/
class OrderTotalRange extends RangeInputFilter
{
public $name = 'Order Total';
public function apply($request, $value)
{
$value = (array) $value;
$min = $value['min'] ?? null;
$max = $value['max'] ?? null;
return $this->builder
->when($min !== null && $min !== '', fn ($q) => $q->where('total', '>=', (float) $min))
->when($max !== null && $max !== '', fn ($q) => $q->where('total', '<=', (float) $max));
}
}
Register it on the resource:
public function filters(NovaRequest $request)
{
return [new OrderTotalRange];
}
Then load a Nova index page, drag the slider or type endpoints, and verify the generated query with ->dd() or Nova's query log before trusting it.
Normalize the value shape in one place. Range filters typically deliver an array (['min' => '10', 'max' => '99']) or a JSON string depending on how the Vue component serializes. Write a small private helper on a shared base filter and reuse it, rather than repeating (array) casts in every filter:
protected function bounds($value): array
{
$value = is_string($value) ? json_decode($value, true) : $value;
$value = (array) $value;
return [
$this->normalize($value['min'] ?? null),
$this->normalize($value['max'] ?? null),
];
}
private function normalize($v): ?float
{
return ($v === null || $v === '') ? null : (float) $v;
}
Inclusive ranges with whereBetween are cleaner when both bounds exist:
[$min, $max] = $this->bounds($value);
return $this->builder->when(
$min !== null && $max !== null,
fn ($q) => $q->whereBetween('total', [$min, $max]),
function ($q) use ($min, $max) {
$q->when($min !== null, fn ($q) => $q->where('total', '>=', $min))
->when($max !== null, fn ($q) => $q->where('total', '<=', $max));
}
);
Reuse one filter class across columns by adding a configurable column property, so you aren't copy-pasting eight near-identical filters:
class NumericRange extends RangeInputFilter
{
public function __construct(public string $column, ?string $name = null)
{
parent::__construct();
if ($name) { $this->name = $name; }
}
public function apply($request, $value) { /* uses $this->column */ }
public function key() { return 'numeric-range-'.$this->column; }
}
Set sensible slider bounds via options() or meta (min/max/step) so the UI doesn't default to 0–100 on a column that ranges into the millions:
public function options(NovaRequest $request)
{
return ['min' => 0, 'max' => 500000, 'step' => 100];
}
Confirm the exact meta keys in the package's Vue component — that is the contract.
Test apply() directly. Nova filters are plain classes; a unit test with a fake NovaRequest and a query builder against a SQLite table is faster and more reliable than clicking through the UI:
$filter = new OrderTotalRange;
$query = $filter->apply(NovaRequest::create('/', 'GET'), ['min' => 10, 'max' => 50]);
$this->assertStringContainsString('between', $query->toSql());
Compose with existing constraints. $this->builder already carries the resource query, so soft deletes, global scopes, and relationship constraints survive — just don't call ->newQuery() inside apply().
if ($value) will eat 0. A user legitimately filtering ">= 0" or maxing at 0 is a common case. Always compare with !== null && !== ''.'10' >= '9' is true lexicographically in PHP in some comparisons; cast to (float) or (int) before it hits the query builder, and be explicit about whereBetween on decimal columns.'', not null, and both endpoints can be empty simultaneously. Guard the fully-empty case and return the builder untouched, otherwise you'll apply >= '' and silently match nothing.key() to keep URLs stable and distinct.dist/js built against Nova 4's mix/Vite pipeline; on Nova 5 the component can render blank or throw in the console. Open devtools first — if the component 404s or errors on Nova.booting, you'll need npm run dev in the package (or a fork) rather than debugging your PHP.php artisan vendor:publish --tag=nova-range-input-filter-assets (tag name per README) and php artisan nova:publish.composer.json, keep a one-file fork strategy in mind, and avoid extending deep into its internals — override apply() and options() only, and reimplement the ~30-line Vue component yourself if the package goes stale.dd($request->input('filters')) inside apply() shows exactly what Nova sent; ->ddSql() on the returned builder confirms the generated SQL. That pair resolves nearly every "the filter does nothing" report in under a minute.How can I help you explore Laravel packages today?