injection picture found on pexels.com

Dependency Injection & Smart Table: Advanced Patterns

In the first article, we learnt how to quickly set up an advanced table component in a declarative way using smart-table-ng.

In the second article, we looked at how we could use Angular built-in dependency injection together with smart-table to achieve different data loading patterns without changing our component.

In this article we are going to consider a new use case, where one would need to configure at run time the service in charge of fetching the data.

Set up the component

Let’s consider the following data set:

interface Product {
id:number;
sku:string;
title:string;
price:number;
sales:number;
refunds:number;
}
const enum MONTH {
JANUARY = 'January',
FEBRUARY = 'February',
MARCH = 'March',
APRIL = 'April',
MAY = 'May',
JUNE = 'June',
JULY = 'July',
AUGUST = 'August',
SEPTEMBER = 'September',
OCTOBER = 'October',
November = 'November',
DECEMBER = 'December'
}
export const data = {
[MONTH.JANUARY]:[{
id:1,
sku:'LTP',
title:'Laptop',
price:800,
sales:345,
refunds:12
}, {
id:2,
sku:'PEN',
title:'Pen',
price:2,sales:2023,
refunds:1
},
 ...
  [MONTH.FEBRUARY]:[{
id:1,
sku:'LTP',
title:'Laptop',
price:820,
sales:300,
refunds:5
}, {
id:2,
sku:'PEN',
title:'Pen',
price:3,
sales:1800,
refunds:70
},
...
  [MONTH.MARCH]
};

We basically have a list of products with a bunch of properties grouped by month.

A service to fetch a monthly report could have the following interface:

interface ProductService{
fetchMonthlyReport(month: MONTH): Observable<Product[]>
}

Note: We use an Observable to handle the asynchronous task.

Then our ProductTable component template could look like:

<div stTable #table="stTable">
<div *ngIf="table.busy">Loading...</div>
<table>
<thead>
<tr>
<th stSort="id">Id</th>
<th stSort="sku">SKU</th>
<th stSort="title">Title</th>
<th stSort="price">Price</th>
<th stSort="sales">Sales</th>
<th stSort="refunds">Refunds</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let post of table.items">
<td>{{post.value.id}}</td>
<td>{{post.value.sku}}</td>
<td>{{post.value.title}}</td>
<td>{{post.value.price}}</td>
<td>{{post.value.sales}}</td>
<td>{{post.value.refunds}}</td>
</tr>
</tbody>
</table>
</div>

A pretty simple table which uses different smart-table directives and make the table sortable by column (see the first article for more details).

The component logic is empty. However we follow the guidelines of the second article to fetch the data.

import { Component} from '@angular/core';
import { SmartTable, from} from 'smart-table-ng';
import { ProductService } from './product.service';
import {MONTH, Product} from './data';
@Component({
selector: 'product-table',
templateUrl: './product-table.component.html',
providers: [{
provide: SmartTable,
useFactory: (products: ProductService) => {
return from(products.fetchMonthlyReport(MONTH.JANUARY));
},
deps:[
ProductService
]}]
})
export class ProductTableComponent {}

The important part is obviously the “providers” where we use a factory to create a new sand boxed SmartTable instance for each mounted ProductTableComponent. The factory needs our Product service singleton to effectively create the data source.

In our example, the data source is hard coded to January, which is not ideal as we would need to create a different component class for each month. To fix that issue let’s create a Token for the month so it can be an injected parameter:

import {InjectionToken} from '@angular/core';
const MONTH_TOKEN = new InjectionToken<MONTH>('month');

Now the providers can be:

const providers = [{ 
provide: SmartTable,
useFactory: (products: ProductService, month: MONTH) => {
return from(products.fetchMonthlyReport(month));
},
deps:[
ProductService,
MONTH_TOKEN,
]
}];

Great: our component is now fully configurable. Where the MONTH_TOKEN comes from is not important for that component. It could be injected by the application by a configuration service, a router, a parent component, etc: that is not the concern of the ProductTable Component.

You can see a running example on the following Stackblitz:

Configure the month in a declarative way through an attribute

Imagine you want to specify the monthly report to fetch thanks to an attribute on the component (or a directive). You can’t resolve this value as injected value before the component is mounted and the binding value is evaluated.

Thankfully Smart Table allows you to specify the data source whenever you want and therefore hook yourself into the component life cycle.

Let’s create a directive which updates the data source whenever a bound property changes:

import { Directive, Input, OnChanges } from '@angular/core';
import { SmartTable } from 'smart-table-ng';
import { Product, MONTH } from './data';
import { ProductService } from './product.service';
@Directive({
selector: '[monthly-report]'
})
export class MonthlyReportDirective {
  @Input('monthly-report') month: MONTH;
  constructor(private _smartTable: SmartTable<Product>, 
private _products: ProductService) {}
  ngOnChanges(change){
this._products.fetchMonthlyReport(change.month.currentValue)
.subscribe(products => this._smartTable.use(products));
}
}

This directive requires the available SmartTable instance and will specifically ask it to use a different data set whenever the input value changes: that is the purpose of the “use” method.

You’ll need to change a little bit you ProductTableComponent as well:

import { Component } from '@angular/core';
import { SmartTable, of } from 'smart-table-ng';
@Component({
selector: 'product-table',
templateUrl: './product-table.component.html',
providers: [
{ provide: SmartTable, useFactory: () => of([]) }
]
})
export class ProductTableComponent { }

You will have noticed that the smart table provider uses now a factory which returns a new instance for each product table component mounted. The data source is an empty array as it will be provided later by the recently created directive.

We can now use our component together with the new directive to make it configurable in a declarative way.

<div>
<h2>January Report</h2>
<product-table monthly-report="January"></product-table>
  <h2>February Report</h2>
<product-table monthly-report="February"></product-table>
  <h2>March Report</h2>
<product-table monthly-report="March"></product-table>
</div>

If you run the following stackblitz:

The three components are bound to a different monthly report and have their own state (you can sort one table independently than the others).

Change the data source dynamically

Even better you can dynamically change the data source as we have registered to binding changes. For example, let’s add to our container component a currentMonth variable:

import { Component } from '@angular/core';
import {MONTH} from './data';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
currentMonth = MONTH.JANUARY;
}

with the following template

<div>
<label>Current Month:
<select #select (input)="currentMonth = select.value">
<option value="January">January</option>
<option value="February">February</option>
<option value="March">March</option>
</select>
</label>
<product-table [monthly-report]="currentMonth"></product-table></div>

We have added a select control to change the current month so the table updates with the relevant data whenever the select value changes. As the smart table service is not disposed you’ll keep the same table state between two different data sources: for example if you sort your data for January by “sales”, the data will also be sorted by “sales” when you change the data source to February or March.

See the following stackblitz:

Partial conclusion

The “use” method is very powerful as it gives us control on when and how the data is loaded into our table. However, it requires us to manage some state in our component which could normally be handled by the smart table service instance; and it that sens, could lead to more complex code.

We have used a monthly-report directive specific to our domain model to configure our table in a declarative way, however we could imagine a more generic one (like “st-src”) to make the data source a binding of any smart table component so it could be passed by a parent component in the React style. It could be really convenient if your team mates are not comfortable with dependency injection for example.

Use a service to manage data sources

Observable is an important concept in the Angular framework. It is indeed a powerful abstraction when it comes to deal with asynchronous data or data which evolve in time. Our data source has actually both properties: it resolves asynchronously and may change within time if we want the capabilities of changing the selected month. Let’s implement a service that emits a sequence of monthly reports and exposes this sequence through an Observable:

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { Product, MONTH } from './data';
import { ProductService } from './product.service';
@Injectable()
export class MonthlyReportService {

constructor(private _products: ProductService) {}

private dataSource = new Subject<Product[]>();
data$ = this.dataSource.asObservable();
  useMonth(month: MONTH) {
this._products.fetchMonthlyReport(month)
.subscribe(report => this.dataSource.next(report));
}
}

We can now simplify our ProductTable component to rely on the observable data source. In this case we also have decided the selected month should be part of the component state although we could just rely on the injected MonthlyReportService whose own state could be changed by a parent component:

import { Component, Input, OnChanges } from '@angular/core';
import { SmartTable, from } from 'smart-table-ng';
import { MONTH, MONTH_TOKEN, Product } from './data';
import { MonthlyReportService } from './monthly-report.service';
@Component({
selector: 'product-table',
templateUrl: './product-table.component.html',
providers: [
MonthlyReportService, // here the monthly report service is created but it could be injected in a parent component
{
provide: SmartTable, useFactory: (source: MonthlyReportService) => from(source.data$),
deps: [MonthlyReportService]
}]
})
export class ProductTableComponent implements OnChanges {
  @Input() month: MONTH;
  constructor(private _montlyReport: MonthlyReportService) {}
  ngOnChanges(change) {
const month = change.month.currentValue;
this._montlyReport.useMonth(month);
}
}

This approach may seem quite similar to the “use” method aforementioned but it presents some advantages. For example some others directives could rely on the data source to create a select box with a range of available values, they can now require the MonthlyReport service to be injected. We have somehow on one side an observable data source (held by the MonthlyReportService instance), and on the other side an observable displayed data collection (held by the SmartTable service instance) evolving with the table state.

You can see a running example in the following stackblitz

Bonus: take full control with dynamically loaded components

Angular framework does a lot behind the scene for us: it creates and disposes components, it manages injectors to inject the required services etc.

You can actually do all that job “manually” too, in order to keep full control on how components and services are created/disposed and how the components are attached or detached from/to a DOM tree.

Let’s first create a directive to expose a ViewContainerRef.

import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[st-table-container]'
})
export class StTableContainerDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}

This will be used later by our loader component as a ViewChild

We can now create our table loader component which will be in charge of dynamically creating our table components based on a bound value

import { ProductService } from './product.service';
import { MONTH, MONTH_TOKEN, Product } from './data';
import { Component, ViewChild, ComponentFactoryResolver, Input, OnChanges, Injector } from '@angular/core';
import { StTableContainerDirective } from './table-container.directive'
import { SmartTable, from } from 'smart-table-ng';
import { ProductTableComponent } from './product-table.component';
@Component({
selector: 'table-loader',
templateUrl: './table-loader.component.html'
})
export class TableLoaderComponent implements OnChanges {

private _reports = new Map();

@Input() month: MONTH;

@ViewChild(StTableContainerDirective) tableContainer: StTableContainerDirective;
  constructor(private _componentFactoryResolve: ComponentFactoryResolver, private _prodcuts: ProductService) {}
  ngOnChanges(change) {
if (change.month) {
const month = change.month.currentValue;
const viewContainerRef = this.tableContainer.viewContainerRef;
viewContainerRef.detach();

if (this._reports.has(month) === false) {
const table = from(this._prodcuts.fetchMonthlyReport(month));
const injector = Injector.create([{ provide: SmartTable, useValue: table }], viewContainerRef.injector);
const componentFactory = this._componentFactoryResolve.resolveComponentFactory(ProductTableComponent);
this._reports.set(month, viewContainerRef.createComponent(componentFactory, 0, injector));
} else {
viewContainerRef.insert(this._reports.get(month).hostView);
}
}
}
}

with the following template

<div>
<ng-template st-table-container></ng-template>
</div>

The template is pretty simple. It just has a placeholder with the st-table-container directive to keep a slot for our components.

The interesting part is ngOnChange life cycle hook.

code snippet of the ngOnChanges life cycle

If the month value changes, we get the view container related to the slot held by the st-table-container directive and we detach the mounted component.

We keep a registry of table components associated to each month, if we already have an entry for a given month we attach the related component. If not we create one.

To create the component we use the ComponentFactoryResolver. Normally the framework takes care of creating the associated injector and providing the required services. However the whole purpose of this loader is for us to manage that part; therefore we need to create an injector ourselves by passing the wished data source (related to the appropriate month).

Note: in this example, we have decided to keep created components and services in a registry whose keys are the month values. It allows us to keep them in a given state even though the component is detached: if you sort a table, change the month and then change again the month to its former value, the component attached will remain in the sorted state. Of course one could decide to entirely dispose components and services when month value changes and create new instances instead.

This technique requires more boilerplate code but gives us a full control on phases that are usually handled by the framework. This technique could be used to create a router for example.

Here is the whole stackblitz:

Conclusion

In this article we have reviewed different interesting techniques to configure smart table data sources at run time with a configuration (a month value here) provided in a declarative way by a component in the tree. Thanks to the flexibility of Angular framework and the design of smart-table-ng, we have at our disposition a wide range of possibilities