Overview
Air Tracker provides powerful filtering capabilities to help users find specific flights. Filters are applied in real-time and automatically update the map and flight list.
Operator Filtering Filter by airline or operator name
Ground Status Show flying, grounded, or all aircraft
Filter Model
Filters are defined using a simple interface:
// From flight-filters.ts
export interface FlightFilters {
operator : string | null ; // Selected operator/airline name
onGround : 'all' | 'flying' | 'onGround' ; // Ground status filter
}
The filter model is stored as a signal in the FlightsStoreService, enabling reactive updates throughout the application.
Filter Component
Component Structure
The FlightsFilterMenuComponent provides the filter UI:
@ Component ({
selector: 'app-flights-filter-menu' ,
imports: [
MatIconModule ,
MatMenuModule ,
MatListModule ,
ReactiveFormsModule ,
MatRadioModule ,
MatFormFieldModule ,
MatSelectModule ,
MatButtonModule ,
],
templateUrl: './flights-filter-menu.component.html' ,
styleUrl: './flights-filter-menu.component.scss' ,
})
export class FlightsFilterMenuComponent implements OnInit {
store = inject ( FlightsStoreService );
private readonly destroyRef = inject ( DestroyRef );
filterForm = new FormGroup ({
operator: new FormControl < string [] | null >([ '' ]),
onGround: new FormControl < 'all' | 'flying' | 'onGround' >( 'all' )
});
ngOnInit () {
this . filterForm . valueChanges
. pipe ( takeUntilDestroyed ( this . destroyRef ))
. subscribe ( value => {
const operatorArray = value . operator ?? [];
const operator = operatorArray [ 0 ] || null ;
this . store . updateFilters ({
operator ,
onGround: value . onGround ?? 'all' ,
});
});
}
}
The component uses reactive forms to track filter changes and automatically updates the store whenever a filter value changes.
Operator/Airline Filtering
Operator List Generation
The store automatically generates a unique list of operators from the current flights:
// From FlightsStoreService
/**
* Unique list of operators for filter dropdown
* - Transforms null operators to 'Other'
* - Automatically updates when flights change
* - Sorted alphabetically (Other at the end)
*/
readonly operatorList = computed (() => {
const flights = this . _flights ();
const operators = flights
. map ( f => f . operator === null ? 'Other' : f . operator )
. filter ( Boolean );
return [ ... new Set ( operators )]. sort (( a , b ) => {
if ( a === 'Other' && b !== 'Other' ) return 1 ;
if ( b === 'Other' && a !== 'Other' ) return - 1 ;
if ( a === 'Other' && b === 'Other' ) return 0 ;
return a . localeCompare ( b , undefined , { sensitivity: 'base' });
});
});
Operator Filter UI
< mat-form-field >
< mat-label > Operator </ mat-label >
< mat-select formControlName = "operator" multiple >
< mat-option value = "" > All Operators </ mat-option >
@for (operator of store.operatorList(); track operator) {
< mat-option [value] = "operator" > {{ operator }} </ mat-option >
}
</ mat-select >
</ mat-form-field >
The operator dropdown is populated dynamically from the computed operatorList signal, so it always reflects the current set of flights.
Operator Filtering Logic
// From FlightsStoreService - filteredFlights computed signal
readonly filteredFlights = computed (() => {
const allFlights = this . _flights ();
const { operator , onGround } = this . _filters ();
return allFlights . filter ( flight => {
// Filter by operator
if ( ! operator || operator === '' ) return true ; // No filter
if ( operator === 'Other' ) {
return flight . operator == null || flight . operator === '' ;
}
return flight . operator === operator ;
}). filter ( flight => {
// Filter by ground status (chained below)
// ...
});
});
Filter Behavior:
Filter Value Flights Shown null or ""All flights (no filtering) "Other"Flights with null or empty operator Any operator name Flights matching that exact operator
Ground Status Filtering
Ground Status Options
All Show all aircraft regardless of status
Flying Show only airborne aircraft
On Ground Show only grounded aircraft
Ground Status UI
< mat-radio-group formControlName = "onGround" >
< mat-radio-button value = "all" > All </ mat-radio-button >
< mat-radio-button value = "flying" > Flying </ mat-radio-button >
< mat-radio-button value = "onGround" > On Ground </ mat-radio-button >
</ mat-radio-group >
Ground Status Filtering Logic
// From FlightsStoreService - filteredFlights computed signal
readonly filteredFlights = computed (() => {
const allFlights = this . _flights ();
const { operator , onGround } = this . _filters ();
return allFlights
. filter ( /* operator filter */ ) // First filter by operator
. filter ( flight => {
// Then filter by ground status
if ( onGround === 'all' ) return true ;
if ( onGround === 'flying' ) return ! flight . onGround ;
if ( onGround === 'onGround' ) return flight . onGround ;
return true ;
});
});
Filter Logic:
Filter Value Condition Flights Shown 'all'Always true All flights 'flying'!flight.onGroundOnly airborne 'onGround'flight.onGroundOnly grounded
The onGround property comes from the flight telemetry data and indicates whether the aircraft is currently on the ground.
Combined Filtering
Filters are applied sequentially, allowing for powerful combinations:
readonly filteredFlights = computed (() => {
const allFlights = this . _flights ();
const { operator , onGround } = this . _filters ();
return allFlights
// Step 1: Filter by operator
. filter ( flight => {
if ( ! operator || operator === '' ) return true ;
if ( operator === 'Other' ) {
return flight . operator == null || flight . operator === '' ;
}
return flight . operator === operator ;
})
// Step 2: Filter by ground status
. filter ( flight => {
if ( onGround === 'all' ) return true ;
if ( onGround === 'flying' ) return ! flight . onGround ;
if ( onGround === 'onGround' ) return flight . onGround ;
return true ;
});
});
Example 1
Example 2
Example 3
Filter: operator = "Iberia", onGround = "flying"Result: Only Iberia aircraft that are currently airborne
Filter: operator = "Other", onGround = "onGround"Result: Only grounded aircraft with no operator information
Filter: operator = null, onGround = "all"Result: All flights (no filtering)
Real-Time Filter Updates
Reactive Flow
The filtering system uses Angular signals for instant UI updates:
Update Method
// From FlightsStoreService
/**
* Update filters partially (merge with current)
* @param newFilters - Partial filter updates
* @example store.updateFilters({ operator: 'Iberia' })
*/
updateFilters ( newFilters : Partial < FlightFilters > ): void {
this . _filters . update ( current => ({ ... current , ... newFilters }));
}
The update method merges new filter values with existing ones, so you can update individual filters without affecting others.
Automatic UI Updates
Because filteredFlights is a computed signal, all components consuming it update automatically:
// In FlightsShellComponent
@ Component ({
selector: 'app-flights-shell' ,
template: `
<!-- Map automatically shows only filtered flights -->
<app-flights-map [flights]="store.filteredFlights()"></app-flights-map>
<!-- List automatically shows only filtered flights -->
<app-flights-list [flights]="store.filteredFlights()"></app-flights-list>
<!-- Filter menu bound to store -->
<app-flights-filter-menu></app-flights-filter-menu>
`
})
export class FlightsShellComponent {
store = inject ( FlightsStoreService );
}
No manual subscription or change detection is needed - Angular signals handle all reactivity automatically.
Filter State Management
Initial State
private readonly _filters = signal < FlightFilters >({
operator: null , // No operator filter
onGround: 'all' // Show all flight statuses
});
Reading Current Filters
// Get current filter values
const currentFilters = store . _filters ();
console . log ( currentFilters );
// { operator: "Iberia", onGround: "flying" }
Updating Filters
// Update single filter
store . updateFilters ({ operator: 'Lufthansa' });
// Update multiple filters
store . updateFilters ({
operator: 'Air France' ,
onGround: 'flying'
});
// Clear operator filter
store . updateFilters ({ operator: null });
// Reset to defaults
store . updateFilters ({ operator: null , onGround: 'all' });
Computed Signal Optimization
Angular’s computed signals are highly optimized:
Memoization : Results are cached until dependencies change
Lazy Evaluation : Only recomputes when accessed
Automatic Dependency Tracking : Knows exactly when to recompute
// This computed signal only reruns when _flights or _filters change
readonly filteredFlights = computed (() => {
const allFlights = this . _flights (); // Dependency 1
const { operator , onGround } = this . _filters (); // Dependency 2
return allFlights . filter ( /* ... */ );
});
Even with thousands of flights, filtering is instant because computed signals efficiently track dependencies and minimize unnecessary recalculations.
Usage Example
import { Component , inject } from '@angular/core' ;
import { FlightsStoreService } from './services/flights-store.service' ;
@ Component ({
selector: 'app-filter-demo' ,
template: `
<div>
<h2>Filters</h2>
<!-- Operator dropdown -->
<select (change)="onOperatorChange($event)">
<option value="">All Operators</option>
@for (op of store.operatorList(); track op) {
<option [value]="op">{{ op }}</option>
}
</select>
<!-- Ground status radio buttons -->
<label>
<input type="radio" value="all"
[checked]="store._filters().onGround === 'all'"
(change)="onGroundStatusChange('all')">
All
</label>
<label>
<input type="radio" value="flying"
[checked]="store._filters().onGround === 'flying'"
(change)="onGroundStatusChange('flying')">
Flying
</label>
<label>
<input type="radio" value="onGround"
[checked]="store._filters().onGround === 'onGround'"
(change)="onGroundStatusChange('onGround')">
On Ground
</label>
<!-- Results -->
<p>Showing {{ store.filteredFlights().length }} flights</p>
</div>
`
})
export class FilterDemoComponent {
store = inject ( FlightsStoreService );
onOperatorChange ( event : Event ) {
const value = ( event . target as HTMLSelectElement ). value ;
this . store . updateFilters ({ operator: value || null });
}
onGroundStatusChange ( status : 'all' | 'flying' | 'onGround' ) {
this . store . updateFilters ({ onGround: status });
}
}
Filter Persistence
Currently, filters reset when the page is refreshed. To persist filters across sessions, consider storing them in localStorage or URL query parameters.
Example: LocalStorage Persistence
// Save filters to localStorage
private saveFiltersToStorage (): void {
localStorage . setItem ( 'flight-filters' , JSON . stringify ( this . _filters ()));
}
// Load filters from localStorage
private loadFiltersFromStorage (): FlightFilters {
const stored = localStorage . getItem ( 'flight-filters' );
if ( stored ) {
return JSON . parse ( stored );
}
return { operator: null , onGround: 'all' };
}
// Initialize with stored filters
constructor () {
const savedFilters = this . loadFiltersFromStorage ();
this . _filters . set ( savedFilters );
// Save on every change
effect (() => {
this . saveFiltersToStorage ();
});
}