4

I'm trying to get some configuration options from the database before loading my angular 2 app. The angular app needs to wait for the data being loaded before starting because access to certain components should be restricted based on these configuration options.

So I've created a ConfigurationService to get the data from the backend and I'm trying to use the app_initializer in the root module to call the configurationservice before starting the app. I've managed to do this in an angular app I've created a few months ago (angular RC4) and tried to do It the same way this time around at first.

app.module.ts - First try

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { HttpModule } from '@angular/http';   

import { ConfigurationService } from "./shared/services/configuration.service";

@NgModule({
    imports: [
        ...
    ],
    declarations: [
        ...   
    ],    
    providers: [
        {
            provide: APP_INITIALIZER,
            useFactory: (config: ConfigurationService) => () => config.loadConfiguration(),
            deps: [ConfigurationService]
        },                 
        ConfigurationService        
    ],
    bootstrap: [AppComponent]
})

export class AppModule { }

This resulted in the following error:

Error encountered resolving symbol values statically. Function calls are not supported. Consider replacing the function or lambda with a reference to an exported function

I think this has something to do with the AoT compiler, which I'm using. I've replaced the lambda expressions with a factory.

app.module.ts - Second try

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { AppComponent } from './app.component';
import { RouterModule } from '@angular/router';
import { HttpModule } from '@angular/http';   

import { ConfigurationService } from "./shared/services/configuration.service";
import { ConfigurationServiceFactory } from "./shared/services/configuration.service.factory";

@NgModule({
    imports: [
        ...
    ],
    declarations: [
        ...   
    ],    
    providers: [
        {
            provide: APP_INITIALIZER,
            useFactory: ConfigurationServiceFactory,
            deps: [ConfigurationService]
        },                 
        ConfigurationService        
    ],
    bootstrap: [AppComponent]
})

export class AppModule { }

configuration.service.factory.ts

import { ConfigurationService } from "./configuration.service";

export function ConfigurationServiceFactory(configurationService: ConfigurationService) {  
    return configurationService.loadConfiguration()
        .then(currentUser => {
                configurationService.currentUser = currentUser;                
            }
        );    
}

This actually gets the data from the backend using the configurationservice, but doesn't wait for the promise to resolve before starting the app. It works when the user starts at the homepage (which is accessible for everyone) and then navigates to a component which is restricted for non admin users. But when the user refreshes the page which is restricted, the OnInit (where I determine if a user has access) fires before the configuration can be loaded. Which, in my case, results in users to be denied access to pages they should have access to.

configuration.service.ts

import { Injectable } from "@angular/core";
import { Http, Response } from '@angular/http';

import { CurrentUser } from "../models/currentuser.model";

@Injectable()
export class ConfigurationService {
    public apiUrl: string = "api/";
    public currentUser: CurrentUser;

    constructor(private http:Http) {}

    /**
    * Get configuration from database
    */
    public loadConfiguration(): Promise<CurrentUser> {
        return this.http.get(this.apiUrl + "application/GetDataForCurrentUser")
            .toPromise()
            .then(this.extractData)
            .catch(this.handleError);
    }       
}

Component implementation

import { Component, OnInit } from '@angular/core';
import { MessageService } from '../shared/message/message.service'
import { ConfigurationService } from "../shared/services/configuration.service";

@Component({    
    selector: 'simple',
    templateUrl: 'simple.component.html'
})
export class SimpleComponent implements OnInit{ 
    constructor(private messageService: MessageService, private configurationService: ConfigurationService) {

    }
    ngOnInit() {                      
        if (!this.configurationService.isAdmin()) {            
            this.messageService.addMessage("Access denied", "danger");
        }
    }
}

I'm all out of ideas at this time, I've looked for a solution but only managed to find people recommending the lambda option, which is not working for me.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Jop
  • 406
  • 4
  • 10

1 Answers1

5

With the help of @DzmitryShylovich on the angular gitter channel, who pointed out that calling the service from inside the app.module.ts was to late and I should call the service from within the main.ts file instead, I managed to solve my problem. He provided me with the folowing plnkr.

This is what I've done in my own solution to get It to work.

constants.ts

import { OpaqueToken } from '@angular/core'

export const CONFIG_TOKEN = new OpaqueToken('config');

I've create an OpaqueToken to avoid naming collisions

main.ts

import { platformBrowser } from '@angular/platform-browser';
import { AppModuleNgFactory } from '../aot/app/app.module.ngfactory';
import { CONFIG_TOKEN } from './shared/constants/constants'

const configApi = "yoururlhere";
fetch(configApi, {
    credentials: 'include'
}).then(parseResponse);

function parseResponse(res: any) {    
    return res.json().then(parseData);
}

function parseData(data: any) {    
    return platformBrowser([{ provide: CONFIG_TOKEN, useValue: data }]).bootstrapModuleFactory(AppModuleNgFactory);
}

In my main.ts file I call the webservice to get my configuration data, parse the data and create a provider to pass into my app. I set the credentials to include because I'm using windows authentication on the webservice which will give a 401 when you don't explicitly tell the fetch to provide credentials.

app.component.ts

import { Component, OnInit, Inject } from "@angular/core";
import { CONFIG_TOKEN } from './shared/constants/constants'

@Component({
    selector: "app",
    templateUrl: "app.component.html"
})

export class AppComponent implements OnInit {
    public configuration: any;
    public isAdmin: boolean;
    constructor(@Inject(CONFIG_TOKEN) config: any) {        
        this.configuration = config;       
    }

    ngOnInit() {     
        this.isAdmin = this.configuration.currentUser_IsAdmin;
    }    
}

In my app.component I can inject my created provider to use the configuration object I got from the webservice.

Typescript issue

I ran into an typescript issue in visual studio when developing this solution, I got the following error:

error TS2304: Cannot find name 'fetch'.

I solved this by manually downloading the whatwg-fetch typescript definition file

npm install --save-dev @types/whatwg-fetch

@DzmitryShylovich, thank you for your help!

Jop
  • 406
  • 4
  • 10
  • How do you get around duplicate identifier errors when pulling in @types/whatwg-fetch? It seems like lib.d.ts includes fetch typings now, so there are collisions when adding the typings manually. – Sam Storie Mar 07 '17 at 15:41