DailyJS
Published in

DailyJS

injection picture found on pexels.com

Dependency Injection & Smart Table: Advanced Patterns

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]};
interface ProductService{
fetchMonthlyReport(month: MONTH): Observable<Product[]>
}
<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>
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 {}
import {InjectionToken} from '@angular/core';const MONTH_TOKEN = new InjectionToken<MONTH>('month');
const providers = [{ 
provide: SmartTable,
useFactory: (products: ProductService, month: MONTH) => {
return from(products.fetchMonthlyReport(month));
},
deps:[
ProductService,
MONTH_TOKEN,
]
}];

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.

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));
}
}
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 { }
<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>

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;
}
<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>

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.

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));
}
}
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);
}
}

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.

import { Directive, ViewContainerRef } from '@angular/core';@Directive({
selector: '[st-table-container]'
})
export class StTableContainerDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
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);
}
}
}
}
<div>
<ng-template st-table-container></ng-template>
</div>
code snippet of the ngOnChanges life cycle

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

--

--

JavaScript news and opinion.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store