Building a reusable table layout for your Angular 2 project

One of the most exciting things about working with Angular 2 is how quickly you can build up your application with components. Building components isn’t very hard, but creating highly-reusable, general-purpose components requires some planning.

This article demonstrates how to build a versatile table layout component. Because such a component is a must have for any application that displays data, it’s a good choice as an example.

The completed component will dynamically generate html tables like the ones shown below.

In the finished example, the html selectors used to generate these tables, respectively, are:

<ct-table [records]="projects" 
[caption]="'NASA Projects'"
[settings]="projectSettings">
</ct-table>
<ct-table [records]="people"
[caption]="'NASA Astronauts'"
[settings]="personnelSettings">
</ct-table>

You’ll find that the simply attaching an *ngFor on the <tr> and <td> does not handle many use cases.

<tr *ngFor="let record of records">
<td *ngFor="let key of objectKeys">
{{ record[key] }}
</td>
</tr>

You need to be able to format table cells based on the value they contain and define column headings that aren’t left up to chance based on the properties of the data fetched by your data service.

Requirements

With this in mind, the following requirements have been defined for a general-purpose table component.

  • Control the name of the column headers and the ordering of columns based on settings supplied by a parent component.
  • Format cell values differently based on the type of value the cell contains. Design based on what an Excel user would expect. For example, numerical values are aligned right, whereas text values are aligned left.
  • Handle nested arrays and objects contained by the record. For example, in a record such as:
{
name: 'Tom',
interests: ['writing', 'programming', 'playing drums'],
emergency_contacts: [
{
name: 'Kathy',
mobile: '832-867-5309'
}...
]
}
  • Find the appropriate column value when supplied objects have different property keys used to access that value. E.g. My Cost column value could be stored as object.cost, object.total_cost, or object.funding.
  • Explicitly indicate when column value is undefined rather than display an empty cell.

Solution design

To meet these requirements, we will build the following elements:

  • TableLayoutComponent: Iterates through records and record properties to create the table. Required input is an array of records. Also accepts inputs for table caption and for settings used to configure the table format and style.
// usage
<ct-table [records]="projects"
[caption]="'NASA Projects'"
[settings]="columnSettings">
</ct-table>
  • FormatCellPipe: Transforms cell values based on the type of value returned by the object property. Handles arrays and objects nested in the record object.
  • StyleCellDirective: Applies styling to the table cell based on the type of value contained by the cell.
  • ColumnMap class: Constructs configuration settings for each column. Supplies the appropriate parameters to the TableLayoutComponent, FormatCellPipe, and StyleCellDirective for each column. Constructs class instances initialized with default values, if user settings are not supplied.

Completed template based on above requirements:

<tr *ngFor="let record of records">
<td *ngFor="let map of columnMaps" // map: instance of ColumnMap
[ctStyleCell]="record[map.access(record)]" //StyleCellDirective
>
{{ record[map.access(record)] | formatCell:map.format }}
// record access key identified by ColumnMap instance
// resulting value sent to FormatCellPipe for text format
</td>
</tr>

Set up the baseline application

Unfortunately, it can take a lot of work just to establish a baseline app with a common structure. If you want to follow this demonstration using the same code base, you can download this seed project from Github. The structure of this seed project is described in this article.

If you use the same setup as the seed project, the app directory will look like this:

We will build our solution in the shared folder. This folder contains the SharedModule that exports components we plan to reuse in various feature modules in our application. This aligns with the architecture recommendations in the Angular Docs on NgModule.

The SharedModule will be the core deliverable of this demonstration. However, the only way we will know if the SharedModule meets our requirements is if we test it with a feature module that imports and implements it.

Therefore, we need to create a feature module, called ProjectCenterModule, that will implement the TableLayoutComponent developed in the SharedModule.

Setup the feature module

The initial ProjectCenterModule has the following directory structure.

The following is the data we’ll initially display in our table.

// ./project-center/fakedata.ts
import { Project, Person } from './model';
export const PROJECTS: Project[] = [
{
id: 1,
name: 'Mercury',
cost: 277000000,
first_flight: 'September 9, 1959',
status: 'Complete'
},
{
id: 2,
name: 'Gemini',
cost: 1300000000,
first_flight: 'April 8, 1964',
status: 'Complete'
},
{
id: 3,
name: 'Apollo',
cost: 25400000000,
first_flight: 'February 26, 1966',
status: 'Complete'
},
{
id: 4,
name: 'Skylab',
launch: 'May 14, 1973',
status: 'Complete'
},
{
id: 5,
name: 'Apollo-Soyuz',
launch: 'July 15, 1975',
status: 'Complete'
},
{
id: 6,
name: 'Space Shuttle',
total_cost: 196000000000,
first_flight: 'August 12, 1977',
status: 'Complete'
}

];
export const PERSONNEL: Person[] = [
{
id: 151,
name: 'Alan B. Shepard, Jr.',
job: 'Astronaut',
year_joined: 1959,
missions: ['MR-3', 'Apollo 14']
},
{
id: 152,
name: 'Virgil I. Grissom',
job: 'Astronaut',
year_joined: 1959,
missions: ['MR-4', 'Apollo 1']
},
{
id: 153,
name: 'John H. Glenn, Jr.',
job: 'Astronaut',
year_joined: 1959,
missions: ['MA-6','STS-95']
},
{
id: 154,
name: 'M. Scott Carpenter',
job: 'Astronaut',
year_joined: 1959,
missions: ['MA-7']
}
];

And below are the model classes for that data.

// ./project-center/model.ts
export class Project {
id: number;
name: string;
cost?: number;
total_cost?: number;
first_flight?: string;
launch?: string;
status: string;
}
export class Person {
id: number;
name: string;
year_joined: number;
job: string;
missions: string[];
crewWith?: {
id: number,
name: string
}[];
manager?: any;
}

The module file includes one component and one service. It imports the SharedModule that handles all the hard work of actually building the tables for the various views in our application.

// ./project-center/project-center.module.ts
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { ProjectCenterComponent } from './project-center.component';
import { ProjectService } from './project.service';
@NgModule({
imports: [ SharedModule ],
declarations: [ ProjectCenterComponent ],
providers: [ ProjectService ],
exports: [ ProjectCenterComponent ]
})
export class ProjectCenterModule { }

The service imports our mock data with get methods that simply return the imported data. In a real data service, methods would use Promise or Observable interfaces for asynchronous request/response handling.

// ./project-center/project.service.ts
import { Injectable } from '@angular/core';
import { Project, Person } from './model';
import { PROJECTS, PERSONNEL } from './fakedata';
@Injectable()
export class ProjectService {
getProjects(): Project[] {
// actual implementation would use async method
return PROJECTS;
}
getPersonnel(): Person[] {
return PERSONNEL;
}
}

The ProjectComponentComponent below injects the service through the class’ constructor. The console.logs are included simply to verify the initial setup is working. We’ll perform this check shortly.

// ./project-center/project-center.component.ts
import { Component, OnInit } from '@angular/core';
import { ProjectService } from './project.service';
import { Project, Person } from './model';
@Component({
selector: 'ct-project-center',
templateUrl: 'app/project-center/project-center.component.html'
})
export class ProjectCenterComponent implements OnInit {
title: string = 'Project Center';
projects: Project[];
people: Person[];
constructor(private projectService: ProjectService){}
ngOnInit() {
this.projects = this.projectService.getProjects();
this.people = this.projectService.getPersonnel();
console.log(this.projects);
console.log(this.people);
}
}

The initial template is minimalist. There’s not even a table yet. Note that Bootstrap is used for styling.

// ./project-center/project-center.component.html
<div class="container-fluid">
<div class="page-header">
<h1>{{ title }}</h1>
</div>
</div>

Setup SharedModule

The SharedModule contains the table component we’ll be working on.

// ./shared/shared.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TableLayoutComponent } from './table-layout.component';
@NgModule({
imports: [ CommonModule ],
declarations: [ TableLayoutComponent ],
exports: [
CommonModule,
TableLayoutComponent
]
})
export class SharedModule { }

The component itself does not do anything yet.

// ./shared/table-layout.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'ct-table',
templateUrl: 'app/shared/table-layout.component.html'
})
export class TableLayoutComponent { }

And the initial template is just a static table.

// ./shared/table-layout.component.html
<table class="table">
<caption>Table</caption>
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Some value</td>
<td>Some value</td>
</tr>
</tbody>
</table>

If you like to see how the root AppModule and AppComponent are setup, please read this article describing the seed project setup.

Test out the initial version of ProjectCenterModule and SharedModule by running the app with them loaded into your AppModule and the 
<ct-project-center> selector inserted into your AppComponent template.

The page will be empty except for the Project Center title. Check the console in developer tools. Verify that you see the arrays stored in the projects and people variables.

Now add the <ct-table> selector to the ProjectCenterComponent template.

// ./project-center/project-center.component.html
<div class="container-fluid">
<div class="page-header">
<h1>{{ title }}</h1>
</div>
<ct-table></ct-table>
</div>

Refresh the app. Now you should see the static table as well.

After verifying the setup, we are ready to start building our solution.


Develop the table layout

First draft of TableLayoutComponent

We are ready to build our first iteration of TableLayoutComponent that dynamically generates the table based on supplied data.

We do the following:

  1. Define Inputs each table instance will receive.
  2. Capture object keys from data to determine table columns. Make sure keys are updated whenever Input data changes.
  3. Iterate through objects and object keys in the component template.
// ./shared/table-layout.component.ts
import { Component, Input, OnChanges } from '@angular/core';
@Component({
selector: 'ct-table',
templateUrl: 'app/shared/table-layout.component.html'
})
export class TableLayoutComponent implements OnChanges {
@Input() records: any[];
@Input() caption: string;
keys: string[];
    ngOnChanges() {
this.keys = Object.keys(this.records[0]);
}

}

In the controller class of TableLayoutComponent, we implement OnChanges. We use the associated method ngOnChanges() to grab the keys from the first record each time a Angular detects a change in Input() from the parent component.

Of course, retrieving the keys this way means that we are relying on a uniform array of objects with no uninitialized optional properties.

The template can now iterate through the input records and the retrieved keys to generate the table.

// ./shared/table-layout.component.ts
<table class="table">
<caption *ngIf="caption">{{ caption }}</caption>
<thead>
<tr>
<th *ngFor="let key of keys">{{ key }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let record of records">
<td *ngFor="let key of keys">{{ record[key] }}</td>
</tr>

</tbody>
</table>

We attach an *ngIf to the <caption>. We want this input to be optional. If no caption is defined, the caption will not be attached to the table.

For the column header, we iterate through the keys.

For the table body, we iterate through the records and then nest another *ngFor on the <td> to iterate through the keys.

Before we can test our table. We have to use it in the ProjectCenterComponent template inside our feature module.

<div class="container-fluid">
<div class="page-header">
<h1>{{ title }}</h1>
</div>
<ct-table [records]="projects"
[caption]="'NASA Projects'">
</ct-table>
<ct-table [records]="people"
[caption]="'NASA Astronauts'">
</ct-table>

</div>

Run the application. It’s a good start, but it has the limitations we’d expect. Our headers are ugly and the large numbers in the cost column are hard to read without currency formatting.

We have dates in our data for Skylab and Apollo-Soyuz but they are accessed through the property key “launch” rather than “first_flight”. The records are setup this way because there was only one launch for each rather than several such as in the Apollo program.

In the astronauts list, we see that Angular template expressions actually handle the arrays nested in the “missions” property rather well. However, we’d still like to see a space between each array element.

A model for configuration settings

The TableLayoutComponent will only be able to fine tune how the table is displayed if the parent component provides information to it about how it want the data to be displayed.

We need to create a model for these configuration settings. Create the layout.model.ts file in the shared folder.

// ./shared/layout.model.ts
export class ColumnSetting {
primaryKey: string;
header?: string;
format?: string;
alternativeKeys?: string[];
}

We import this class into the component that is implementing the table layout. The primaryKey is the property on the object that we expect to contain the value for a particular table column, and header is the title we want to apply to that column. The other properties on the model will be discussed and applied later on.

We define settings for our projects table, but not our astronauts table. We want the settings to be optional. If settings are not supplied, TableLayoutComponent should attempt to generate a clean view of the table by applying default settings.

// ./project-center/project-center.component.ts
...
import { ColumnSetting } from '../shared/layout.model';
@Component({...})
export class ProjectCenterComponent implements OnInit {
...
projectSettings: ColumnSetting[] =
[
{
primaryKey: 'name',
header: 'Name'
},
{
primaryKey: 'first_flight',
header: 'First launch'
},
{
primaryKey: 'cost',
header: 'Cost'
}
];

constructor(private projectService: ProjectService){}
ngOnInit() {...}
}

The settings below will generate a three column table. Columns are generated only if a settings object with the primaryKey property is defined. They define the appropriate header format for each column and the columns are generated in the same order as the array. In this way, we control which values are displayed from each record and the order in which they are displayed.

In the ProjectCenterComponent template, we bind the projectSettings to the settings input property that we will soon define in our TableLayoutComponent.

// ./project-center/project-center.component.html
...
<ct-table [records]="projects"
[caption]="'NASA Projects'"
[settings]="projectSettings">
</ct-table>
...

Back in the shared folder, we modify TableLayoutComponent. We import the ColumnSetting class and apply it in the ngOnChanges() method. We call the property containing an array of ColumnSetting instances columnMaps. We think of each ColumnSetting instance as having a map that tells us where to get all the information we need for a particular column, hence the naming convention.

// ./shared/table-layout.component.ts
import { Component, Input, OnChanges } from '@angular/core';
import { ColumnSetting } from './layout.model';
@Component({...})
export class TableLayoutComponent implements OnChanges {
@Input() records: any[];
@Input() caption: string;
@Input() settings: ColumnSetting[];
columnMaps: ColumnSetting[];
ngOnChanges() {
if (this.settings) { // when settings provided
this.columnMaps = this.settings;
} else {
// no settings, create column maps with defaults
this.columnMaps = Object.keys(this.records[0])
.map( key => {
return {
primaryKey: key,
header: key.slice(0, 1).toUpperCase() +
key.replace(/_/g, ' ' ).slice(1)
}
});
}

}
}

Right now setting objects are equivalent to the column maps. Therefore, if settings are provided, we can simply initialize the columnMaps component property to the settings input.

If the component isn’t provided with settings, we do our best by once again grabbing the keys from the first record. We map over these keys, returning objects with the same structure as ColumnSetting. We clean up the column headers by capitalizing the first letter and replacing underscores with spaces.

We update the template to make use of the columnMaps component property.

<!-- ./shared/table-layout.component.html -->
<table class="table">
<caption *ngIf="caption">{{ caption }}</caption>
<thead>
<tr>
<th *ngFor="let map of columnMaps">{{ map.header }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let record of records">
<td *ngFor="let map of columnMaps">
{{ record[map.primaryKey] }}</td>
</tr>
</tbody>
</table>

When we run the app again, we see this:

This is notable progress. In the projects table, we were able to control which columns are displayed and the column ordering. The astronauts table looks pretty good, despite no supplied settings.

The formatting of the cost column is still a glaring issue. Also, when no property values are found, we would like the cell to display “Not available”. This instills confidence that the app actually attempted to retrieve the value and did not encounter an error. We will tackle these requirements next by developing a pipe.

Develop a custom pipe

Our planned solution calls for a custom pipe call FormatCellPipe. Our short specification described it as follows.

FormatCellPipe: Transforms cell values based on the type of value returned by the object property. Handles arrays and objects nested in the record object.

To get a quick win that demonstrates we are able to successfully define a custom pipe and use it in our application, our first iteration will just check for undefined values and return “not available” if a record property is undefined.

// ./shared/format-cell.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'formatCell' })
export class FormatCellPipe implements PipeTransform {
transform(value: any) {
if ( value === undefined ) {
return 'not available';
}
return value;
}
}

Before using it, we have to declare the pipe in our SharedModule.

// ./shared/shared.module.ts
...
import { FormatCellPipe } from './format-cell.pipe';
@NgModule({
imports: [ CommonModule ],
declarations: [
TableLayoutComponent,
FormatCellPipe
],
...

We apply it to our table template.

<!-- ./shared/table-layout.component.html -->
...
<tr *ngFor="let record of records">
<td *ngFor="let map of columnMaps">
{{ record[map.primaryKey] | formatCell }}
</td>
</tr>
...

When we run the app, we should now see the following results:

There are no blank cells, instead when no value is found the pipe returns “not available” to the table cell.

In the next iteration of the pipe, we handle the currency values, nested arrays, and nested objects. This version of the pipe takes an argument format because we will use our ColumnSettings objects to tell the pipe the appropriate format to use.

// ./project-center/project-center.component.ts
...
export class ProjectCenterComponent implements OnInit {
...
projectSettings: ColumnSetting[] =
[
...
{
primaryKey: 'cost',
header: 'Cost',
format: 'currency'
}
];
...

If no settings are supplied, the TableLayoutComponent will initialize ColumnSetting instances with the format property set to “default”.

// ./shared/table-layout.component.ts
...
export class TableLayoutComponent implements OnChanges {
...
ngOnChanges() {
if (this.settings) {
this.columnMaps = this.settings; // TODO
// if no format provided - initialize to 'default'
} else {
this.columnMaps = Object.keys(this.records[0])
.map( key => {
return {
primaryKey: key,
header: key.slice(0, 1).toUpperCase() +
key.replace(/_/g, ' ' ).slice(1),
format: 'default'
}
});
}
}
}

We also need to handle the case where settings are supplied but those supplied settings do not specify the format. We will leave that as a to do list item for now.

We update the template to provide the format to the pipe via our column map.

<!-- ./shared/table-layout.component.html -->
<tr *ngFor="let record of records">
<td *ngFor="let map of columnMaps">
{{ record[map.primaryKey] | formatCell:map.format }}
</td>
</tr>

We rewrite the FormatCellPipe to take the format argument. We leverage the Angular built-in CurrencyPipe for our currency values. Before use, we inject it into our pipe using dependency injection.

If the value sent to the pipe is a nested array, we join the array with a comma and a space. If the value sent to the pipe is an object, we look for a name property and return it.

import { Pipe, PipeTransform } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
@Pipe({ name: 'formatCell' })
export class FormatCellPipe implements PipeTransform {
constructor (
private currencyPipe: CurrencyPipe
) { }
transform(value: any, format: string) {
if ( value === undefined ) {
return 'not available';
}
        if ( format === 'default' ) {
if ( Array.isArray(value) ) {
if ( typeof value[0] !== 'object' ) {
return value.join(', ');
} else {
return value.map( obj => {
return obj.name
}).join(', ');
}
}
if ( typeof value === "object") {
return value.name
}
}

if (format === 'currency') {
return this.currencyPipe.transform(value, 'USD', true);
}

return value;
}
}

Before we can run the application, we must set CurrencyPipe as a provider in the SharedModule.

// ./shared/shared.module.ts
...
import { CurrencyPipe } from '@angular/common';
...
@NgModule({
...
providers: [ CurrencyPipe ]
})
export class SharedModule { }

The pipe will later need to be expanded to handle more cases, but it’s a good starter template that meets the needs of this demonstration. Running our app, we now see:

The next step is to handle cell styling.

Develop an attribute directive for cell styling

Our specification is as follows.

StyleCellDirective: Applies styling to the table cell based on the type of value contained by the cell.

And the corresponding requirement is:

Format cell values differently based on the type of value the cell contains. Design based on what an Excel user would expect. For example, numerical values are aligned right, whereas text values are aligned left.

We also would like to make “not available” values stand out less than the rest of our cell values. We’ll have the attribute set the font color of these values to a light gray.

The directive needs to take the cell value as an argument so it can determine the appropriate styling to apply based on the value type.

Create the style-cell.directive.ts in the shared folder.

// ./shared/style-cell.directive.ts
import { Directive, ElementRef,
Input, Renderer, OnInit } from '@angular/core';
@Directive({ selector: '[ctStyleCell]'})
export class StyleCellDirective implements OnInit {
@Input() ctStyleCell: string;
constructor(
private el: ElementRef,
private renderer: Renderer) { }
ngOnInit() {
if (this.ctStyleCell === undefined) {
this.renderer.setElementStyle(
this.el.nativeElement,
'color',
'#dcdcdc');
this.renderer.setElementStyle(
this.el.nativeElement,
'text-align',
'center');
}
if (typeof this.ctStyleCell === 'number') {
this.renderer.setElementStyle(
this.el.nativeElement,
'text-align',
'right');
}
}
}

We must import it into our SharedModule before we can use it.

// ./shared/shared.module.ts
...
import { StyleCellDirective } from './style-cell.directive';
@NgModule({
imports: [ CommonModule ],
declarations: [
...
StyleCellDirective
],
...

Finally, we apply it to our table cell. We bind record[map.primaryKey] to it in order to provide it with the cell value for type checking.

<!-- ./shared/table-layout.component.html -->
...
<tr *ngFor="let record of records">
<td *ngFor="let map of columnMaps"
[ctStyleCell]="record[map.primaryKey]">
{{ record[map.primaryKey] | formatCell:map.format }}
</td>
</tr>
...

The table looks pretty good now when we run the app.

Making the solution extensible— the ColumnMap class

The final piece is to create a class that can handle all the complicated initialization logic of our column settings, applying default values as needed.

It also will handle our most challenging requirement:

Find the appropriate column value when supplied objects have different property keys used to access that value. E.g. My Cost column value could be stored as object.cost, object.total_cost, or object.funding.

As part of our planning, we wrote the following specification.

ColumnMap class: Constructs configuration settings for each column. Supplies the appropriate parameters to the TableLayoutComponent, FormatCellPipe, and StyleCellDirective for each column. Constructs class instances initialized with default values, if user settings are not supplied.

We write this class in the layout.model.ts file in the shared folder underneath the ColumnSetting class.

// ./shared/layout.model.ts
...
export class ColumnMap {
primaryKey: string;
private _header: string;
private _format: string;
alternativeKeys?: string[];
constructor ( settings ) {
this.primaryKey = settings.primaryKey;
this.header = settings.header;
this.format = settings.format;
this.alternativeKeys = settings.alternativeKeys;
}
set header(setting: string) {
this._header = setting ?
setting :
this.primaryKey.slice(0, 1).toUpperCase() +
this.primaryKey.replace(/_/g, ' ' ).slice(1)
}
get header() {
return this._header;
}
set format(setting: string) {
this._format = setting ? setting : 'default';
}
get format() {
return this._format;
}
access = function ( object: any ) {
if (object[this.primaryKey] || !this.alternativeKeys) {
return this.primaryKey;
}
for (let key of this.alternativeKeys) {
if (object[key]) {
return key;
}
}
return this.primaryKey;
}
}

This code applies three features of TypeScript classes.

  1. Define getter and setter methods that access private variables. This allows for initialization logic to define default setting values when corresponding settings are not supplied.
  2. Use the constructor initializes class instance properties, calling setter methods for the private variables.
  3. Define a method, called access, for each class instance that can be used to look for the column value on several object properties if alternativeKeys are provided in the settings. This handles the case, for example — 
    My Cost column value could be stored as object.cost, object.total_cost, or object.funding.

We update the TableLayoutComponent to take advantage of this class. Notice that it simplifies the code because we can remove the logic previously used to initialize default values from the component.

// ./shared/table-layout.component.ts
import { Component, Input, OnChanges } from '@angular/core';
import { ColumnSetting, ColumnMap } from './layout.model';
@Component({
selector: 'ct-table',
templateUrl: 'app/shared/table-layout.component.html'
})
export class TableLayoutComponent implements OnChanges {
@Input() records: any[];
@Input() caption: string;
@Input() settings: ColumnSetting[];
columnMaps: ColumnMap[];
ngOnChanges() {
if (this.settings) {
this.columnMaps = this.settings
.map( col => new ColumnMap(col) );

} else {
this.columnMaps = Object.keys(this.records[0])
.map( key => {
return new ColumnMap( { primaryKey: key });
});
}
}
}

Update the template to use the access method on the ColumnMap instances, rather than primaryKey.

...
<tr *ngFor="let record of records">
<td *ngFor="let map of columnMaps"
[ctStyleCell]="record[map.access(record)]">
{{ record[map.access(record)] | formatCell:map.format }}
</td>
</tr>
...

To take advantage of the additional functionality we’ve extended our table layout with, we’ll apply some additional settings to our columns in the ProjectCenterComponent.

// ./project-center.component.ts
...
projectSettings: ColumnSetting[] =
[
...
{
primaryKey: 'first_launch',
header: 'First Launch',
alternativeKeys: ['launch', 'first_flight']
},
{
primaryKey: 'cost',
header: 'Cost',
format: 'currency',
alternativeKeys: ['total_cost']
}
];
...

We know that a first_launch property does not exist on our sample data set, but we set this as the primaryKey so we can see if our ColumnMap class will successfully check multiple alternativeKeys.

For the Space Shuttle record, the cost value we want is stored as total_cost, so we specify this in the alternativeKeys.

Running the app, we see:

We now see the Space Shuttle cost, and our launch dates still display as expected.

One more test with more complicated data

Finally, let’s see how well our TableLayoutComponent can handle more complex data.

Update /project-center/fakedata.ts as follows.

...
export const PERSONNEL: Person[] = [
{
id: 151,
name: 'Alan B. Shepard, Jr.',
job: 'Astronaut',
year_joined: 1959,
missions: ['MR-3', 'Apollo 14'],
crewWith: [
{
id: 175,
name: 'Stuart Roosa'
},
{
id: 176,
name: 'Edgar Mitchell'
}
]

},
...
{
id: 161,
name: 'James A. Lovell',
job: 'Astronaut',
year_joined: 1962,
missions: ['Gemini 7', 'Gemini 12',
'Apollo 8', 'Apollo 13'],
manager: {
id: 157,
name: 'Deke Slayton'
}
}

];

We update the record of Alan Shepard to include his crew mates on the Apollo 14 mission as an array of objects. We add a new record for James Lovell who was on four missions. We identify his manager, Chief Astronaut Deke Slayton, in a nested object. Remember that we setup our pipe to return the name property of nested objects. However, we never tested if it worked.

Add the following settings in ProjectCenterComponent.

// ./project-center/project-center.component.ts
...
personnelSettings: ColumnSetting[] =
[
{ primaryKey: 'name' },
{ primaryKey: 'year_joined', header: 'Joined' },
{ primaryKey: 'missions' },
{ primaryKey: 'manager' },
{ primaryKey: 'crewWith', header: 'Crew mates'}
];
...

Bind the settings in the template.

<!-- ./project-center/project-center.component.html -->
...
<ct-table [records]="projects"
[caption]="'NASA Projects'"
[settings]="projectSettings">
</ct-table>
<ct-table [records]="people"
[caption]="'NASA Astronauts'"
[settings]="personnelSettings">
</ct-table>

The astronauts table now looks like:

We now have a good general-purpose component for tables in our Angular 2 project. There’s a lot that can be done to extend it, and we have a solid architecture as a basis. We’ll leave the demonstration at this for now. I will continue to post articles that follow up on this example.

Thank you for following.