Sometimes your Angular application needs to be a little bit different depending on the environment. Maybe your local development build has some API services stubbed out. Or you have two production destinations with a little bit different behavior. Or you have some other build-time configuration or feature toggles.
The differences can be on any level: services, components, suites of ngrx/effects, or entire modules.
For example, it could look like this:
The dependency injection and module mechanism in Angular is pretty basic, and it does not seem to offer much to answer such use cases. If you want to use ahead-of-time (AOT) compilation (and you should!), you can’t just put random functions in module definition. I found a solution for doing it one service at a time, but it’s pretty ugly and not flexible enough. The problem does not at all seem uncommon to me, though, so I wanted to describe a nice little trick to work around this.
Sample app
Let’s study this on a sample app. To keep things interesting, let’s make a realistic simulator of various organization hierarchies.
We’ll need a component to tell us who rules the “organization”:
As well as a service:
On a picture it could look like this:
Now, let’s say we have two environments:
- Playground, where little Steve would really like to own the place… except that he can only do that when the local bully is not around, and that’s only when he’s eating. To determine the ruler we need to ask the Bully’s mother about his status. So it goes.
- Wild West, where the rules are much simpler in comparison: It’s sheriff who runs the show, period.
In other words, we need something like this:
So, how to achieve that?
Solution
The solution is actually pretty straightforward. All it takes is a little abuse (?) of Angular CLI environments. Long story short, this mechanism provides different environment file to the compiler based on a compile-time flag.
If you check the documentation or pretty much any existing examples, the environments are typically used to provide different configuration. Just a single object with a bunch of properties. You might be tempted to use it in some functions on module definition, but it’s not probably not going to work with AOT. Oops.
However, at the end of the day it’s just a simple file substitution. You can put whatever you like in the file, and as long as it compiles everything is going to be OK. Classes. Exports from other files. Anything.
In our case, the AppModule
can import the RulersModule
from the environment. It doesn’t care much what the module actually contains.
The environments would export it from the relevant “package”. They could have the classes inline, but I prefer to keep them in separate files closer to the application.
Now, the modules:
The final trick that makes these work is that there is an abstract class/interface for the RulersService
, and AppComponent
(or other services, if we had them) only depends on that. Actually, this pattern has been around for a while. This is the old good programming to interfaces.
The same goes for the RulersModule
: In a sense it’s abstract too, AppModule
doesn’t know or care what concrete class it is. As long as the symbol with the same name exists during compilation.
This sample demonstrates it on the module level, but you can use the same trick for any TypeScript code, be it a component, a service, etc.
I am not sure if such use of environments is ingenious, or insane and abusive. I have not seen this anywhere. However, it does solve a real problem that does not seem to have a solution in the Angular toolbox (and it’s not particularly toxic).
Sample app on GitHub
Check out this sample app at my GitHub repository.
Credits
The idea comes from Kyle Cordes. I only made it work.
I ran across this trying to figure out how to conditionally configure HTTP_INTERCEPTORS providers for dev and prod. I ran into an issue with circular dependencies. Using your example, what if BullysMotherService used a LogService, which in turn imported environment to determine the log level:
environment (reall environment.playground )-> PlaygroundModule -> BullysMotherService -> LogService -> environment.
Its a compiler warning, not an error. However, from what I gather, this might cause a problem with the dependency injector, and even if not, is generally something to be avoided. Moreover, its placing the configuration IN the environment definition, which isn’t really great for separation of concerns.
Ideally, we could specify multiple environment specific files , not just environment.ts, something like this in .angular-cli.json:
“environments”: {
“dev”: [ “environments/environment.ts”, app/app-config/config.dev.ts”]
“prod”: [ “environments/environment.prod.ts”, app/app-config/config.prod.ts”]
}
Then environment could be used as it typically is, but app-module could import app-config.
My workaround (which works, at least for providers), is simply to add the following in app-module.ts:
const COMMON_PROVIDERS : Provider[] = [ /* common providers here */];
const DEV_PROVIDERS: Provider[] = COMMON_PROVIDERS.concat( /* dev specific providers here */);
const PROVIDERS : Provider[] = env.production ? COMMON_PROVIDERS : DEV_PROVIDERS;
then in the NgModule decorator, simply:
providers: PROVIDERS,
This seems to be simple enough to appease the AOT compiler.
Not sure if that would also apply to modules.