13

I am using APP_INITIALIZER like it is recommended in this answer, with my service returning a promise, but it doesn't always wait for it to resolve and I can see my component console.logging undefined and then the service logging the downloaded object.

I need the app to not do anything before this data is loaded.

app.module.ts

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { Http, HttpModule, JsonpModule } from '@angular/http';
import { UserService } from '../services/user.service';

<...>
@NgModule({
  imports: [
    BrowserModule,
    HttpModule,
    FormsModule,
    JsonpModule,
    routing
  ],
  declarations: [
    AppComponent,
    <...>
  ],
  providers: [
    <...>
    UserService,
    {provide: APP_INITIALIZER,
      useFactory: (userServ: UserService) => () => userServ.getUser(),
      deps: [UserService, Http],
      multi: true
    }
  ],
  bootstrap: [AppComponent]

user.service.ts

@Injectable()
export class UserService {

    public user: User;
    constructor(private http: Http) { }

    getUser(): Promise<User> {
        console.log('get user called');
        var observable= this.http.get('/auth/getuser', { headers: getHeaders() })
            .map(extractData);

        observable.subscribe(user => {this.user = user;
            console.log(this.user)});
        return observable.toPromise();
    }
}
Community
  • 1
  • 1
LLL
  • 3,566
  • 2
  • 25
  • 44
  • It looks ok. Please, state which package versions you are using. Are you able to replicate the issue as a fiddle/plunk? – Estus Flask Oct 01 '16 at 01:32
  • I am not sure why you are getting logs with undefined values, but if you use both `toPromise()` and `subscribe()` on observable you will call `/auth/getuser` twice. You should chain also rxjs operator `share()` (`rxjs/add/operator/share`) – Michal Moravcik Feb 23 '17 at 00:57

4 Answers4

19

Try the following code:

getUser(): Promise<User> {
    console.log('get user called');
    var promise = this.http.get('/auth/getuser', {headers: getHeaders()})
        .map(extractData)
        .toPromise();
    promise.then(user => {
        this.user = user;
        console.log(this.user);
    });
    return promise;
}

I was facing the same problem, and using a promise instead of a observable did the trick for me.

Remidy
  • 351
  • 1
  • 5
  • Hey, I made it work quite some time ago actually, but this is exactly what I did. – LLL Oct 26 '16 at 10:09
  • I think observables get triggered at the first change of the object, which does not contain all response data, while promises are triggered when the whole task is done. Voted Up as this solved the issue for me as well. – Desislav Kamenov Mar 06 '17 at 12:46
  • @DesislavKamenov, No, it's "by design" :) Read this thread: https://github.com/angular/angular/issues/9047 – rook Mar 23 '17 at 02:37
  • I would give you 200 points if I could! Brilliant!! Thank you!! – CodyBugstein Mar 08 '18 at 04:16
3

I think the problem is caused because you subscribe to the observable. This should work

@Injectable()
export class UserService {

    public user: User;
    constructor(private http: Http) { }

    getUser(): Promise<User> {
        console.log('get user called');
        return observable= this.http.get('/auth/getuser', { headers: getHeaders() })
            .map(extractData)
            .do(user => {
                this.user = user;
                console.log(this.user)
             })
            .toPromise();
    }
}

I'm not sure if toPromise() is necessary. I'd expect it to work with Observable as well.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
2

Guard your route with a CanActivate class using a Promise which loads the config settings should also work.

Use the appSettings.service with a function just like the one returning a promise

getAppSettings(): Promise<any> {
        var observable = this.http.get(this.ApiUrl, { headers: this.headers })
            .map((response: Response) => {
                var res = response.json();
                return res;
            });

        observable.subscribe(config => {
        this.config= config;
        console.log(this.config)
        });
        return observable.toPromise();  
    }

And the CanActivate guard as below:

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AppSettingsService } from './appsettings.service';

@Injectable()
export class CanActivateViaAuthGuard implements CanActivate {

//router: Router
    constructor(private appSettingsService: AppSettingsService)
    {
    }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
               return this.appSettingsService.getAppSettings().then(() => { 
    return true });
   }
}

This will ensure that your settings are available when the corresponding components are constructed. (using the APP_INITIALIZER did not restrict the constructor being called, so I had to use this technic, Also please make sure, you dont export all the components in the exports:[] in the module)

To guard the routes and ensure settings are loaded before the constructors are called, please use the usual canActivate option in the path for the route definition

 path: 'abc',
 component: AbcComponent,
 canActivate: [CanActivateViaAuthGuard]

The initialization of appsettings should happen before the constructor for AbcComponent is called, this is tested and works in Angular 2.0.1

I'm not sure if it is the right place to load config but seems to serve the purpose

abhijoseph
  • 307
  • 1
  • 3
  • 5
0

Late, but if you want to keep your Service class returning Observables (I do), call it like this in your App Module class:

function authenticationFactory(service: AuthenticationService) {
  console.log("calling login");
  //Call auth service login to get JWT info and startup data.
  //Also convert from an Observable to a Promise to work with APP_INITIALIZER.
  return () => service.login().toPromise().then(/*do nothing here*/);
}

NgModule Metadata stuff...

providers: [
   ...
    AuthenticationService,
    {
      provide: APP_INITIALIZER,
      useFactory: authenticationFactory,
      deps: [AuthenticationService],
      multi: true
    },
    ...
  ],
Chris Moore
  • 475
  • 1
  • 5
  • 12