Angular Data Tables for jQuery DataTables Developers: A Simple Start
Ben Butler
Posted on June 17, 2019
DataTables vs. Data Tables
If you've been developing for a "while" like I have (so you also feel old), there's a decent chance that you've used jQuery on the front end (or still use it) and thus have made use of jQuery DataTables (all one word). There's also a decent chance that you're moving to newer JavaScript (well, TypeScript, in this case) frameworks such as Angular for your "greenfield" (new code) projects to create SPA (Single Page Applications), which many would agree is the best approach.
So, how would you use Data Tables (two words) in Angular Material to create tables that function like jQuery DataTables? Let's go through how Angular Material Data Tables works, how it compares to jQuery DataTables, and how to create similar elements with the same data.
To get some "housekeeping" out of the way, all of the examples I link to on my site and show images of here use the Employees Sample Database, which as I noted there is used on the jQuery DataTables site as well. To use that data with this example, I retrieve it from an API I wrote in Laravel Lumen, but obviously you can use any API and database you want to get data into your Angular project. I'm also going to assume basic knowledge of Angular 8, including HttpClient. I would also highly recommend you take a look at the CDK table documentation on the Angular Material site (which I missed/skipped the first time I read the docs due to the fact it's pretty much mentioned as an aside) as I found it pretty vital to get a solid understanding of how exactly Angular Data Tables work and how to customize them. With all that said, let's move on to a simple example.
I'm skipping the "Getting Started" tutorial with a static array, because it's already there, and (as that tutorial mentions), there is virtually no "real world" use for a table built from a static array in the front end. I'm instead going to move on to what amounts to the previous practice of "AJAX-ing in" data, very much like this function my "old site", using AJAX:
function initDeptTable () {
var table = $('#employee-dept').DataTable({
"ajax" : {
"url" : '../main/php/serverHTML.php?type=JSON&content=employee-dept',
"cache": false,
"contentType": "application/json",
}
I've based this update example largely based upon the "Table retrieving data through HTTP" example here, with a large number of "tweaks" because I had the ability to use multiple services/classes/interfaces/etc, and my own API.
In that example, there are two simple interfaces, one for the collection of items and a count of those items and another for the items themselves. This is a solid practice (and as you might guess it's typically best to do the count on the back end if you have control over the API). The only change I made was to break these up into multiple files:
export interface Department {
DepartmentName: string;
DepartmentNumber: string;
}
import { Department } from './department.model';
export interface DepartmentApi {
items: Department[];
total_count: number;
}
As compared to jQuery DataTables, this might seem a bit complicated (and there is a little more work so far), but keep in mind we're getting dynamic table content and this is our means of reformatting the JSON into the specific format we need, removing the need to reformat it on the back-end or with custom JavaScript. I had to use this PHP to reformat my previous JSON like this for jQuery DataTables. With more control over each step of building the table, we also gain more flexibility. For example, I'm going to integrate some of this tutorial (note it's for Angular 6, but we can still use most of it). Specifically, the concept of using an observable for our data instead of MatTableDataSource (here's why). Here's a service that provides that Observable:
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http';
import { catchError, retry } from 'rxjs/operators';
import { DepartmentApi } from './shared/department-api.model';
@Injectable({
providedIn: 'root'
})
export class DepartmentsService {
constructor(private http: HttpClient) { }
DepartmentUrl = 'https://api.benfrog.net/departments';
getDepartments(sortDirection: string, sortParameter: string ):Observable<DepartmentApi> {
sortDirection = sortDirection.trim();
sortParameter = sortParameter.trim();
let httpParams = new HttpParams({ fromObject: { sortDirection: sortDirection, sortBy: sortParameter } });
return this.http.get<DepartmentApi>(this.DepartmentUrl, {params: httpParams})
.pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
'Backend returned code ${ 'error.status'}, ' +
'body was: ${ 'error.error'} ');
}
// return an observable with a user-facing error message
return throwError(
'Something bad happened; please try again later.');
};
}
What we are doing here is providing an Observable that calls the HttpClient's http.get() function, expects a return as a "DepartmentApi" object (the array of Department objects and the count of those object), and implements some simple error handling:
return this.http.get<DepartmentApi>(this.DepartmentUrl, {params: httpParams})
.pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}
In the Department Table Component's ngOnInit function (which works much like the oft-usedjQuery $(document).ready() function), the constructor requires its own private instance of HttpClient, then passes it along to an instance of the above departmentService, which makes use of it to get our API data (the entire component is here):
this.departmentsService = new DepartmentsService(this.http);
merge(this.sort.sortChange)
.pipe(
startWith({}),
switchMap(() => {
this.isLoadingResults = true;
return this.departmentsService!.getDepartments(this.sort.direction, this.sort.active)
}),
map(data => {
// Flip flag to show that loading has finished.
this.isLoadingResults = false;
this.resultsLength = data.total_count;
return data.items;
})).subscribe(data =>this.data = data);
A large amount just happened in a small amount of code, which is a great feature of Angular provided it does not contribute to confusion on the part of the developer. Let's break it down (and it's also a great idea for this or any other data flow to make use of the excellent stack trace tools we have now such as the the Visual Studio Code Debugger for Firefox or Chrome.
First, we have an (admittedly unnecessary) RxJs Merge (we're actually merging the matSortChange event with nothing, but can add events if necessary), then using the RxJS Observable.pipe function, as documented here.
Step by step, the pipe uses the literally-named RxJS StartWith() function to begin with an empty observable, then the RxJs switchMap function (there's a deep div into how switchMap works
here essentially switches around that empty observable with the output of departmentsService.getDepartments() when we start to get data from that service. The map function then removes a cosmetic loading flag, sets the resultsLength variable equal to data.length, and finally returns the array of items (a Department[] object). Because we subscribe to this observable, whenever the department data updates our table will update. If this sounds familiar, it's likely because it shares that advantage with jQuery AJAX (an observable is actually much more sophisticated and flexible, in that it does not require a direct call to update our table data).
The completed sample table's HTML is here. I've made very few changes from the above examples, and we finally get this (very simple) table:
At this point, anyone could be forgiven for asking if all of that was "worth it", particularly since we still don't have base elements from jQuery DataTables such as search and pagination. However, we're going to move on to showcase the power and flexibility of Angular to implement those features (with newfound flexibility) in the next example.
Posted on June 17, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 12, 2023