diff --git a/resources/lang/en/ui.php b/resources/lang/en/ui.php
index 2bfaf49..ec8c63e 100644
--- a/resources/lang/en/ui.php
+++ b/resources/lang/en/ui.php
@@ -5,4 +5,5 @@
'default' => 'Default',
'all' => 'All',
'value' => 'Value',
+ 'to' => 'to',
];
diff --git a/resources/views/livewire/filters/lf-dual-range.blade.php b/resources/views/livewire/filters/lf-dual-range.blade.php
new file mode 100644
index 0000000..5342bda
--- /dev/null
+++ b/resources/views/livewire/filters/lf-dual-range.blade.php
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ {{ __('statamic-livewire-filters::ui.to') }}
+
+
+
+
+
+
+@script
+
+@endscript
\ No newline at end of file
diff --git a/src/Http/Livewire/LfDualRangeFilter.php b/src/Http/Livewire/LfDualRangeFilter.php
new file mode 100644
index 0000000..332046b
--- /dev/null
+++ b/src/Http/Livewire/LfDualRangeFilter.php
@@ -0,0 +1,93 @@
+condition = 'dual-range';
+ $this->selectedMin = $defaultMin ?? $this->min;
+ $this->selectedMax = $defaultMax ?? $this->max;
+ }
+
+ public function dispatchEvent()
+ {
+ $this->dispatch('filter-updated',
+ field: $this->field,
+ condition: $this->condition,
+ payload: [
+ 'min' => $this->selectedMin,
+ 'max' => $this->selectedMax,
+ ],
+ command: 'replace',
+ modifier: $this->modifier,
+ )
+ ->to(LivewireCollection::class);
+ }
+
+ public function updatedSelectedMin($value)
+ {
+ // Ensure min doesn't exceed max - minRange
+ if ($value > $this->selectedMax - $this->minRange) {
+ $this->selectedMin = $this->selectedMax - $this->minRange;
+ }
+ $this->dispatchEvent();
+ }
+
+ public function updatedSelectedMax($value)
+ {
+ // Ensure max doesn't go below min + minRange
+ if ($value < $this->selectedMin + $this->minRange) {
+ $this->selectedMax = $this->selectedMin + $this->minRange;
+ }
+ $this->dispatchEvent();
+ }
+
+ // #[On('livewire:initialized')]
+ // public function livewireComponentReady()
+ // {
+ // $this->dispatchEvent();
+ // }
+
+ #[On('preset-params')]
+ public function setPresetSort($params)
+ {
+ $paramKey = $this->getParamKey();
+ if (isset($params[$paramKey]['min'])) {
+ $this->selectedMin = $params[$paramKey]['min'];
+ }
+ if (isset($params[$paramKey]['max'])) {
+ $this->selectedMax = $params[$paramKey]['max'];
+ }
+ }
+
+ public function render()
+ {
+ return view('statamic-livewire-filters::livewire.filters.'.$this->view);
+ }
+}
diff --git a/src/Http/Livewire/LivewireCollection.php b/src/Http/Livewire/LivewireCollection.php
index ae8b174..0d155f5 100644
--- a/src/Http/Livewire/LivewireCollection.php
+++ b/src/Http/Livewire/LivewireCollection.php
@@ -55,6 +55,11 @@ public function filterUpdated($field, $condition, $payload, $command, $modifier)
return;
}
+ if ($condition === 'dual-range') {
+ $this->handleDualRangeCondition($field, $payload, $command, $modifier);
+
+ return;
+ }
$this->handleCondition($field, $condition, $payload, $command);
}
diff --git a/src/Http/Livewire/Traits/HandleParams.php b/src/Http/Livewire/Traits/HandleParams.php
index ddb3e55..89da163 100644
--- a/src/Http/Livewire/Traits/HandleParams.php
+++ b/src/Http/Livewire/Traits/HandleParams.php
@@ -127,6 +127,36 @@ protected function handleQueryScopeCondition($field, $payload, $command, $modifi
$this->dispatchParamsUpdated();
}
+ protected function handleDualRangeCondition($field, $payload, $command, $modifier)
+ {
+ $minModifier = 'gte';
+ $maxModifier = 'lte';
+
+ // If the modifier is set, we need to extract the min and max modifiers
+ if ($modifier !== null) {
+ [$minModifier, $maxModifier] = explode('|', $modifier);
+ }
+
+ $minParamKey = $field.':'.$minModifier;
+ $maxParamKey = $field.':'.$maxModifier;
+
+ switch ($command) {
+ case 'replace':
+ $this->params[$minParamKey] = $payload['min'];
+ $this->params[$maxParamKey] = $payload['max'];
+ break;
+
+ case 'clear':
+ unset($this->params[$minParamKey]);
+ unset($this->params[$maxParamKey]);
+ break;
+
+ default:
+ throw new CommandNotFoundException($command);
+ }
+ $this->dispatchParamsUpdated();
+ }
+
protected function runCommand($command, $paramKey, $value)
{
switch ($command) {
diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php
index 0d370e4..88a3b65 100644
--- a/src/ServiceProvider.php
+++ b/src/ServiceProvider.php
@@ -6,6 +6,7 @@
use Livewire\Livewire;
use Reach\StatamicLivewireFilters\Http\Livewire\LfCheckboxFilter;
use Reach\StatamicLivewireFilters\Http\Livewire\LfDateFilter;
+use Reach\StatamicLivewireFilters\Http\Livewire\LfDualRangeFilter;
use Reach\StatamicLivewireFilters\Http\Livewire\LfRadioFilter;
use Reach\StatamicLivewireFilters\Http\Livewire\LfRangeFilter;
use Reach\StatamicLivewireFilters\Http\Livewire\LfSelectFilter;
@@ -46,6 +47,7 @@ public function bootAddon()
Livewire::component('livewire-collection', LivewireCollectionComponent::class);
Livewire::component('lf-checkbox-filter', LfCheckboxFilter::class);
Livewire::component('lf-date-filter', LfDateFilter::class);
+ Livewire::component('lf-dual-range-filter', LfDualRangeFilter::class);
Livewire::component('lf-radio-filter', LfRadioFilter::class);
Livewire::component('lf-range-filter', LfRangeFilter::class);
Livewire::component('lf-text-filter', LfTextFilter::class);
diff --git a/tests/Feature/LfDualRangeFilterTest.php b/tests/Feature/LfDualRangeFilterTest.php
new file mode 100644
index 0000000..d1ed13f
--- /dev/null
+++ b/tests/Feature/LfDualRangeFilterTest.php
@@ -0,0 +1,229 @@
+collection = Facades\Collection::make('yachts')->save();
+ $this->blueprint = Facades\Blueprint::make()->setContents([
+ 'sections' => [
+ 'main' => [
+ 'fields' => [
+ [
+ 'handle' => 'title',
+ 'field' => [
+ 'type' => 'text',
+ 'display' => 'Title',
+ ],
+ ],
+ [
+ 'handle' => 'cabins',
+ 'field' => [
+ 'type' => 'text',
+ 'display' => 'Cabins',
+ 'listable' => 'hidden',
+ ],
+ ],
+ [
+ 'handle' => 'year',
+ 'field' => [
+ 'type' => 'text',
+ 'display' => 'Year',
+ 'listable' => 'hidden',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ $this->blueprint->setHandle('yachts')->setNamespace('collections.'.$this->collection->handle())->save();
+
+ $this->makeEntry($this->collection, 'yacht-a')->set('title', 'Luxury Yacht A')->set('cabins', 4)->set('year', 2020)->save();
+ $this->makeEntry($this->collection, 'yacht-b')->set('title', 'Luxury Yacht B')->set('cabins', 6)->set('year', 2022)->save();
+ $this->makeEntry($this->collection, 'yacht-c')->set('title', 'Luxury Yacht C')->set('cabins', 8)->set('year', 2024)->save();
+ }
+
+ #[Test]
+ public function it_renders_the_component_with_correct_min_and_max_values()
+ {
+ Livewire::test(LfDualRangeFilter::class, [
+ 'field' => 'cabins',
+ 'blueprint' => 'yachts.yachts',
+ 'condition' => 'gte|lte',
+ 'min' => 2,
+ 'max' => 10,
+ 'defaultMin' => 4,
+ 'defaultMax' => 8,
+ 'minRange' => 2,
+ ])
+ ->assertSet('selectedMin', 4)
+ ->assertSet('selectedMax', 8)
+ ->assertSet('minRange', 2)
+ ->assertSee('10');
+ }
+
+ #[Test]
+ public function it_throws_a_field_not_found_exception_if_the_field_doesnt_exist()
+ {
+ $this->expectExceptionMessage('Field [not-a-field] not found');
+
+ Livewire::test(LfDualRangeFilter::class, [
+ 'field' => 'not-a-field',
+ 'blueprint' => 'yachts.yachts',
+ 'condition' => 'between',
+ 'min' => 2,
+ 'max' => 10,
+ 'defaultMin' => 4,
+ 'defaultMax' => 8,
+ ]);
+ }
+
+ #[Test]
+ public function it_throws_a_blueprint_not_found_exception_if_the_blueprint_doesnt_exist()
+ {
+ $this->expectExceptionMessage('Blueprint [not-a-blueprint] not found');
+
+ Livewire::test(LfDualRangeFilter::class, [
+ 'field' => 'cabins',
+ 'blueprint' => 'yachts.not-a-blueprint',
+ 'condition' => 'between',
+ 'min' => 2,
+ 'max' => 10,
+ 'defaultMin' => 4,
+ 'defaultMax' => 8,
+ ]);
+ }
+
+ // #[Test]
+ // public function it_enforces_minimum_range_between_handles()
+ // {
+ // $component = Livewire::test(LfDualRangeFilter::class, [
+ // 'field' => 'cabins',
+ // 'blueprint' => 'yachts.yachts',
+ // 'condition' => 'between',
+ // 'min' => 2,
+ // 'max' => 10,
+ // 'defaultMin' => 4,
+ // 'defaultMax' => 8,
+ // 'minRange' => 2,
+ // ]);
+
+ // // Try to set min too close to max
+ // $component->set('selectedMin', 7)
+ // ->assertSet('selectedMin', 6) // Should be forced to max - minRange
+ // ->assertSet('selectedMax', 8);
+
+ // // Try to set max too close to min
+ // $component->set('selectedMax', 5)
+ // ->assertSet('selectedMin', 4)
+ // ->assertSet('selectedMax', 6); // Should be forced to min + minRange
+ // }
+
+ #[Test]
+ public function it_dispatches_filter_updated_event_when_values_change()
+ {
+ Livewire::test(LfDualRangeFilter::class, [
+ 'field' => 'cabins',
+ 'blueprint' => 'yachts.yachts',
+ 'condition' => 'dual-range',
+ 'min' => 2,
+ 'max' => 10,
+ 'minRange' => 2,
+ ])
+ ->set('selectedMin', 5)
+ ->assertDispatched('filter-updated',
+ field: 'cabins',
+ condition: 'dual-range',
+ payload: ['min' => 5, 'max' => 10],
+ command: 'replace',
+ );
+ }
+
+ #[Test]
+ public function collection_component_handles_dual_range_filter_events()
+ {
+ Livewire::test(LivewireCollection::class, ['params' => ['from' => 'yachts']])
+ ->dispatch('filter-updated',
+ field: 'cabins',
+ condition: 'dual-range',
+ payload: ['min' => 5, 'max' => 10],
+ command: 'replace',
+ modifier: null,
+ )
+ ->assertSet('params', [
+ 'cabins:gte' => 5,
+ 'cabins:lte' => 10,
+ ])
+ ->dispatch('filter-updated',
+ field: 'cabins',
+ condition: 'dual-range',
+ payload: ['min' => 5, 'max' => 8],
+ command: 'replace',
+ modifier: null,
+ )
+ ->assertSet('params', [
+ 'cabins:gte' => 5,
+ 'cabins:lte' => 8,
+ ]);
+ }
+
+ #[Test]
+ public function collection_component_handles_different_conditions_by_modifier()
+ {
+ Livewire::test(LivewireCollection::class, ['params' => ['from' => 'yachts']])
+ ->dispatch('filter-updated',
+ field: 'cabins',
+ condition: 'dual-range',
+ payload: ['min' => 5, 'max' => 10],
+ command: 'replace',
+ modifier: 'gt|lt',
+ )
+ ->assertSet('params', [
+ 'cabins:gt' => 5,
+ 'cabins:lt' => 10,
+ ]);
+ }
+
+ // #[Test]
+ // public function it_loads_preset_params_correctly()
+ // {
+ // Livewire::test(LfDualRangeFilter::class, [
+ // 'field' => 'cabins',
+ // 'blueprint' => 'yachts.yachts',
+ // 'condition' => 'between',
+ // 'min' => 2,
+ // 'max' => 10,
+ // 'defaultMin' => 4,
+ // 'defaultMax' => 8,
+ // ])
+ // ->assertSet('selectedMin', 4)
+ // ->assertSet('selectedMax', 8)
+ // ->dispatch('preset-params', ['cabins:between' => ['min' => 5, 'max' => 7]])
+ // ->assertSet('selectedMin', 5)
+ // ->assertSet('selectedMax', 7);
+ // }
+
+ protected function makeEntry($collection, $slug)
+ {
+ return EntryFactory::id($slug)->collection($collection)->slug($slug)->make();
+ }
+}