Sort Table Columns with Angular and Typescript
Ancient Knowledge
This article is getting old. It was written in the ancient times and the world of software development has changed a lot since then. I'm keeping it here for historical purposes, but I recommend you check out the newer articles on my site.
Here is a simple method for adding sorting to any table in Angular 2+. It's a simple approach that allows you to define how the table is filled with data, instead of the table sort forcing you to use either client side or server side.
tldr; Here is the Plunker https://plnkr.co/DITVzCSqHHB1uNrTxFit/
Let's start with our basic table and some data. Assuming we have a component that serves this html and exposes a public property that is an array of Customers, we can do the standard *ngFor syntax to display our table data.
<table>
<thead>
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of customers">
<td>{{c.id}}</td>
<td>{{c.firstname}}</td>
<td>{{c.lastname}}</td>
</tr>
</tbody>
</table>
export class AppComponent implements OnInit {
customers: Customer[];
constructor(private service: CustomerService){}
getCustomers(){
this.customers = this.service.getCustomers();
}
ngOnInit(){
this.getCustomers();
}
}
The service above could be a static data service like above, or you could be returning an observable and subscribing to it. The key takeaway here is you don't need to do anything out of the ordinary to make this table sort work. Just standard Angular methods of loading data into a table.
Sort Icons
The first thing I want to be able to do is display those fancy caret icons next to the column names so I know which column is being sorted. The trick to watch out for, especially with tables, is that browsers expect <table>
content to be very specific, so you can't have angular wrapping the dom with extra elements. To that end, we need to make a component that targets a directive, instead of its own html element. I like to start at the usage and work backwards, so expanding our html table I want the syntax to end up like this
<table>
<thead>
<tr>
<th sortable-column="id">Id</th>
<th sortable-column="firstname">First Name</th>
<th sortable-column="lastname">Last Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of customers">
<td>{{c.id}}</td>
<td>{{c.firstname}}</td>
<td>{{c.lastname}}</td>
</tr>
</tbody>
</table>
The sortable-column
attribute adds up and down chevrons and binds a click handler to the column header. It takes a parameter that is equal to the data column name we want to sort by.
import { Component, OnInit, Input, EventEmitter, OnDestroy, HostListener } from '@angular/core';
@Component({
selector: '[sortable-column]',
templateUrl: './sortable-column.component.html'
})
export class SortableColumnComponent implements OnInit {
constructor() { }
@Input('sortable-column')
columnName: string;
@Input('sort-direction')
sortDirection: string = '';
@HostListener('click')
sort() {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
}
ngOnInit() { }
}
<i class="fa fa-chevron-up" *ngIf="sortDirection === 'asc'" ></i>
<i class="fa fa-chevron-down" *ngIf="sortDirection === 'desc'"></i>
<ng-content></ng-content>
Now if we click the headers, the caret changes to the indicate the sort direction.
Component Communication
We only want one column to be sorted at any given time so we need a way for the columns to communicate. We can create a service to broker between the columns when changes occur.
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
@Injectable()
export class SortService {
constructor() { }
private columnSortedSource = new Subject<ColumnSortedEvent>();
columnSorted$ = this.columnSortedSource.asObservable();
columnSorted(event: ColumnSortedEvent) {
this.columnSortedSource.next(event);
}
}
export interface ColumnSortedEvent {
sortColumn: string;
sortDirection: string;
}
Now we can expand our SortableColumnComponent
to subscribe to changes from other columns.
import { Component, OnInit, Input, Output, EventEmitter, OnDestroy, HostListener } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { SortService } from './sort.service';
@Component({
selector: '[sortable-column]',
templateUrl: './sortable-column.component.html'
})
export class SortableColumnComponent implements OnInit, OnDestroy {
constructor(private sortService: SortService) { }
@Input('sortable-column')
columnName: string;
@Input('sort-direction')
sortDirection: string = '';
private columnSortedSubscription: Subscription;
@HostListener('click')
sort() {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
this.sortService.columnSorted({ sortColumn: this.columnName, sortDirection: this.sortDirection });
}
ngOnInit() {
// subscribe to sort changes so we can react when other columns are sorted
this.columnSortedSubscription = this.sortService.columnSorted$.subscribe(event => {
// reset this column's sort direction to hide the sort icons
if (this.columnName != event.sortColumn) {
this.sortDirection = '';
}
});
}
ngOnDestroy() {
this.columnSortedSubscription.unsubscribe();
}
}
When a column is clicked, it calls the sortService.columnSorted
method with the details of what changed. The SortService
emits an event with that information to all subscribers. Each column listens for that event and if the sort column has changed, it hides its icons to indicate to the user the table is no longer sorted by that column. The column that emitted the event ignores the change.
Sorting the Table
To finish this up, we need a way to listen to the sort event and bubble the change to our table. We could subscribe directly to the SortService itself and that would work OK, but it gets messy if you have more than one table. Its better to encapsulate this into its own directive and bubble the event through the table since that is what we are sorting. Again, I like to design how I want to use the directive first.
<table sortable-table (sorted)="onSorted($event)">
<thead>
<tr>
<th sortable-column="id" sort-direction="asc">Id</th>
<th sortable-column="firstname">First Name</th>
<th sortable-column="lastname">Last Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let c of customers">
<td>{{c.id}}</td>
<td>{{c.firstname}}</td>
<td>{{c.lastname}}</td>
</tr>
</tbody>
</table>
Here we can see a new attribute sortable-table
and I'm using the Angular () syntax to subscribe to changes in sorting.
import { Directive, OnInit, EventEmitter, Output, OnDestroy, Input } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { SortService } from './sort.service';
@Directive({
selector: '[sortable-table]'
})
export class SortableTableDirective implements OnInit, OnDestroy {
constructor(private sortService: SortService) {}
@Output()
sorted = new EventEmitter();
private columnSortedSubscription: Subscription;
ngOnInit() {
this.columnSortedSubscription = this.sortService.columnSorted$.subscribe(event => {
this.sorted.emit(event);
});
}
ngOnDestroy() {
this.columnSortedSubscription.unsubscribe();
}
}
This directive has no visual component, but instead gives us an extension point to subscribe to changes. This allows us the flexibility of multiple tables on screen and can be more easily extended with additional features. A full working sample can be found on Plunker. https://plnkr.co/DITVzCSqHHB1uNrTxFit/