Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Chris Simmons
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Chris Simmons@cfchimp )

Providing Module Configuration Using forRoot() And Ahead-Of-Time Compiling In Angular 7.2.0

By Ben Nadel on

Yesterday, I was trying to design a feature module in Angular 7.2.0 that used the .forRoot() pattern to provide optional configuration data to the bootstrapping process of my feature module. However, since I was using Ahead-of-Time (AoT) compiling, I was running into a number of errors that wouldn't be relevant when using the Just-in-Time (JIT) compiler. Getting over these errors wasn't completely obvious to me. And, it took me over an hour of trial-and-error to find a valid approach. As such, I wanted to document my solution for optional, feature module configuration so that I could refer back to it in the future.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To set the context for this problem, imagine that I have a service class that accepts, as a dependency, an instance of an options class. It may look something like this:

  • // Import the core angular services.
  • import { Injectable } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Injectable({
  • providedIn: "root"
  • })
  • export class MyServiceOptions {
  •  
  • public retryCount: number = 6;
  • public retryInterval: number = 2000;
  •  
  • }
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Injectable({
  • providedIn: "root"
  • })
  • export class MyService {
  •  
  • private options: MyServiceOptions;
  •  
  • // I initialize the my-service service.
  • constructor( options: MyServiceOptions ) {
  •  
  • this.options = options;
  •  
  • console.group( "MyService Constructor" );
  • console.log( "Injected Options" );
  • console.log( this.options );
  • console.groupEnd();
  •  
  • }
  •  
  • }

Notice that both MyService and MyServiceOptions are full-on classes. That is, they can both be treated as "Types" by the TypeScript compiler which means that when I inject the MyServiceOptions "type" into the MyService "type", I don't have to fool around with any @Inject() meta-data; the Angular Dependency-Injection container will simply know to inject the correct "type" when instantiating the service class.

The MyServiceOptions class bakes-in some default configuration values. But, it is nice to provide a way for the consuming application to override these configuration values when importing the feature module. This is where the static module method, .forRoot(), comes into play. Ideally, I'd love to be able to have an AppModule that looks something like this:

  • // 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 { MyServiceModule } from "./my-service/my-service.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • BrowserModule,
  • // When importing the MyServiceModule, we can provide optional configuration
  • // data. This will be used, under the hood, to instantiate the MyServiceOptions
  • // and MySerivce classes.
  • MyServiceModule.forRoot({
  • retryInterval: 5000,
  • retryCount: 3
  • })
  • ],
  • declarations: [
  • AppComponent
  • ],
  • bootstrap: [
  • AppComponent
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

As you can see, when the AppModule imports the MyServiceModule, it provides override settings for the "retryInterval" and "retryCount" properties. Of course, the hash being passed to the .forRoot() method invocation is not the same as the MyServiceOptions "type" being injected into the MyService class. As such, our feature module has to translate this incoming raw data-bag into a fully-featured "Type" in order to play nice with the Dependency-Injector container.

If we were using the Just-in-Time (JIT) compiler, I could just create a factory function that translates the incoming options hash into an instance of the MyServiceOptions class:

  • // CAUTION: THIS DOES NOT WORK WHEN USING AoT COMPILER.
  • // ....
  • static forRoot( options?: ModuleOptions ) : ModuleWithProviders {
  •  
  • return({
  • ngModule: MyServiceModule,
  • providers: [
  • {
  • provide: MyServiceOptions,
  • useFactory: () => {
  •  
  • var myServiceOptions = new MyServiceOptions();
  • myServiceOptions.retryCount = options.retryCount;
  • myServiceOptions.retryInterval = options.retryInterval;
  •  
  • return( myServiceOptions );
  •  
  • }
  • }
  • ]
  • });
  •  
  • }
  • // ....

With the Ahead-of-Time (AoT) compiler, however, this approach breaks with the following error:

Function calls are not supported in decorators.

In order to play nicely with the AoT compiler, you have to extract the factory function declaration out as a top-level exported value on the JavaScript module. However, when you do that, the factory function is no longer lexically-bound to (ie, no longer "closes over") the "options" argument of the .forRoot() method.

To solve the closure problem, you might try to just supply the "options" value using the "deps" option on your extracted factory function:

deps: [ options ]

However, since the "options" arguments is just a plain hash, TypeScript will fail with the following error:

Error: Internal error: unknown identifier {"retryInterval":5000,"retryCount":3}

The problem here is that the Dependency-Injection framework relies on "Types". And, our "options" argument isn't a Type - it's an instance of Object. As such, the DI container can't locate a relevant value to inject as part of the factory function invocation.

To solve this Types problem, I ended up creating an intermediary injectable using the InjectionToken class. This InjectionToken can then be used to provide the .forRoot() "options" object as a proper dependency of our extracted factory function:

  • // Import the core angular services.
  • import { InjectionToken } from "@angular/core";
  • import { ModuleWithProviders } from "@angular/core";
  • import { NgModule } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { MyService } from "./my-service.service";
  • import { MyServiceOptions } from "./my-service.service";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // Re-export services, treating the module like a "barrel".
  • export { MyService };
  • export { MyServiceOptions };
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule()
  • export class MyServiceModule {
  •  
  • // I setup the module providers for the root application.
  • static forRoot( options?: ModuleOptions ) : ModuleWithProviders {
  •  
  • return({
  • ngModule: MyServiceModule,
  • providers: [
  • // In order to translate the raw, optional OPTIONS argument into an
  • // instance of the MyServiceOptions TYPE, we have to first provide it as
  • // an injectable so that we can inject it into our FACTORY FUNCTION.
  • {
  • provide: FOR_ROOT_OPTIONS_TOKEN,
  • useValue: options
  • },
  • // Once we've provided the OPTIONS as an injectable, we can use a FACTORY
  • // FUNCTION to then take that raw configuration object and use it to
  • // configure an instance of the MyServiceOptions TYPE (which will be
  • // implicitly consumed by the MyService constructor).
  • {
  • provide: MyServiceOptions,
  • useFactory: provideMyServiceOptions,
  • deps: [ FOR_ROOT_OPTIONS_TOKEN ]
  • }
  •  
  • // NOTE: We don't have to explicitly provide the MyService class here
  • // since it will automatically be picked-up using the "providedIn"
  • // Injectable() meta-data.
  • ]
  • });
  •  
  • }
  •  
  • }
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // I define the shape of the optional configuration data passed to the forRoot() method.
  • export interface ModuleOptions {
  • retryCount?: number;
  • retryInterval?: number;
  • }
  •  
  • // I am the token that makes the raw options available to the following factory function.
  • // --
  • // NOTE: This value has to be exported otherwise the AoT compiler won't see it.
  • export var FOR_ROOT_OPTIONS_TOKEN = new InjectionToken<ModuleOptions>( "forRoot() MyService configuration." );
  •  
  • // I translate the given raw OPTIONS into an instance of the MyServiceOptions TYPE. This
  • // will allows the MyService class to be instantiated and injected with a fully-formed
  • // configuration class instead of having to deal with the Inject() meta-data and a half-
  • // baked set of configuration options.
  • // --
  • // NOTE: This value has to be exported otherwise the AoT compiler won't see it.
  • export function provideMyServiceOptions( options?: ModuleOptions ) : MyServiceOptions {
  •  
  • var myServiceOptions = new MyServiceOptions();
  •  
  • // If the optional options were provided via the .forRoot() static method, then apply
  • // them to the MyServiceOptions Type provider.
  • if ( options ) {
  •  
  • if ( typeof( options.retryCount ) === "number" ) {
  •  
  • myServiceOptions.retryCount = options.retryCount;
  •  
  • }
  •  
  • if ( typeof( options.retryInterval ) === "number" ) {
  •  
  • myServiceOptions.retryInterval = options.retryInterval;
  •  
  • }
  •  
  • }
  •  
  • return( myServiceOptions );
  •  
  • }

As you can see, I'm using the InjectionToken class to make the raw "options" hash available as a dependency. This, dependency then gets injected into the provideMyServiceOptions() factory function, which translates it into a MyServiceOptions class instance. This MyServiceOptions instance is then implicitly available to the MyService class as part of the Dependency-Injection container.

ASIDE: Once the "options" object was made available using the InjectionToken, it could theoretically be injected into the MyService class using @Inject(FOR_ROOT_OPTIONS_TOKEN) meta-data in the MyService constructor; however, I wanted the MyService class to depend on a Type rather than a raw object. Using a Type feels cleaner to me. But, that's just personal preference.

To test that the MyService feature module works with the optional overrides, I injected it into my AppComponent and logged it to the console:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // Import the application components and services.
  • import { MyService } from "./my-service/my-service.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • Testing forRoot() configuration, see console logging.
  • `
  • })
  • export class AppComponent {
  •  
  • // I initialize the app component.
  • constructor( myService: MyService ) {
  •  
  • console.group( "AppComponent Constructor" );
  • console.log( "myService injectable" );
  • console.log( myService );
  • console.groupEnd();
  •  
  • }
  •  
  • }

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


 
 
 

 
 Providing .forRoot() feature-module options as a real Type for subsequent dependency-injection in Angular 7.2.0. 
 
 
 

As you can see, by translating the .forRoot() options object into an instance of the MyServiceOptions Type (using the factory function and the intermediary InjectionToken), the Dependency-Injection container was able to instantiate the MyService class, which we were then able to inject into the AppComponent.

The Ahead-of-Time (AoT) compiler for Angular provides nice performance benefits; however, it does so with some caveats. As we've seen here, we can't just use an in-line factory function to provide feature-module configuration to our feature-module services. Instead, we have to jump through a few intermediary hoops. But, the outcome is that we can still use proper Types to represent developer-provided .forRoot() arguments.



Reader Comments

Hi,
Great tutorial!

Although, I can run the app using standard 'npm run start', I still cannot build the code.

It still gives me the following error:
ERROR in Error during template compile of 'ParentModule'
Function calls are not supported in decorators but 'ChildModule' was called.

Reply to this Comment

Hi! Really great tutorial, but I still have the error: "Function calls are not supported in decorators" if I build the module and try to install it from NPM. Your code on Github works perfectly "as is", but if I create a NPM module with the two files in "my-service" directory, it fails. Any idea? I am going mad to solve this issue...

Reply to this Comment

@Alexandro,

Honestly, this limitation of not having Functions is so frustrating. Unfortunately, I have no advice -- I'm just Googling for solutions when I run into this. I know you posted a few days ago, were you able to find a solution since then?

Reply to this Comment

@Ben,

Hey,
Yes, I did solve it.
The issue was a console.log in the ChildComponent's constructor wiith AOT=true

Reply to this Comment

@Ben,

Unfortunately, I didn't. I tried to look into Angular Router module code, that uses forRoot(), but it is too hard to understand. Anyway, if I start the server with "--aot" option, it works. I really don't understand how it is written...

Reply to this Comment

@Alexandro,

Ha ha, I can relate. Sometimes, you just gotta get something to "Work" and then move onto more important things, like writing the application and providing value for your users. Understanding the intricacies of low-level compilation stuff is a bit of luxury. I'll defer that kind of stuff to Max Koretskyi, who is an expert in digging into the really low-level nitty-gritty stuff.

Reply to this Comment

@Ben,
I met a really good angular developer and we took a look to my code. In my libraries services, I directly Inject the InjectionToken. However, we discovered the error was the presence of index.ts files, used as the entry points for modules, services, models, etc. We have deleted all index.ts files, adjusted the public_api.ts, rebuilt and published all modules to NPM. Now, every library work with AOT in Angular 7/8!

Reply to this Comment

I tried using your jit version but ModuleOptions gives me an error saying TS2304: Cannot find name 'ModuleOptions'.

Reply to this Comment

@Harsh,

Hmm, that's interesting. Are you importing ModuleOptions into the context in which you are using it as a Type?

Reply to this Comment

@Alexandro,

Ah, very cool! I don't know much about the use of index.js files. I know they are sometimes referred to as "barrels"; at least, in the Angular community. But, I have very little experience actually publishing anything.

Reply to this Comment

@Ben,

Thank you for this great tutorial!

We encountered the same problem as @Alexandro in our code.
We provide the MyServiceModule in an angular library which is published to a npm repo and have a dependency to the library in our package.json/code.
During build time of our applicatione we got an error like:

ERROR in Error during template compile of 'AppModule'
  Function calls are not supported in decorators but 'MyServiceModule' was called.

The problem was the kind we imported the MyServiceModule from the library.
We imported it through a my.service.module.ts file (initially we did not want do expose all components through the main entry point in our library):

import { MyServiceModule } from '@company/libray/lib/my.service.module';

The fix for us was to add the module export also to the public_api.ts file of the library and imported it from there:

import { MyServiceModule } from '@company/libray';

Everythings works now. Switching to the old "import style" will arise again the mentioned error.

Btw: our democode, which was placed in the same sourcecode location as the library, worked well and compiled without errors. The problem occured only if we used the compiled library through node_modules folder in our application.

Reply to this Comment

@Remsy,

That's really interesting. And, unfortunately, it goes a bit over my head. I really have no hands-on experience with publishing a library from a TypeScript context. Frankly, I'm not even sure exactly how .t.ds files work and how TypeScript figures out where all that stuff is. Sometimes, I'll poke around in an existing .t.ds file, and a lot of the notation and organization trips my brain up. As such, I appreciate you all sharing your solutions for others who run into the same issue.

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.