Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 231
martinoss Avatar asked Jun 23 '17 07:06

martinoss


1 Answers

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 ]
        };
    }    
}
like image 173
martinoss Avatar answered Sep 28 '22 23:09

martinoss