Wait for http inside Guard on Angular 5
Asked Answered
I

2

10

I'm using a Guard on an Angular application to resolve initial critical data. On the version 4 of Angular I was duing it like this:

// app.routing.ts
routing = [{
    path: '', component: AppComponent, canActivate: [ResolveGuard],
}];

// resolve.guard.ts
@Injectable()
export class ResolveGuard implements CanActivate {
    constructor(
        private _api: ApiService,
    ) { }

    canActivate(): any {
        return this._api.apiGet('my/url').map(response) => {
            if ( response.status === 'success') {
                // Consume data here
                return true;
            }

            return false;
        }).first();
    }
}

Since the new version of Http on Angular 5 doesn't use the .map() property anymore, this is not working.

If I change .map() to .subscribe() it doesn't throw any errors, but the application never resolve properly. On the other hand, using .first() and/or .map() throw some errors, as expected in this version.

What should I do in this case?

I need to activate that route only if and when the initial data is loaded.


Edit to add info about the apiGet function:

constructor(private _http: HttpClient) {}

public apiGet(url: string): any {
    return this._http
        .get(this.apiUrl + url)
        .catch(this.handleError.bind(this));
}
Interpretation answered 9/11, 2017 at 20:25 Comment(4)
what do you mean by doesnt use the map() method anymore? o.OHandkerchief
Please add the definition of you apiGet method, otherwise is hard to tell what you could doHandkerchief
@Handkerchief I meant to say it's no longer needed on the new Http client module (If i understood correctly). Also, I've updated the question with the apiGet function, but it's a service to call the http functions.Interpretation
have you checked the answers?Handkerchief
H
17

So, first thing first: avoid the use of any when possible, specially when you know which type belongs where.

export interface FooInterface {
  status: string;
  fooString : string;
  fooNumber : number;
}

First Ill define the interface of the service in a clean manner, and then Ill refactor the guard class.

UPDATED ANSWER for rxjs 6.x

import { throwError, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class FooService {
   constructor(private _http: HttpClient) {}

   public apiGet(url: string): Observable<FooInterface> {
    return this._http
        .get<FooInterface>(this.apiUrl + url)
        .pipe(
          catchError(error => {
             // do general error handling if necessary and throw
            throwError(error);
           })
        );
  }
}

The guard class:

import { of, Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

@Injectable()
export class ResolveGuard implements CanActivate {
constructor(
    private fooService: FooService ,
) { }

canActivate(): Observable<boolean> {
    return this.fooService.apiGet('my/url')
       .pipe(
         map(response => response.status === 'success'),
         catchError(error => of(false))
       );
}

ORIGINAL ANSWER for rxjs 5.x

import { _throw } from 'rxjs/observable/throw':

constructor(private _http: HttpClient) {}

public apiGet(url: string): Observable<FooInterface> {
    return this._http
        .get<FooInterface>(this.apiUrl + url)
        .catch(error => {
          // do general error handling if necessary and throw
          _throw(error);
       });
}

The guard class:

import { of } from 'rxjs/observable/of';

@Injectable()
export class ResolveGuard implements CanActivate {
constructor(
    private _api: ApiService,
) { }

canActivate(): Observable<boolean> {
    return this._api.apiGet('my/url')
       .map(response => {
          let val = false;
          if ( response.status === 'success') {
                // Consume data here
                val = true;
          }
          return val;
        }).catch(error => {
          // consume the error maybe?
          of(false)
        });
}
Handkerchief answered 9/11, 2017 at 21:2 Comment(4)
hi , can u please explain your code , like why using map in place of subscribe , using of() and other such things.Stumpage
Internally, angular subscribes to the observable returned by the guard. For that reason, the only thing that the guard needs to do is to transform the observable returned by the api service into a boolean stream. In the case that the source observable, the HTTP request, fails,the guard still has to return a stream, not throw. Thats why I use of to map the error to a new stream. @StumpageHandkerchief
oh ok , but there are various cases where if we try to call a method from inside the AuthGuard (which basically calls a service) , the AuthGuard does not seems to wait for the response of the service, can u tell why ?Stumpage
"various cases" is too vague. Depends on what you are actually doing. Feel free to open a new question and tag me on it.Handkerchief
G
3

just import map operator and it will work :

import { Observable } "rxjs/Observable"; 
import "rxjs/add/operator/map"; 

 canActivate(): Observable<boolean>{
        return this._api.apiGet('my/url').map(response => {
            if ( response.status === 'success') {
                // Consume data here
                return true;
            }
            return false;
        });
    }
Galactometer answered 9/11, 2017 at 20:59 Comment(3)
why first() at the end?Handkerchief
Your code is just missing "from" on the Observable import, but it's working. Also, on the Angular 5 release blog post it says I should import the map like this: import { map } from 'rxjs/operators'; but it's not working. It just work when I import the old way. Maybe this is what was wrong with my code (?)Interpretation
@Interpretation read the article carefully and complete, there is a difference on how to use the elements from 'rxjs/operators'Handkerchief

© 2022 - 2024 — McMap. All rights reserved.