Providing Module Configuration Using forRoot() And Ahead-Of-Time Compiling In Angular 7.2.0
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:
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.
Want to use code from this post? Check out the license.
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.
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...
@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?
@Vivek,
I've never seen that kind of error :( I hope you were able to get it solved.
@Ben,
Hey,
Yes, I did solve it.
The issue was a console.log in the ChildComponent's constructor wiith AOT=true
@Vivek,
So funky! Glad you got it -- sorry I wasn't able to help :D
@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...
@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.
@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!
I tried using your jit version but ModuleOptions gives me an error saying TS2304: Cannot find name 'ModuleOptions'.
@Harsh,
Hmm, that's interesting. Are you importing
ModuleOptions
into the context in which you are using it as a Type?@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.@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:
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):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: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.
@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.Hello, i would like to now how we install the configuration module
@Lukayi,
I am not sure what you mean by "configuration module?"
I am trying to create a angular custom library. In this library there are 2 lazy loaded modules.
In Application SRC, we can provide routing for lazy loaded module with forRoot in root-module-routing.ts file, and we can provide the module specific routing with module routing file having forChild.
But in library case, I am trying same
@Shivam,
I'm sorry, I don't have any experience in building 3rd-party libraries for Angular -- I've only ever built modules that are defined and consumed within the same app. As such, I'm not sure that I understand what is going on.
That said, it is my understanding that the
.forRoot()
and.forChild()
static methods on a module are just conventions, not framework-level constraints. As such, it seems odd that you'd get an error message telling you how to use these methods. The idea is that.forRoot()
provides the Declarations + Services, and then the.forChild()
provides just the Declarations (since the Services should already be available via the.forRoot()
call).But, since I don't really build libraries, I don't have good instincts for that stuff.
Instead of using a
class
for the options, use an object with an InjectionTokenconst DEFAULT_OPTIONS = {
a: 1,
b: 2
}
static forRoot( options?: ModuleOptions ) : ModuleWithProviders {
.....
{
provide: FOR_ROOT_OPTIONS_TOKEN,
useValue: {DEFAULT_OPTIONS, ...options}
}
}
In the service inject the FOR_ROOT_OPTIONS_TOKEN, with a better name though:-)
This way you get around having to work with classes and constructors, and can treat the config as they are usually treated, as a simple object
@Brian,
I'm so wrong here! I just tried to build my library with ng serve lib --prod, and had to come back here as I'm getting the exact errors mentioned. Talk about speaking too soon:-) Great article btw
@Brian,
Ha ha, no problem, I do that all the time ;)