martinoss
martinoss

Reputation: 5458

Initialize other provider after APP_INITIALIZE

I have a little bootstrapping problem using Angular 4. I have a config file (json) that I'm loading from the server as my application starts (see also here). This works so far.

Now I have a dependency that requires config values to be passed in a forRoot method. The implementation looks like this:

static forRoot(config: AppInsightsConfig): ModuleWithProviders {
    return {
        ngModule: ApplicationInsightsModule,
        providers: [
            { provide: AppInsightsConfig, useValue: config }
        ]
    };
}

My Idea was to import the module without forRoot(), but providing the AppInsightsConfig by after the configs (from server) have been loaded.

@NgModule({
    bootstrap: sharedConfig.bootstrap,
    declarations: [...sharedConfig.declarations],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        ApplicationInsightsModule,
        ...sharedConfig.imports        
    ],
    providers: [
        { provide: 'ORIGIN_URL', useValue: location.origin },
        { provide: 
            APP_INITIALIZER, 
            useFactory: (configService: ConfigService) => () => configService.load(), 
            deps: [ConfigService], multi: true 
        },
        { provide: 
            AppInsightsConfig, 
            useFactory: (configService: ConfigService) => { 
                // outputs undefined, expected it to have the config
                console.log(configService.config); 
                return { instrumentationKey: 'key from config' } 
            },
            // My idea was, if I add APP_INITIALIZER as dependency,
            // the config would have been loaded, but this isn't the case.
            deps: [APP_INITIALIZER, ConfigService] 
        },
        AppInsightsService
    ]
})

How can I provide AppInsightsConfig or another service AFTER my configs have been loaded?

Upvotes: 11

Views: 8009

Answers (2)

user2977624
user2977624

Reputation: 103

I'm late to the game here, but I have the same issue and I found an elegant solution. Use method injection instead of constructor injection.

  1. Declare your service that needs the config data like so: (note that it has a paramaterless constructor)

export class SomethingService {
    public config!: AppInsightsConfig;
}
export const SOMETHING_SERVICE = new InjectionToken<SomethingService>("SOMETHING_SERVICE");
export const somethingService = new SomethingService();

  1. Register the SomethingService in the main.ts like so: (this runs BEFORE app initialization)

import { SOMETHING_SERVICE, somethingService } from "..path to somethingService.ts";
platformBrowserDynamic([
        { provide: SOMETHING_SERVICE, useValue: somethingService }
    ])
    .bootstrapModule(AppModule)
    .catch(err => console.error(err));

  1. Now, during your app initialization process you're able to inject the somethingService into your config service:

export class ConfigService {
   connstructor(@Inject(SOMETHING_SERVICE) private readonly somethingService: SomethingService){}

   load(config: AppInsightsConfig){
      this.somethingService.config = config;
   }
}

  1. When your app initialization factory function is invoked, pass the instance of the AppInsightsConfig class into the load function:

let loadConfigPromise = configService.load(config : AppInsightsConfig);

And, like magic, whenever you inject the SomethingService elsewhere, the load function will have already ran since it was invoked during app initialization and the config variable will be assigned a value!

Upvotes: 0

martinoss
martinoss

Reputation: 5458

Finally, I came up with following solution:

  • I merged the sources of the angular-application-insights package (was not my original intention) to my code base to be able to alter things that made it hard for me to do the bootstrapping how I wanted to. I also fighted against cyclic dependencies. Thanks to MarkPieszak for sharing your library.

This is how my app-module looks like now:

import { Resolve, Router } from '@angular/router';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { sharedConfig } from './app.module.shared';

import { SiteControlModule } from './site-control/site-control.module';
import { SiteControlComponent } from './site-control/site-control.component';
import { AuthModule } from './auth/auth.module';

import { ConfigService } from './core/config/config.service';

import { ApplicationInsightsModule } from './application-insights/application-insights.module';
import { ApplicationInsightsService } from './application-insights/application-insights.service';

import { CoreModule } from './core/core.module';

let initializeConfig = (aiService: ApplicationInsightsService, configService: ConfigService) => () => { 
    let loadConfigPromise = configService.load(); 

    loadConfigPromise.then(() => {
        aiService.config = configService.config.applicationInsights;
        aiService.init();
    });

    return loadConfigPromise; 
};


@NgModule({
    bootstrap: sharedConfig.bootstrap,
    declarations: [...sharedConfig.declarations],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        AuthModule,
        ...sharedConfig.imports,
        CoreModule.forRoot(),
        ApplicationInsightsModule.forRoot(),
        SiteControlModule
    ],
    providers: [
        { provide: 'ORIGIN_URL', useValue: location.origin },
        { provide: 
            APP_INITIALIZER, 
            useFactory: initializeConfig, 
            deps: [ ApplicationInsightsService, ConfigService ], 
            multi: true 
        }     
    ]
})
export class AppModule {    
}

config.service.ts

import { Injectable } from '@angular/core';
import { Headers, RequestOptions,  Http, Response} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/toPromise';

import { Config } from './models/config.model';

@Injectable()
export class ConfigService {

    config : Config;

    constructor(private http: Http) {
        console.log('constructing config service');
    }

    public load(): Promise<void> {
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let url = 'environment.json';

        let promise = this.http.get(url, { headers })
            .toPromise()
            .then(configs => { 
                console.log('environment loaded');
                this.config = configs.json() as Config; 
            })
            .catch(err => { 
                console.log(err); 
            });

        return promise;
    }

}

Altered part of the application-insights.service.ts

import { Injectable, Optional, Injector } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { AppInsights } from 'applicationinsights-js';
import 'rxjs/add/operator/filter';
import IAppInsights = Microsoft.ApplicationInsights.IAppInsights;

import { ApplicationInsightsConfig } from '../core/config/models/application-insights.model';

@Injectable()
export class ApplicationInsightsService implements IAppInsights {

    context: Microsoft.ApplicationInsights.ITelemetryContext;
    queue: Array<() => void>;
    config: Microsoft.ApplicationInsights.IConfig;

    constructor(@Optional() _config: ApplicationInsightsConfig, private injector: Injector) {
        this.config = _config;
    }

    private get router(): Router {
        return this.injector.get(Router);
    }

    ...

application-insights.module.ts

import { NgModule, ModuleWithProviders, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ApplicationInsightsService} from './application-insights.service';

export * from './application-insights.service';

@NgModule({
    imports: [ CommonModule ],
    declarations: [],
    exports: [],
    providers: []
})

export class ApplicationInsightsModule {

    constructor(@Optional() @SkipSelf() parentModule: ApplicationInsightsModule) {
        if (parentModule) {
            throw new Error(
                'ApplicationInsightsModule is already loaded. Import it in the AppModule only');
        }
    }

    static forRoot(): ModuleWithProviders {
        return {
            ngModule: ApplicationInsightsModule,
            providers: [ ApplicationInsightsService ]
        };
    }    
}

Upvotes: 7

Related Questions