Providing Run Blocks Using A Service Constructor In Angular 2.1.1
Yesterday, I took a look at configuring Angular 2 services using both dependency-injection and run blocks. As demonstrated in that post, run blocks can be implemented as factory functions. But, these factory functions are kind of ugly and require a special dependency-injection notation. I wanted to see if I could improve the optics of the run block by leveraging the fact that JavaScript Constructor functions can return values that override the "newable" assignment.
Run this demo in my JavaScript Demos project on GitHub.
In Angular 2, Services have built-in dependency-injection mechanics - when using Type annotations - which make them much more attractive than the Factory function "deps" array. But, the real question is whether or not the return value of the Service constructor can be used to drive the dependency-injection providers. As I've demonstrated in the past, if a JavaScript Constructor returns an "object", that object will take the place of the instantiated class. But, it's not entirely clear how the Angular 2 bootstrapping works. So, I thought I would see what kind of shenanigans I could get up to.
In the following App Module code, I'm providing two different "run blocks"; one with a transitional Factory function; and, one with a Service class that returns the run block from its constructor:
// Import the core angular services.
import { APP_INITIALIZER } from "@angular/core";
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 { AppComponent } from "./app.component";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Injectable()
export class OnRun {
// Because this is a "class" and not a Factory function, we can use the normal
// dependency-injection semantics. All we have to do is define Types here and those
// instances will be provided to the constructor.
constructor( errorHandler: ErrorHandler ) {
// CAUTION: Here, we are "hacking" the workflow. Instead of letting Angular use
// the OnRun class instance, we're explicitly overriding the constructor method's
// return value, passing back the "run block". This is a core feature of
// JavaScript's "newable" Functions (from MDN):
// --
// >> The object returned by the constructor function becomes the
// >> result of the whole new expression. If the constructor function
// >> doesn't explicitly return an object, the object created in step 1
// >> is used instead. (Normally constructors don't return a value,
// >> but they can choose to do so if they want to override the normal
// >> object creation process.)
// --
return( runblock );
// NOTE: This is the method that gets run at the end of the bootstrapping process.
function runblock() : void {
console.group( "Class Instance" );
console.log( "Run block generated by class instance (return value)." );
console.log( errorHandler );
console.groupEnd();
}
}
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@NgModule({
bootstrap: [ AppComponent ],
imports: [ BrowserModule ],
providers: [
OnRun,
// Here, we are defining an app initializer or "run block" using a factory
// function. The factory function returns a Function reference which will be
// invoked at the end of the bootstrapping process. Notice that the factory
// function can use dependency-injection using the "deps" array.
{
provide: APP_INITIALIZER,
multi: true,
deps: [ ErrorHandler ],
useFactory: function( errorHandler: ErrorHandler ) : () => void {
return( runblock );
// NOTE: This is the method that gets run at the end of the
// bootstrapping process.
function runblock() : void {
console.group( "Factory Function" );
console.log( "Run block generated by Factory function." );
console.log( errorHandler );
console.groupEnd();
}
}
},
// As it turns out, we can also define app initializers as classes. Sort of.
// Ultimately, we still need to return a Function reference; but, we can leverage
// the Class mechanics to drive the dependency-injection as long as the class
// constructor still returns the Function reference that gets invoked at the end
// of the bootstrapping process.
{
provide: APP_INITIALIZER,
multi: true,
useClass: OnRun
}
],
declarations: [ AppComponent ]
})
export class AppModule {
// ... nothing to do here.
}
As you can see, both "run blocks" require the core ErrorHandler service in order to make sure that dependency-injection works. Then, both factor functions log to the console. And, when we run this Angular 2 application, we get the following output:
As you can see, both run blocks were provided and invoked alongside the full power of dependency-injection. Not only does this provide an alternate means for defining run blocks, it also demonstrates that Angular 2, even when using TypeScript, will adhere to the return value rules of JavaScript Class constructors.
Want to use code from this post? Check out the license.
Reader Comments
@All,
While overriding constructor functionality is a core part of JavaScript, it feels a bit wonky in a TypeScript context. As such, I wanted to explore a slightly different approach - implementing "Runnable" service classes:
www.bennadel.com/blog/3177-providing-run-blocks-as-services-that-implement-a-runnable-interface-in-angular-2-1-1.htm
This approach uses a proxy module that uses the traditional Factory function approach to consume a collection of "Runnable" services.