Providing Run Blocks As Services That Implement A Runnable Interface In Angular 2.1.1
Over the weekend, I experimented with using Service constructors to provide "run blocks" in Angular 2. In that experiment, I leveraged the core JavaScript functionality that allows you to override the return value of a Class constructor. That experiment worked; but, overriding a constructor's return value felt a little wonky. Now that I'm getting used to TypeScript, I wanted to do another quick follow-up experiment in which we implement "run blocks" using a Runnable interface. Meaning, rather than using lambdas, we can provide a Service class that implements a Runnable interface.
CAUTION: I am not actually suggesting that you take this approach. This is much more of an exploration of the Angular 2 mechanics than it is a practical guide to solving Angular 2 problems.
Run this demo in my JavaScript Demos project on GitHub.
Ultimately, a "run block" still needs to be run at the end of the Angular 2 bootstrapping process using the APP_INITIALIZER dependency-injection (DI) token. So, what I want to do is create another module that wraps the APP_INITIALIZER provider, consuming our Runnable implementations from within the traditional factory function. In the following module, I'm providing this factory function:
// Import the core angular services.
import { APP_INITIALIZER } from "@angular/core";
import { NgModule } from "@angular/core";
import { OpaqueToken } from "@angular/core";
// The RunnerModule will collect and execute a series of IRunnable class implementations
// at the end of the bootstrapping process. Each runnable must provide a public .run()
// method that will be invoked at the end of the bootstrapping process.
export interface IRunnable {
run() : void;
}
// I am the dependency-injection token for the collection of IRunnable implementations.
export var RUNNABLE = new OpaqueToken( "I am the multi collection of IRunnable classes." );
@NgModule({
providers: [
// This module uses a Factory function to add to the APP_INITIALIZER collection;
// however, there's no way to provide @Optional() dependencies to a factory (at
// least not that I can find). As such, we need to provide a "null" Runnable that
// will initialize the multi: true collection. This way, if the application
// doesn't go on to add to this DI token, we'll still have something to inject
// into our Factory function below.
{
provide: RUNNABLE,
multi: true,
useValue: null
},
// I provide the "run block" that iterates over the IRunnable implementations. This
// allows other modules in the application to provide application initializers in
// the form of IRunnable classes, as opposed to "run blocks".
{
provide: APP_INITIALIZER,
multi: true,
deps: [ RUNNABLE ],
useFactory: function( runnables: IRunnable[] ) : () => void {
return( runblock );
function runblock() : void {
runnables.forEach(
function iterator( runnable ) {
// NOTE: Checking for truthy due to NULL runnable (see above).
runnable && runnable.run();
}
);
}
}
}
]
})
export class RunnerModule {
// ... nothing to do here.
}
This module provides a few public values; IRunnable is the interface that our service-based "run blocks" need to implement; the RUNNABLE DI token is how those runnable implementations will be defined in the various module providers. As you can see, in the factory function provided by the Runner module, the collection of RUNNALBE instances is injected and consumed - each having its .run() method invoked.
I couldn't find a way to injection an optional dependency into a factory function in Angular 2; so instead, I'm initializing the RUNNABLE multi:true provider with a null value. Then, in the factory function, I'm checking for existence - to weed-out the null value - before invoking the .run() method. This isn't exactly elegant; but, I couldn't find any other way around it.
Now, in the App Module, rather than providing run blocks as factory functions, we can provide them as Services, complete with dependency-injection capabilities. In the following app module code, I'm providing a MyRunner IRunnable implementation that logs to the console:
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { ErrorHandler } from "@angular/core";
import { Injectable } from "@angular/core";
import { NgModule } from "@angular/core";
// Import the application components and services.
import { IRunnable } from "./runner.module";
import { RUNNABLE } from "./runner.module";
import { RunnerModule } from "./runner.module";
import { AppComponent } from "./app.component";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// In this demo, rather than providing a "run block", we're going to provide a Runnable
// implementation that will be exercised at the end of the bootstrapping process.
@Injectable()
export class MyRunner implements IRunnable {
private errorHandler: ErrorHandler;
// I initialize the runner service.
constructor( errorHandler: ErrorHandler ) {
this.errorHandler = errorHandler;
}
// ---
// PUBLIC METHODS.
// ---
// I am invoked (by the RunnerModule) at the end of the bootstrapping process.
public run() : void {
console.group( "Runnable Implementation" );
console.log( "Runnable implementation exposing public run() method." );
console.log( this.errorHandler );
console.groupEnd();
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@NgModule({
bootstrap: [ AppComponent ],
imports: [ BrowserModule, RunnerModule ],
providers: [
// Here, instead of using a Factory to generate a "run block", we're
// providing a Runner implementation that will be invoked by a "run block" in
// the RunnerModule. The outcome is exactly the same; but, this allows the app
// initializer to be implemented as a Service rather than as a Factory.
{
provide: RUNNABLE,
multi: true,
useClass: MyRunner
}
],
declarations: [ AppComponent ]
})
export class AppModule {
// ... nothing to do here.
}
As you can see, the MyRunner Service class looks just like any other service class - it accepts injected values and it exposes public methods. In this case, it only has to expose one method - .run() - in order to adhere to the IRunnable interface. The MyRunner Service class is then provided as part of the multi:true RUNNABLE collection.
Now, when we run the Angular 2 application, the RunnerModule will take the RUNNABLE services and consume them at the end of the bootstrapping process:
As you can see, the RunnerModule invokes the .run() method of our MyRunner service class.
There's no doubt that this approach requires a lot more cruft and boilerplate than using a traditional "run block". But, I do think there is something nice about dealing with a Class rather than a Factory function. Of course, the reality is, when using run blocks, you're likely to only have one run block in your application (since there is only one root application); as such, this was much more of a fun Angular 2 experiment than a suggestion on how to do things.
Want to use code from this post? Check out the license.
Reader Comments