13

Our Angular 12 app has a module that imports a dependency we want to configure based on a configuration file that's only available in runtime, not in compile-time.

The package in question is ng-intercom, although I imagine the same issue could come up later with other packages as well.

The motivation behind the dynamic configuration is that our app runs in 4 different environments and we don't want to create separate builds for each, since the only difference between them is the configuration file which contains the backend's URL and a few Application IDs (like Intercom, Facebook app ID, etc.)

This is how the import in question is made currently:

imports: [ ... IntercomModule.forRoot({ appId: env.intercomID, updateOnRouterChange: true, }), ... 

The issue is that appID should be configurable, the env variable should be loaded dynamically. Currently, it's a JSON imported and compile into the code, but that means we can't change it for our different environments without rebuilding the code for each:

import env from '../../assets/environment.json'; 

We have an APP_INITIALIZER, however, that doesn't stop modules from being imported before it's resolved:

{ provide: APP_INITIALIZER, useFactory: AppService.load, deps: [AppService], multi: true, }, 

... and the relevant configuration loader:

static load(): Promise<void> { return import('../../assets/environment.json').then((configuration) => { AppService.configSettings = configuration; }); } 

We can use this configuration without issues with our components and services.

We managed to achieve the results we want in angularx-social-login's configuration:

providers: [ ... { provide: 'SocialAuthServiceConfig', useValue: new Promise(async resolve => { const config = await AppService.config(); resolve({ autoLogin: true, providers: [ { id: FacebookLoginProvider.PROVIDER_ID, provider: new FacebookLoginProvider(config.facebookApi), } ] } as SocialAuthServiceConfig); ... ] 

SocialAuthServiceConfig is a provider however, we couldn't find a way to configure ng-intercom's import similarly.

Can we achieve this somehow? Is there a way to dynamically configure module imports?

4
  • Did you try using env variables? and config as appId: proccess.env.INTERCOM_ID! Commented Sep 21, 2021 at 13:25
  • You can load anything at runtime before bootstrapping Angular app like this stackoverflow.com/questions/56431192/… See also stackoverflow.com/questions/54469571/… Commented Sep 21, 2021 at 18:59
  • @strdr4605 We originally planned to use environment variables instead of a configuration file, however, in our environment that would've been harder to configure, and at this point we'd rather not split up our configurations between environment variables and the configuration file. Commented Sep 22, 2021 at 14:01
  • @yurzui Thanks, sadly I couldn't make it work this way, I injected my configuration into the extraProviders parameter of platformBrowserDynamic, however I didn't find a way to actually access it in the @ NgModule configuration, it was still, always undefined. My suspicion is that the @ NgModule configuration actually gets evaluated when it's imported into the main.ts file (which contains the bootstrapModule() logic), but I could be wrong. Because of this I went with the other solutions. Commented Sep 22, 2021 at 14:07

2 Answers 2

5
+500

I think there is no need to configure the module imports dynamically to achieve that, instead, you can do the following:

  • Import the IntercomModule without the forRoot function.
  • Provide the IntercomConfig class using useFactory function that read the data config from the AppService.configSettings:
 providers: [ { provide: IntercomConfig, useFactory: intercomConfigFactory, }, ], // .... export function intercomConfigFactory(): IntercomConfig { return { appId: AppService.configSettings.intercomAppId, updateOnRouterChange: true, }; } 
Sign up to request clarification or add additional context in comments.

4 Comments

Your code will only work when AppService.configSettings.intercomAppId is already available. But is is actually loaded later - as OP said.
Sorry, my bad. I updated my answer to use the factory instead of the value. And the AppService.configSettings.intercomAppId should be available within the factory since it's already assigned within the APP_INITIALIZER promise
@Amer Thank you, your solution worked perfectly and we used it to configure our other dependencies like angularx-social-login and angular-google-tag-manager as well!
dont forget import line imports: [ IntercomModule ]
1

most library authors provide a way to initialize the library later. The forRoot call can be faked. If you need to configure intercom, you can still call forRoot but you can use empty id:

 IntercomModule.forRoot({ appId: null, updateOnRouterChange: true, }), 

Then you can call boot with app_id which is then used.

 // AppComponent constructor(private appService: AppsService, private intercom: Intercom) { this.startIntercom(); } private async startIntercom() { const config = this.appService.config(); this.intercom.boot({app_id: config.intercom_app_id}); } 

In general you can learn a lot by reading the library source code. Most libraries provide methods similar to intercom.boot.

4 Comments

this.intercom.boot will not overwrite the config.appId value, and the other functions use the config.appId directly without reading the one passed to the boot method e.g. loadItercom
You are not correct, boot is the only place that appId is actually read. Just search for it in the code.
I already mentioned the function that is using the config.appId directly :)
@kvetis Thanks for the tip, this solution would've worked perfectly for ng-intercom! I accepted the other answer however, since that was immediately reusable for other modules, without diving into the library's source code.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.