Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Masha Edelen
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Masha Edelen@mashaedelen )

Handling Service Configuration Without A Configuration Phase In Angular 2.1.1

By Ben Nadel on

In Angular 1.x, we had a "configuration phase" during the application bootstrapping. This was the part of the bootstrapping process in which we could inject configuration data into service providers before the associated services were instantiated. For example, we could inject HTTP Interceptors into the Http Provider. Later came the concept of "decorators" in which we could alter the behavior of a service by monkey-patching its implementation during bootstrapping. In Angular 2, neither of these concepts exist. The configuration phase in Angular 2.x has to be re-imagined using dependency-injection.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When I think about why we needed a configuration phase in Angular 1.x, I keep coming back to the idea of Collections. By this, I mean that a single service needed to be provided with a Collection of configurations that may have been supplied by various modules within the application. In Angular 1.x, there was no way for distinct parts of the application to contribute to a composite dependency. As such, Angular 1.x had to provide a configuration phase in which different parts of the application could access the pre-instantiation providers.

Take Routing for example. In Angular 1.x, route definitions are configured during the configuration phase of the bootstrapping. This is necessary because different modules may have their own route definitions. And, since there's no way to aggregate route definitions into a single dependency (that could be injected into the $route service), a $routeProvider had to be exposed and consumed during the configuration phase.

In Angular 2.x, this "composite dependency" roadblock has been removed by the concept of "multi" providers. A "multi" provider is a single dependency that aggregates a collection of values that have been supplied by different modules within the Angular 2 application.


 
 
 

 
 Composite dependency-injection in Angular 2 replaces the configuration phase in Angular 1.x. 
 
 
 

To leverage the multi provider, you have to invert your mental model. Instead of thinking about explicitly "supplying values" during configuration, you have to start thinking about "receiving values." If you have a service that needs to be instantiated with a Collection, design it to receive that collection in the same way you would outside the context of Angular. Then, configure your dependency-injection to aggregate that collection across the application providers.

To explore this "configuration as dependency-injection" concept, let's look at a Greeter service. The Greeter service exposes one method - greet() - that takes a Name and returns a greeting. Internally, the Greeter service produces the greeting by reducing a collection of "transformers" down into a single value. In Angular 1.x, building this collection of transformers would have been done in the configuration phase with a "greetProvider". But, in Angular 2.x, we need to invert this thinking, designing the Greeter service to simply receive the collection of transformers:

  • // Import the core angular services.
  • import { Inject } from "@angular/core";
  • import { OpaqueToken } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { IGreetTransformer } from "./transformers";
  •  
  •  
  • // I am the dependency-injection token that can be used to aggregate greet transformers.
  • // This is the collection that will be injected into the Greeter class during application
  • // bootstrapping. This kind of "multi" collection replaces the concept of a configuration
  • // phase in Angular 1.
  • export var GREETER_TRANSFORMERS = new OpaqueToken( "Injection token for Greet transformers." );
  •  
  •  
  • // I provide a service for generating greeting messages.
  • export class Greeter {
  •  
  • private transformers: IGreetTransformer[];
  •  
  •  
  • // I initialize the service.
  • constructor( @Inject( GREETER_TRANSFORMERS ) transformers: IGreetTransformer[] ) {
  •  
  • this.transformers = transformers;
  •  
  • }
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I return the greeting for the given name.
  • public greet( name: string ) : string {
  •  
  • var greeting = this.transformers.reduce(
  • ( reduction: string, transformer: IGreetTransformer ) : string => {
  •  
  • return( transformer.transform( reduction ) );
  •  
  • },
  • name
  • );
  •  
  • return( greeting );
  •  
  • }
  •  
  • }

As you can see, the Greeter constructor is designed to receive a collection of Transformers that implement the IGreetTransformer interface:

Greeter( transformers: IGreetTransformer[] )

Of course, we have additional meta-data for the dependency-injection (DI) because this is an Angular 2 application. In this case, we're telling the Angular 2 Injector that this transformers collection will be supplied by the DI token, GREETER_TRANSFORMERS:

Greeter( @Inject( GREETER_TRANSFORMERS ) transformers: IGreetTransformer[] )

This GREETER_TRANSFORMERS value is the collection that we'll be aggregating across the application. This is the value that would have been populated, so to speak, during the configuration phase in Angular 1.x. In Angular 2, however, we can design this Greeter class to be simple and naive to the configuration process.

In this exploration, I want the Greeter service to come pre-baked with a core Transformer. But, I don't want to hard-code that relationship within the Greeter itself because I want to keep the Greeter nice and flexible and open to change. As such, I'm going to wrap the Greeter up in its own Angular Module (NgModule) where I can initialize the GREETER_TRANSFORMERS "multi" dependency with the core Transformer:

  • // Import the core angular services.
  • import { NgModule } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { CoreGreetTransformer } from "./transformers";
  • import { Greeter } from "./greeter";
  • import { GREETER_TRANSFORMERS } from "./greeter";
  •  
  • // "Barrel" exports.
  • // --
  • // NOTE: Traditionally, this kind of exporting of the "public" values from a module is
  • // done in a "barrel" file (ie, index.ts). However, in order to keep this demo smaller,
  • // I'm co-opting the Module file to play double-duty as both the module and the "barrel".
  • export { Greeter } from "./greeter";
  • export { GREETER_TRANSFORMERS } from "./greeter";
  • export { IGreetTransformer } from "./transformers";
  •  
  • @NgModule({
  • providers: [
  • Greeter,
  •  
  • // When Angular instantiates the Greeter class, it's going to inject this
  • // collection of Transformers. By default, the Greeter module is configured to
  • // supply the one "core" Transformer. However, the application at large can
  • // easily add to this "multi" dependency collection.
  • {
  • provide: GREETER_TRANSFORMERS,
  • multi: true,
  • useClass: CoreGreetTransformer
  • }
  • ]
  • })
  • export class GreeterModule {
  • // ... nothing to do here.
  • }

Here, the Greeter Module is defining its own set of exposed Providers that includes the initialization of the GREETER_TRANSFORMERS DI token with the CoreGreetTransformer transformer. Since this is a "multi" token, it means that our application can also add to it. And, in fact, in our root Module, we're going to configure the GREETER_TRANSFORMERS DI token with two additional transformers:

  • // Import the core angular services.
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { AppComponent } from "./app.component";
  • import { ComplimentTransformer } from "./app-transformers";
  • import { GreeterModule } from "./greeter/greeter.module";
  • import { GREETER_TRANSFORMERS } from "./greeter/greeter.module";
  • import { YellingTransformer } from "./app-transformers";
  •  
  • // NOTE: This import is here for use with the Factory (which is commented-out).
  • import { Greeter } from "./greeter/greeter.module";
  •  
  • @NgModule({
  • bootstrap: [ AppComponent ],
  • imports: [ BrowserModule, GreeterModule ],
  • providers: [
  • // As part of the Greeter "configuration", we can setup a collection of
  • // Transformers to be injected into the Greeter as part of the instantiation
  • // process. In this way, we are replacing the Angular 1 "configuration phase"
  • // with dependency-injection mechanics.
  • {
  • provide: GREETER_TRANSFORMERS,
  • multi: true, // <-- This creates an array for a single injectable.
  • useClass: ComplimentTransformer
  • },
  • {
  • provide: GREETER_TRANSFORMERS,
  • multi: true, // <-- This creates an array for a single injectable.
  • useClass: YellingTransformer
  • }
  •  
  • // We could have also used a Factory function to accomplish a similar outcome,
  • // letting the dependency-injection system instantiate the individual
  • // transformers and then allowing us to manually instantiate the Greeter with
  • // the given collection.
  • /*
  • ComplimentTransformer,
  • YellingTransformer,
  • {
  • provide: Greeter,
  • deps: [ YellingTransformer, ComplimentTransformer ],
  • useFactory: function(
  • yellingTransformer: YellingTransformer,
  • complimentTransformer: ComplimentTransformer
  • ) : Greeter {
  •  
  • // When using a Factory, we have to manually assemble the collection of
  • // transformers that we want to inject into the Greeter.
  • return( new Greeter( [ complimentTransformer, yellingTransformer ] ) );
  •  
  • }
  • }
  • */
  • ],
  • declarations: [ AppComponent ]
  • })
  • export class AppModule {
  • // ... nothing to do here.
  • }

NOTE: I've supplied a commented-out Factory function approach to see how something similar could be implemented more explicitly.

As you can see, in addition to the CoreGreetTransformer provided by the Greeter Module itself, the App Module is supplying two additional transformers: the YellingTransformer and the ComplimentTransformer implementations. Now, when the Angular 2.x application is bootstrapped, our Greeter service will "receive" a constructor argument that composes all three of these transformers.

Within our root component, we can inject the Greeter service and try to generate a greeting:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { Greeter } from "./greeter/greeter.module";
  •  
  •  
  • @Component({
  • selector: "my-app",
  • template:
  • `
  • <em>Look in the console to see the Greeter result.</em>
  • `
  • })
  • export class AppComponent {
  •  
  • // I initialize the component.
  • constructor( greeter: Greeter ) {
  •  
  • console.group( "Testing Greeter" );
  • console.log( greeter.greet( "Sarah" ) );
  • console.groupEnd();
  •  
  • }
  •  
  • }

And when we run this Angular application, we get the following output:


 
 
 

 
 Thinking about the Configuration phase in Angular 2.x. 
 
 
 

As you can see, the Greeter service successfully applied all three transformers, doing so in the order in which they were defined. First, it applied the CoreGreetTransformer implementation, supplied by the Greeter Module:

  • // I am the interface that must be implemented by all greet transformers.
  • export interface IGreetTransformer {
  • transform( value: string ) : string;
  • }
  •  
  •  
  • // I am the core transformer that is used, no matter what collection of transformers have
  • // been configured for dependency-injection.
  • export class CoreGreetTransformer implements IGreetTransformer {
  •  
  • // I transform the given value as part of the Greeter reduction.
  • public transform( name: string ) : string {
  •  
  • return( "Hello " + name + "." );
  •  
  • }
  •  
  • }

Then, it applied the ComplimentTransformer and the YellingTransformer implementations in the same order in which they were defined in the App Module:

  • // Import the application components and services.
  • import { IGreetTransformer } from "./greeter/greeter.module";
  •  
  •  
  • // I add a compliment to the end of the greeting.
  • export class ComplimentTransformer implements IGreetTransformer {
  •  
  • // I transform the given value as part of the Greeter reduction.
  • public transform( value: string ) : string {
  •  
  • return( value + " You look beautiful this morning." );
  •  
  • }
  •  
  • }
  •  
  •  
  • // I convert the greeting to UPPERCASE!!! FOR THE WIN!!!
  • export class YellingTransformer implements IGreetTransformer {
  •  
  • // I transform the given value as part of the Greeter reduction.
  • public transform( value: string ) : string {
  •  
  • return( value.toUpperCase().replace( /\./g, "!" ) );
  •  
  • }
  •  
  • }

In Angular 2.x, we sill have all the configuration potentiality that we had in Angular 1.x. But, now that we can aggregate a "multi" dependency across the various modules of an Angular 2 application, we no longer need an explicit "configuration phase". This actually forces us to create cleaner service boundaries by requiring that the configuration be dictated by dependency-injection. Meta-data aside, this allows us to design services as we would outside of an Angular 2 context, which helps us think more clearly about object design.

As a final side-note, if you need to configure a service after bootstrapping, such as turning runtime-flags on or off, you can still do this in an application "run block", similar to what we had in Angular 1.x.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Hi, very nice blog.
Thanks for all the help you gave me with it.

I looked at this example, and i'm trying to make something like this

I have a form, for activities for example, and each activity has a type, and each type could have more controls in the form.

My idea is to register one component for each type, and then select based on the type, and plug it on the form.

Have you done anything like that?

Reply to this Comment

@Luiz,

I'm not too familiar with the form stuff; but, from what I have seen none of the form stuff really involves bootstrap configuration. It all seems to be done in the Components that render the form. But, maybe I am not understanding what you are asking.

Reply to this Comment

@Ben,

Sorry,

I read back my comment and it's not as clear as I expected.

What I'm making is this.

I have many components that implements the same interface, what I want to do is get one of this components based for example in the name of the component, and the plug this component in the actual application.
Considering that this component is an UI component.

Thanks

Reply to this Comment

@Luiz,

Ultimately, components need to be "matched" in a template using some sort of a selector. So, regardless of what interface a component implements, it still has to be:

1. Defined as a "declaration" in the root NgModule or one of the module that it imports.

2. Match some element-selector (or attribute selector) in one of the rendered templates.

Now, if you are saying that you have multiple components with the *same selector* and you are trying to have only one of them matching in a template, I am not sure that this is possible. All matching components will be associated with the given element. If you find this happening, you may have to create additional NgModule's that isolate the scope of a particular component (applying it only to templates within that NgModule).

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.