Sort Table Columns with Angular and Typescript

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/

2 Responses to “Sort Table Columns with Angular and Typescript”

Show comments

Intro to UX Accessibility

Web Content Accessibility Guide (WCAG 2.0) is divided into three conformance levels (A, AA, and AAA). They are organized based on the impact they have on design or visual presentation of the pages. The higher the level, the more restraining it becomes on design.

As a rule of thumb, success criteria from level A should be invisible or barely noticeable to the interface. On the other hand, level AAA will have such a high impact on design, that most organizations will not be able to achieve that level (as the compromises on design will be too important). I recommend a goal of AA conformance, with the understanding that 100% conformance is only goal and is typically very difficult to achieve. This is the same recommendation the W3 organization makes as well. (see below links for reference)

For UX accessibility and conformance to both WCAG 2.0 and Section 508 we use an industry standard tool called Pa11y.

Pa11y is a free tool that produces a report of any compliance issues for any website. This tool can be run on developers machines and your build server so that each time a developer changes code, the tool automatically produces a report of which items are not in compliance and at what level they are. The tool is very fast, less than a few seconds for any page.

In practice, the way it works best is:

During Development: Developers create a web page based on the user story requirements. When complete, they run the tool against the completed page and fix up any issues that are found.

During Acceptance Testing: Final acceptance testing should include a review of the Pa11y report amongst the team members before final signoff.

References:
– WCAG 2.0 guide https://www.w3.org/WAI/intro/wcag
– Conformance levels https://www.w3.org/TR/WCAG20/#conformance-reqs

Leave a comment

Hosting a Serverless Hapi.js API with AWS Lambda

Now an NPM package! https://github.com/carbonrobot/hapi-lambda

A good friend of mine wrote an excellent demo of how to use the Serverless framework to build an AWS Lambda function that can serve more than one request.

The folks over at Serverless have named this approach the “Monolithic Pattern” and its a great way to move into serverless hosting without having the sprawl of multiple hosted Lambda functions. It also helps avoid the dreaded cold-start problem as well. I won’t go into detail about how to use the Serverless framework in this post because I want to focus on the Hapi->Lambda interface itself. There is a full working demo in the link at the bottom that includes everything.

I have been a big fan of the Hapi.js framework for a few years now and really like how it pulls together a complete end-to-end solution for a REST API. It covers request and response validation, authorization, process monitoring, an excellent module/plugin system, and a plugin for everything else. There is also dropin plugins available to generate your swagger.json files so you don’t have to write that crud by hand.

AWS Lambda is becoming more popular for hosting functions on the cloud without needing to spin up a traditional VM or mess with Docker images. Typically, you would host a simple function using javascript in the following way:

exports.handler = function(event, context, callback) {
    var response = {
        statusCode: 200,
        body: JSON.stringify({status: "OK"});
    };
    callback(null, response);
};

Using API Gateway, you can then map a route such as GET /hello/world to this function and make HTTP requests to it. This is great for one-off functions that can be easily separated out into there own Lambda function. The drawback to this approach if you have a lot of functions to host is the pain of provisioning this many things in AWS and the risk of cold-start issues. See the patterns article from Serverless on the pros and cons of each approach.

Hapi.js on the other hand provides you with a full routing system and an HTTP listener. What we need to do is use only the routing, and ignore the HTTP side of things.

Lets take a look at this simple Hello World of Hapi.js

const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({ port: 3000, host: 'localhost' });

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('Hello, world!');
    }
});

server.start((err) => {
    if (err) throw err;
    console.log(`Server running at: ${server.info.uri}`);
});

If you run this locally, Hapi will start listening on port 3000 for any HTTP requests that match the defined routes. To host this in Lambda, we need to take out the listener and invoke the correct function based on the route that Lambda receives.

Looking back at that Lambda example, we can see that it passes an event object into the function. This event object contains information about the raw request such as headers, body, path, and parameters. We can use this information and the the inject method of the Hapi server to directly call our API from Lambda.

const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection();

server.route({
    method: 'GET',
    path: '/',
    handler: function (request, reply) {
        reply('Hello, world!');
    }
});

exports.handler = (event, context, callback) => {

    // map lambda event to hapi request
    const options = {
        method: event.httpMethod,
        url: event.path,
        payload: event.body,
        headers: event.headers,
        validate: false
    };

    server.inject(options, function(res){
        const response = {
            statusCode: res.statusCode,
            body: res.result
        };
        callback(null, response);
    });

};

If your using the Hapi plugin system for your development (and you should be), you need to add some extra plumbing to handle the plugin system register callback.

A full example can be found at https://github.com/carbonrobot/hapi-lambda-demo or you can use the npm package which has more up to date code that handles headers properly. https://github.com/carbonrobot/hapi-lambda

Leave a comment