Tricks

Dependent Select Filters

Nov 29, 2022
Hugh Messenger
Table builder

I struggled for a while trying to build a dependent select filter, trying to do it as two separate filters. Turns out the trick is to build it as a single form based filter, with both selects in the custom form.

In my app, we have a FlightLegs table, which pilots use to log flight hours for missions flown in an aircraft for a given day. FlightLegs belong to a DailyLog table, which stores the overall mission/aircraft details - aircraft, flight date, crew members, etc. Aircraft belong to Vendors (contracted aircraft operators). So from the FlightLegs table, the relationship to Aircraft is dailyLog.aircraft, and aircraft belongsTo Vendor.

I needed a pair of filters on the FlightLeg table, filtering for the Vendor and Aircraft, with Aircraft being dependent on Vendor - so if a vendor is selected, only show the aircraft for that vendor, and when a Vendor is selected, clear any current Aircraft selection if it doesn't belong to the selected Vendor.

// filter name can be anything you want
SelectFilter::make('aircraft')
->label('Aircraft')
// build the filter form
->form([
// build the Vendor dropdown
Select::make('vendor_id')
->label('Vendor')
// select all Vendors
->options(
fn() => Vendor::all()->pluck('name', 'id')->toArray()
)
// when state updated, check to see if the currently selected aircraft belongs to the selected vendor ...
->afterStateUpdated(function ($state, callable $get, callable $set) {
$vendor = Vendor::find($state);
 
if ($vendor) {
$aircraftId = (int) $get('aircraft_id');
 
if ($aircraftId && $aircraft = Aircraft::find($aircraftId)) {
if ($aircraft->vendor_id !== $vendor->id) {
// aircraft doesn't belong to vendor, so unselect it
$set('aircraft_id', null);
}
}
}
})
// make the vendor dropdown reactive so we can rebuild the aircraft dropdown when state changes
->reactive(),
// build the Aircraft dropdown
Select::make('aircraft_id')
->label('Aircraft')
// build the options
->options(function (callable $get, callable $set) {
$vendor = Vendor::find($get('vendor_id'));
 
// if a vendor is selected, just fetch the aircraft for this vendor thru the vendor's belongsTo aircraft
if ($vendor) {
return $vendor->aircraft->pluck('n_number', 'id');
}
 
// no vendor selected, so get all aircraft
return Aircraft::all()->pluck('n_number', 'id');
}),
])
// handle the query
->query(function (Builder $query, array $data) {
// get the vendor and aircraft id's from the form's $data array
$aircraftId = (int) $data['aircraft_id'];
$vendorId = (int) $data['vendor_id'];
 
// if an aircraft selected, scope the query to just that aircraft id with a nested whereHas
if (!empty($aircraftId)) {
$query->whereHas(
'dailyLog',
fn(Builder $query) => $query->whereHas(
'aircraft',
fn(Builder $query) => $query->where('id', '=', $aircraftId)
)
);
}
 
// if a vendor selected, scope the query to just aircraft with that vendor id with a nested whereHas
if (!empty($vendorId)) {
$query->whereHas(
'dailyLog',
fn(Builder $query) => $query->whereHas(
'aircraft',
fn(Builder $query) => $query->where('vendor_id', '=', $vendorId)
)
);
}
 
}),
avatar

Thank, it works fine.

however the counter of used filters remains at 0.

screenshot: https://ibb.co/LCk74Vq

avatar

ok, i found a solution https://filamentphp.com/docs/2.x/tables/filters#custom-indicators