Creating a Responsive Dashboard in Angular 5 From Scratch

The motivation behind me writing this is that I have yet not found a robust open-source tutorial of a dashboard module in Angular that allows the developer to customize it in whatever way they see fit. However, after a couple hours of effort, I have come up with a fairly modular solution to this problem that is easy to implement and allows the developer to extend it however they see fit.


Requirements

I will be assuming that the reader of this article has some basic understanding of Angular 2 (v5) and how it works, especially what Modules, Components, Directives, and Services are. You will also need to have some understanding of dynamically creating Angular Components, as well as the Observer Design pattern and the RxJS library. We will be using

  1. Angular 5.1.2 and Angular CLI
  2. Angular Material 5.0.2
  3. Angular Flex Layout 2.0.0-beta.12

Getting Started

Using the Angular CLI, set up a new project. Inside the root directory of the project, run npm install --save @angular/material @angular/animations @angular/flex-layout @angular/cdk. You will also probably want to setup your routing, which I won’t be discussing much. Last but not least, lets create a new module and call it ‘dashboard’ by running ng generate module dashboard.

At first I will be going through the steps to create the dashboard dynamically, at run-time, which allows you to add cool features like storing dashboard preferences for users in the database, and then at the end we will use Angular Flex Layout and Angular Material’s ‘grid list’ feature to make it fully responsive.


Let’s get to it!

Our goal is to create components that allow us to dynamically add and remove ‘cards’ using simple function calls, without having to spaghetti up a bunch of preferences at compile time. Note that by card I am not referring to Angular Material’s card layout but rather the boxes that we will be putting in our dashboard. I’ll begin by first creating all the basic blocks of the dashboard, and then add bits to each of them here and there to reach the final product.

Create a new component called DashboardComponent, which will be the core of holding it all together. You can stylize this component’s template however you wish, with only one requirement; we need some sort of container like a div to be the placeholder of all our cards.

Next we’ll create a simple class called DashboardCard that’s in charge of holding all the properties you’d want the card to have access to once it’s dynamically created, as well as a reference to the component that will be instantiated for the card once we decide to do so. We will fill out the details of this class later as it requires to complete the other parts of the dashboard first; for now the following will suffice.

export class DashboardCard {
constructor() {}
}

We will also want to create a service called DashboardCardsService which will responsible for holding an array of DashboardCard. The reason why you want to keep your collection of cards in a service rather than the DashboardComponent is that you get the flexibility to refer to these cards in multiple parts of your web app, as well as create some database in your back-end service that allows you to store and fetch the cards per user if you wish to do so.

import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {DashboardCard} from '../../dashboard-card';

@Injectable()
export class DashboardCardsService {

constructor() {
}

private _cards: BehaviorSubject<DashboardCard[]> = new BehaviorSubject<DashboardCard[]>([]);

addCard(card: DashboardCard): void {
this._cards.next(this._cards.getValue().concat(card));
}

get cards(): BehaviorSubject<DashboardCard[]> {
return this._cards;
}
}

We set _cards to have type Behaviorsubject<DashboardCard[]> so that our components can subscribe to it and essentially observe any changes in this array so our DOM can change accordingly. We’re also adding a get function for this observable, as well as an addCard function. Feel free to extend this with a removeCard function if you wish! If you’re not too familiar with the RxJS library or the Observable Design Pattern, I suggest reading a few online resources on these before continuing!

We also need to create a component called DashboardCardSpawnerComponent. You can consider this as the factory component whose sole purpose is to spawn the actual components for your cards. Before we dive into the code for this component, let’s talk a little about what we want it to accomplish. We want to be able to separate the spawning operation of a card from the actual content of the card, so that we can reuse the ‘card spawner’ for whatever type of component we wish to spawn as a card on our dashboard. This takes us into the topic of creating dynamic components in Angular. Since the compiler doesn’t really know the details of what component you wish to create dynamically and what services it will be dependent on, from Angular 5.0 onward, it’s the responsibility of the developer to declare every component dependency in advance for whichever component he or she wishes to create dynamically. So let’s take a look at the code of the DashboardCardSpawnerComponent and see what’s going on.

import {Component, ComponentFactoryResolver, Injector, Input, ViewChild, ViewContainerRef} from '@angular/core';
import {DashboardCard} from '../dashboard-card';

@Component({
selector: 'app-dashboard-card-spawner',
template: `
<div #spawn></div>`,
styles: [':host { height: 100%; width: 100%;}']
})
export class DashboardCardSpawnerComponent implements OnInit {
@ViewChild('spawn', {read: ViewContainerRef}) container;

constructor(private resolver: ComponentFactoryResolver) {
}

@Input() set card(data: DashboardCard) {
if (!data) return;
let inputProviders = Object.keys(data.inputs).map((inputName) =>
{
return {provide: data.inputs[inputName].key,
useValue: data.inputs[inputName].value,
deps: []};
});
let injector = Injector.create(inputProviders,this.container.parentInjector);
let factory = this.resolver.resolveComponentFactory(data.component);
let component = factory.create(injector);
this.container.insert(component.hostView);
}
}

In the template, we create a template reference on the placeholder div that our spawn component will essentially use to place the dynamically created component in. Let’s name it #spawn. Also, to be able to access that reference from our component code, we add a container field decorated as a ViewChild and pass the reference name to it. Let’s also inject the ComponentFactoryResolver service into this component, used for the actual component creation. We also create an @Input() set function for the cards, since this component will be receiving the cards through property binding on the template HTML for the DashboardComponent.

Once we receive data as input for this field, we create an array to serve the purpose of providers or things that we need to inject into this component once it is created. This is tightly correlated to the fields we add in the DashboardCard class. Angular expects objects in this array to be of a specific form, with provide, useValue(or useClass), and deps, an array of dependencies that each of those providers need. In our scenario, since we’re only really trying to inject primitive values of type number and string into the components that we spawn, they don’t have dependencies, but if your card components are more complex, you’d have to declare the services they depend on in the deps array, and perhaps use the useClass field rather than useValue. The data.inputs field and the key/value properties on these fields will make more sense once we look at the code of DashboardClass.

Next, we create a StaticInjector using the inputProvider, and a component factory from the ComponentFactoryResolver, passing a reference to the component we want to instantiate. Remember that this function is taking in a parameter of type DashboardCard, which holds a reference to the component we want to instantiate. We then simply call create on the factory using our injector and insert it into the container we created earlier.

Note that I’ve also added host styling on this field so that the component’s HTML element expands to full width and height of its container.

We can now complete the DashboardCard class.

import {InjectionToken} from '@angular/core';

export class DashboardCard {
  static metadata: any = {
NAME: new InjectionToken<string>('name'),
ROUTERLINK: new InjectionToken<string>('routerLink'),
COLOR: new InjectionToken<string>('color')
};
  constructor(private _input: {
name: {
key: InjectionToken<string>,
value: string
},
routerLink: {
key: InjectionToken<string>,
value: string
},
color: {
key: InjectionToken<string>,
value: string,
}
},
private _component: any) {
}


get inputs(): any {
return this._input;
}


get component(): any {
return this._component;
}
}

In this class, you can essentially add whatever properties you’d like your cards to have. In my scenario, I needed the cards themselves to have a name, routerLink, and color. The card class will also hold a reference to the Angular Component that represents it in the_component field as mentioned earlier, as well as all the other properties you see fit to pass to the component once we tell Angular to inject it for us in the DOM. You can now see why we needed to use the map function to iterate through the _inputs keys so that we inject each of these properties separately into the component.

Let’s talk a bit about why we split each property into a key/value pair and why the keys are all of type InjectionToken<T>. Before Angular 5, we could simply rely on the concept of reflection to take care of figuring out what fields we wanted to inject into our dynamically created components, and what dependencies the component would have. However, that has changed and since we are trying to inject primitive types such as string in this scenario, we have to tell Angular that these are not just normal strings, but rather injectable strings into components. This is why in the provide field of our inputProvider we are using these keys to tell Angular what the type of what we are trying to inject into the component is. We also create a static metadata object where we instantiate an InjectionToken for each of these properties, and use them later as keys once we start adding instances of DashboardCard to DashboardCardsService in DashboardComponent.

We can now put it all together in the DashboardComponent and create the dashboard!

import {Component, OnInit} from '@angular/core';
import {DashboardCardsService} from `../services/dashboard-cards/dashboard-cards.service';
import {DashboardCard} from ``../dashboard-card';
import {DashboardUsersComponent} from `../components/dashboard-users/dashboard-users.component';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
stylesUrls: ['./dashboard.component.scss'],
entryComponents: [DashboardUsersComponent]
})
export class DashboardComponent implements OnInit {
cards: DashboardCard[] = [];
constructor(private cardsService: DashboardCardsService) {
this.cardsService.cards.subscribe(cards => {
this.cards = cards;
});
}
ngOnInit() {
this.createCards();
}
createCards() : void {
this.cardsService.addCard(
new DashboardCard(
{name: {key: DashboardCard.metadata.NAME, value: 'users'},
routerLink: {key: DashboardCard.metadata.ROUTERLINK,
value: '/dashboard/users'},
iconClass: {key: DashboardCard.metadata.ICONCLASS,
value: 'fa-users'},
color: {key: DashboardCard.metadata.COLOR, value: 'blue'}
}, DashboardUsersComponent)
);
}

Here I’ve also used a component I created for the dashboard called DashboardUsersComponent through which I will display all users to the user of the dashboard. I’ve included that here as an example to see how you’d instantiate your cards, and perhaps customize it even further.

In the constructor, we are simply registering as an observer of the cards of DashboardCardsService, and then create and add new cards in ngOnInit. You can customize this to create them whenever you wish. It’s important to note that you must pass references to all possible components that you wish to dynamically create in the component’s entryComponent[] array, or else the compiler will not be able to create them for you at run-time!

Let’s take a look at a sample HTML template for DashboardComponent.

<mat-sidenav-container>
<mat-sidenav>
<app-dashboard-sidenav></app-dashboard-sidenav>
</mat-sidenav>
<mat-sidenav-content>
<app-dashboard-card-spawner *ngFor="let card of cards | async"
[card]="card">
</app-dashboard-card-spawner>
</mat-sidenav-content>
</mat-sidenav-container>

Here I’ve created a simple layout using the sidenav layout from Angular Material, creating an <app-dashboard-card-spawner> for each card using *ngFor. Further, we’re passing down each card to the component (remember the @Input() set card(data: DashboardCard) function in DashboardCardSpawnerComponent?)

What if I Have Complex Dependencies for my Card Components?

No problem! You can simply extend DashboardCard by doing the following per say.

export class DashboardCard {
constructor(private _inputs: {...},
private _component: any,
private _services:
{provide: any, useClass: any, deps: any[]}[]
) {}
get services(): any {
return this._services;
}

Here you’d pass a reference to the service class for both provide, and useClass, and include references to all dependencies of that service as well. You’d also have to change the code for DashboardCardSpawnerComponent to

let inputProviders = Object.keys(data.inputs).map((inputName) => {
return {
provide: data.inputs[inputName].key,
useValue: data.inputs[inputName].value,
deps: []};
});
inputProviders = inputProviders.concat(data.services);

However the one pitfall of this is that you have to make sure you’re passing in the right references since we’re typing all three fields of _services with type any!


How do I Make This Responsive?

So by now we’ve achieved the goal of creating dashboard cards on the fly. However, the one major problem with this dashboard is that it’s not quite mobile friendly. First let’s take a look at how we would put all our cards in a grid, and then we’ll make the grid responsive.

Take a look at the HTML code for DashboardComponent

<mat-sidenav-container style="min-height: 100%" fxFlex>
<mat-sidenav [mode]="'side'" [opened]="true"
fxLayoutAlign="center" fxLayout="column">
<app-dashboard-sidenav></app-dashboard-sidenav>
</mat-sidenav>
<mat-sidenav-content>
<mat-grid-list [cols]="cols">
<mat-grid-tile *ngFor="let card of cards"
[colspan]="card.inputs.cols.value"
[rowspan]="card.inputs.rows.value">
<app-dashboard-card-spawner [card]="card">
</app-dashboard-card-spawner>
</mat-grid-tile>
</mat-grid-list>
</mat-sidenav-content>
</mat-sidenav-container>

Here we’ve wrapped a <mat-grid-list> around our spawn component. Take a look at the documentation for the grid list component if you already haven’t. You can also see that I’ve added cols as a field on my DashboardComponent, as well as cols and rows on the _inputs field of DashboardCard.

import {InjectionToken} from '@angular/core';
import {Observable} from 'rxjs/Observable';

export class DashboardCard {
static metadata: any = {
...
COLS: new InjectionToken<number>('cols'),
ROWS: new InjectionToken<number>('rows'),
};

constructor(private _input: {
...
cols: {
key: InjectionToken<number>,
value: number
},
rows: {
key: InjectionToken<number>,
value: number
}
},
private _component: any) {
}

We then instantiate DashboardCard with passing COLS as key for cols, with any desired value.

Cool, now our cards fall into a grid layout, let’s make it responsive. If we resize the browser as is, our columns resize as well, however you’ll end up having a static number of grid columns and card column span for every resolution. That’s not quite what we want! We want to be able to dynamically change the number of columns and rows per card, as well as the total number of columns in the grid, based on the screen width breakpoints that Flex-Layout provides.

Previously, the field cols in DashboardComponent was a simple number. Let’s change that to Observable<number> so that as it changes in code, our HTML updates it dynamically. You’ll also want to use the async pipe on cols for that to happen.

<mat-grid-list [cols]="cols | async">

Next, we need to create some sort of mapping, such that each breakpoint has a corresponding number of columns. This goes in the ngOnInit() function of DashboardComponent.

const col_map = new Map([
['xs', 1],
['sm', 4],
['md', 8],
['lg', 10],
['xl', 18]
]);

And to change the value of cols based on which breakpoint was activated, we first need to inject ObservableMedia into the DashboardComponent, and then add the following code to use the map we just created.

import ...
import
{Observable} from 'rxjs/Observable';
import {ObservableMedia} from '@angular/flex-layout';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/startWith';
constructor(private cardsService: DashboardCardsService,
private observableMedia: ObservableMedia) {...}
ngOnInit() {
...
let start_col: number;
col_map.forEach((cols, mqAlias) => {
if (this.observableMedia.isActive(mqAlias)) {
start_col = cols;
}
}
this.cols = this.observableMedia.asObservable()
.map(change => {
return col_map.get(change.mqAlias);
}).startWith(start_col);
}

Now we’re assigning a new Observable<number> to cols, whose value changes based on whichever breakpoint becomes active in observableMedia.

However, this is not enough. In fact, it won’t work too well. Not only we must change the total number of columns in our grid, but also we must change the column and row span of each card as well. This is where you need to sit back and analyze your design for a bit. In my scenario, I decided that I wanted two types of column and row widths, big and small, allowing me to create 4 distinct types of cards. You can surely extend this further but to illustrate the point, we’ll keep it simple for now.

Let’s go back to DashboardCard and change it up a bit further

import {InjectionToken} from '@angular/core';
import {Observable} from 'rxjs/Observable';

export class DashboardCard {
static metadata: any = {
...
ROWS: new InjectionToken<Observable<number>>('rows'),
COLS: new InjectionToken<Observable<number>>('cols')
};

constructor(private _input: {
...
cols: {
key: InjectionToken<Observable<number>>,
value: Observable<number>
},
rows: {
key: InjectionToken<Observable<number>>,
value: Observable<number>
}
},
private _component: any) {
}

Instead of passing in number for the col and row value of each card, we’re now making those two fields Observable<number> as well, so that we can change them using the same observableMedia in DashboardComponent, as well as have the HTML updated using async pipe. We will then have to come up with a similar mapping for the different types of column and row span values that we wish to have for each of the breakpoints. In my example I’ve set many of those mappings to be the same value but feel free to do otherwise!

...
cols_big: Observable<number>;
cols_sml: Observable<number>;
...
ngOnInit() {
...
const
cols_map_big = new Map([
['xs', 1],
['sm', 4],
['md', 4],
['lg', 4],
['xl', 4]]);
  const cols_map_sml = new Map([
['xs', 1],
['sm', 2],
['md', 2],
['lg', 2],
['xl', 2]]);
  let start_cols_big: number;
let start_cols_sml: number;
cols_map_big.forEach((cols, mqAlias) => {
if (this.observableMedia.isActive(mqAlias))
start_cols_big = cols;
});
cols_map_sml.forEach((cols, mqAlias) => {
if (this.observableMedia.isActive(mqAlias))
start_cols_sml = cols;
});
this.cols_big = this.observableMedia.asObservable()
.map(change => {
return cols_map_big.get(change.mqAlias);
}).startWith(start_cols_big);
this.cols_sml = this.observableMedia.asObservable()
.map(change => {
return cols_map_sml.get(change.mqAlias);
}).startWith(start_cols_sml);

And the HTML would simply change to include the async pipe on [colspan] and [rowspan].

<mat-grid-tile *ngFor="let card of cards"
[colspan]="card.inputs.cols.value | async"
[rowspan]="card.inputs.rows.value | async">
Responsive dashboard skeleton preview

Conclusion

Starting from the basic blocks, we’ve successfully built the skeleton for a modular Angular Dashboard that lets us add and remove cards on the fly! Give yourself a pat on the back and thank you for following along!

If you wish to take a look at the actual code for this skeleton which includes a few card examples too take a look at the GitHub repository!