I'm trying to make the triggering on material table's column update the URL like:
http://localhost:4200/cars?find=m&sort=make:desc
Also, when I enter URL that includes sort then the table is expected to be sorted by specific column.
Currently, when I click on table column to sort, the infinite loops gets triggered, resulting in URL changing from
http://localhost:4200/cars?find=m&sort=make:desc
to
http://localhost:4200/cars?find=m&sort=make:asc
car-list.component.html
<div class="example-container mat-elevation-z8">
<div class="example-header">
<mat-form-field>
<input matInput [formControl]="filterControl" placeholder="Search">
</mat-form-field>
</div>
<mat-table [dataSource]="dataSource" matSort>
<!-- Id Column -->
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header> Id </mat-header-cell>
<mat-cell *matCellDef="let car"> {{car.id}} </mat-cell>
</ng-container>
<!-- Make Column -->
<ng-container matColumnDef="make">
<mat-header-cell *matHeaderCellDef mat-sort-header> Make </mat-header-cell>
<mat-cell *matCellDef="let car"> {{ car.make }} </mat-cell>
</ng-container>
<!-- Model Column -->
<ng-container matColumnDef="model">
<mat-header-cell *matHeaderCellDef mat-sort-header> Model </mat-header-cell>
<mat-cell *matCellDef="let car"> {{ car.model }} </mat-cell>
</ng-container>
<!-- Numberplatez Column -->
<ng-container matColumnDef="numberplate">
<mat-header-cell *matHeaderCellDef mat-sort-header> Numberplate </mat-header-cell>
<mat-cell *matCellDef="let car"> {{ car.numberplate }} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;" (click)="navigateToCarDetail(row)"></mat-row>
</mat-table>
</div>
car-list.component.ts
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Car } from "../models/car.model";
import { ActivatedRoute, Router } from '@angular/router';
import { CarService } from '../services/car.service';
import { FormControl } from "@angular/forms";
import { debounceTime, takeUntil } from "rxjs/operators";
import { Subject } from 'rxjs';
@Component({
selector: 'app-cars',
templateUrl: './car-list.component.html',
styleUrls: ['./car-list.component.css']
})
export class CarListComponent implements OnInit, AfterViewInit {
displayedColumns: string[] = ['id', 'make', 'model', 'numberplate'];
dataSource: MatTableDataSource<Car>;
cars: Car[] = [];
filterControl: FormControl = new FormControl('');
private ngUnsubscribe = new Subject<void>();
private sort!: MatSort;
// Sorting state variables
private sortField: string | null = null;
private sortDirection: 'asc' | 'desc' = 'asc'; // Default ascending
constructor(private carService: CarService,
private route: ActivatedRoute,
private router: Router) {
this.dataSource = new MatTableDataSource<Car>();
}
@ViewChild(MatSort) set matSort(ms: MatSort) {
this.sort = ms;
if (this.sort) {
// Subscribe to sort changes
this.sort.sortChange.pipe(
debounceTime(300),
takeUntil(this.ngUnsubscribe)
).subscribe((sortState: Sort) => {
this.applySort(sortState);
});
}
}
ngOnInit() {
this.initializeTable();
}
ngAfterViewInit() {
// Set initial sorting and filter from query params after view is initialized
this.route.queryParams.pipe(takeUntil(this.ngUnsubscribe)).subscribe(params => {
const { sort, find } = params;
// Apply initial filter
this.filterControl.setValue(find || '');
this.applyFilter(find || '');
// Apply initial sorting
if (sort) {
const [field, direction] = sort.split(':');
if (field && direction) {
this.sortField = field;
this.sortDirection = direction as 'asc' | 'desc';
if (this.sort) {
this.sort.sort({ id: field, start: direction as 'asc' | 'desc', disableClear: true });
}
}
}
});
}
initializeTable() {
// Fetch cars from service
this.carService.getCars().subscribe((data: Car[]) => {
this.cars = data;
this.dataSource.data = this.cars;
});
// Set filter predicate
this.dataSource.filterPredicate = (data, filter: string): boolean => {
return data.make.toLowerCase().includes(filter) || data.model.toLowerCase().includes(filter) || data.numberplate.toLowerCase().includes(filter);
};
// Subscribe to changes in the filter control with debounce
this.filterControl.valueChanges.pipe(
debounceTime(300),
takeUntil(this.ngUnsubscribe)
).subscribe(value => {
this.applyFilter(value);
});
}
applyFilter(filterValue: string) {
filterValue = filterValue.trim().toLowerCase();
this.dataSource.filter = filterValue;
// Update the URL with the new filter value or remove the parameter if the filter is empty
this.router.navigate([], {
relativeTo: this.route,
queryParams: filterValue ? { find: filterValue } : {},
queryParamsHandling: filterValue ? 'merge' : ''
});
}
applySort(sortState: Sort) {
const sortField = sortState.active;
const sortDirection = sortState.direction as 'asc' | 'desc';
// Update component's sorting state
this.sortField = sortField;
this.sortDirection = sortDirection;
// Update the URL with the new sort value
this.router.navigate([], {
relativeTo: this.route,
queryParams: { sort: `${sortField}:${sortDirection}` },
queryParamsHandling: 'merge'
});
}
navigateToCarDetail(car: Car): void {
this.router.navigate(['/cars', car.id]);
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
UPDATE 1:
The infinite loop is gone but the code not is not sorting the table
Current state:
car-list.component.ts
import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core';
import { MatSort, Sort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Car } from "../models/car.model";
import { ActivatedRoute, Router } from '@angular/router';
import { CarService } from '../services/car.service';
import { FormControl } from "@angular/forms";
import { debounceTime, takeUntil } from "rxjs/operators";
import { Subject } from 'rxjs';
@Component({
selector: 'app-cars',
templateUrl: './car-list.component.html',
styleUrls: ['./car-list.component.css']
})
export class CarListComponent implements OnInit, AfterViewInit {
displayedColumns: string[] = ['id', 'make', 'model', 'numberplate'];
dataSource: MatTableDataSource<Car>;
cars: Car[] = [];
filterControl: FormControl = new FormControl('');
private ngUnsubscribe = new Subject<void>();
@ViewChild(MatSort) sort!: MatSort;
constructor(private carService: CarService,
private route: ActivatedRoute,
private router: Router) {
this.dataSource = new MatTableDataSource<Car>();
}
ngOnInit() {
this.initializeTable();
}
ngAfterViewInit() {
this.route.queryParams.pipe(takeUntil(this.ngUnsubscribe)).subscribe(params => {
const { find } = params;
this.filterControl.setValue(find || '');
this.applyFilter(find || '');
});
}
initializeTable() {
// Fetch cars from service
this.carService.getCars().subscribe((data: Car[]) => {
this.cars = data;
this.dataSource.data = this.cars;
});
// Set filter predicate
this.dataSource.filterPredicate = (data, filter: string): boolean => {
return data.make.toLowerCase().includes(filter) || data.model.toLowerCase().includes(filter) || data.numberplate.toLowerCase().includes(filter);
};
// Subscribe to changes in the filter control with debounce
this.filterControl.valueChanges.pipe(
debounceTime(300),
takeUntil(this.ngUnsubscribe)
).subscribe(value => {
this.applyFilter(value);
});
}
applyFilter(filterValue: string) {
filterValue = filterValue.trim().toLowerCase();
this.dataSource.filter = filterValue;
// Update the URL with the new filter value or remove the parameter if the filter is empty
this.router.navigate([], {
relativeTo: this.route,
queryParams: filterValue ? { find: filterValue } : {},
queryParamsHandling: filterValue ? 'merge' : ''
});
}
applySort(sortState: Sort) {
const sortField = sortState.active;
const sortDirection = sortState.direction as 'asc' | 'desc';
// Update the URL with the new sort value
this.router.navigate([], {
relativeTo: this.route,
queryParams: { sort: `${sortField}:${sortDirection}` },
queryParamsHandling: 'merge'
});
}
navigateToCarDetail(car: Car): void {
this.router.navigate(['/cars', car.id]);
}
}
car-list.component.html
<div class="example-container mat-elevation-z8">
<div class="example-header">
<mat-form-field>
<input matInput [formControl]="filterControl" placeholder="Search">
</mat-form-field>
</div>
<mat-table [dataSource]="dataSource" matSort (matSortChange)="applySort($event)">
<!-- Id Column -->
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header> Id </mat-header-cell>
<mat-cell *matCellDef="let car"> {{car.id}} </mat-cell>
</ng-container>
<!-- Make Column -->
<ng-container matColumnDef="make">
<mat-header-cell *matHeaderCellDef mat-sort-header> Make </mat-header-cell>
<mat-cell *matCellDef="let car"> {{ car.make }} </mat-cell>
</ng-container>
<!-- Model Column -->
<ng-container matColumnDef="model">
<mat-header-cell *matHeaderCellDef mat-sort-header> Model </mat-header-cell>
<mat-cell *matCellDef="let car"> {{ car.model }} </mat-cell>
</ng-container>
<!-- Numberplatez Column -->
<ng-container matColumnDef="numberplate">
<mat-header-cell *matHeaderCellDef mat-sort-header> Numberplate </mat-header-cell>
<mat-cell *matCellDef="let car"> {{ car.numberplate }} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;" (click)="navigateToCarDetail(row)"></mat-row>
</mat-table>
</div>
UPDATE 2:
The full app project is available here
https://github.com/mr-olufsen/webapp
I hope it's easy to reproduce
UPDATE 3:
I expected the existing code filter functionality to work together with sort and be reflected in URL.
Empty filter should resolve to /cars
(not /cars?filter=
)
The sort is expected to be in URL only if it's asc
or desc
When I tested the solution:
When I go to /cars?find=&sort=make:asc
the URL resolves into /cars
but I expected: /cars?sort=make:asc
When I go to /cars?find=4&sort=model:desc
works as expected. Sorting is working. URL is updated accordingly. Thanks to Naren Murali
When I go to /cars
and click on header table columns, URL is expected to change to something like /cars?sort=make:asc
. Actually the table is sorted but URL stays the same /cars
matSort
has a change event. Use that, and get rid of the code inside ViewChild
; the code gets called multiple times (getters and setters are called during every change detection cycle), due to change detection. It's a very bad place to use the subscribe.
HTML:
<mat-table [dataSource]="dataSource" matSort (matSortChange)="applySort($event)">
TS:
@ViewChild(MatSort) sort!: MatSort;
To make sorting work you need a custom function, that will perform the sorting.
function compare(a: number | string, b: number | string, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
...
...
applySort(sort: Sort) {
const data = this.users.slice();
if (!sort.active || sort.direction === '') {
this.dataSource.data = data;
return;
}
this.dataSource.data = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'make':
return compare(a.make, b.make, isAsc);
case 'model':
return compare(a.model, b.model, isAsc);
case 'numberplate':
return compare(a.numberplate, b.numberplate, isAsc);
case 'id':
return compare(a.id, b.id, isAsc);
case 'fat':
default:
return 0;
}
});
}
...