12

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?

Dyin
  • 5,815
  • 8
  • 44
  • 69
barney.balazs
  • 630
  • 7
  • 19
  • Did you try using env variables? and config as `appId: proccess.env.INTERCOM_ID`! – strdr4605 Sep 21 '21 at 13:25
  • You can load anything at runtime before bootstrapping Angular app like this https://stackoverflow.com/questions/56431192/inject-default-firebase-config-into-angular-app/56431386#56431386 See also https://stackoverflow.com/questions/54469571/angular-load-external-configuration-before-appmodule-loads – yurzui Sep 21 '21 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. – barney.balazs Sep 22 '21 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. – barney.balazs Sep 22 '21 at 14:07

2 Answers2

5

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,
  };
}
Amer
  • 6,162
  • 2
  • 8
  • 34
  • Your code will only work when `AppService.configSettings.intercomAppId` is already available. But is is actually loaded later - as OP said. – kvetis Sep 21 '21 at 16:17
  • 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 Sep 21 '21 at 16:47
  • 1
    @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! – barney.balazs Sep 22 '21 at 13:53
  • dont forget import line imports: [ IntercomModule ] – Ahmet Uğur Jan 03 '22 at 14:00
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.

kvetis
  • 6,682
  • 1
  • 28
  • 48
  • `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](https://github.com/CaliStyle/ng-intercom/blob/4d9d574f6b1dc427478562c1dad7026ef3961b31/src/app/ng-intercom/intercom/intercom.ts#L242) – Amer Sep 21 '21 at 16:02
  • You are not correct, `boot` is the only place that `appId` is actually read. Just search for it in the code. – kvetis Sep 21 '21 at 16:17
  • I already mentioned the function that is using the config.appId directly :) – Amer Sep 21 '21 at 16:21
  • @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. – barney.balazs Sep 22 '21 at 13:59