6

I'm using route guards, specifically the canActivate() method, but Angular is calling ngOnInit() of my root AppComponent before canActivate is called.

I have to wait on some data in canActivate before the AppComponent can render it in the template.

How can I do this?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Kingamere
  • 9,496
  • 23
  • 71
  • 110
  • 5
    `canActivate` gets called to find out if the relevant route component loads into the outlet, not the root app component. If you need something to happen before the app starts, you'll need to use `APP_INITIALIZER`. – jonrsharpe Jun 07 '18 at 18:25
  • 6
    `AppComponent` is the default bootstrapped component. It does not wait for any routes to activate. – Amit Chigadani Jun 07 '18 at 18:27
  • You want to make some asynchronous calls inside `canActivate` and activate/don't activate specific component based on this async response? – P.S. Jun 07 '18 at 19:13
  • @CommercialSuicide Yes that's right. – Kingamere Jun 07 '18 at 19:19

2 Answers2

3

I was dealing with such cases, and here is what I usually do:

1. I create a Resolver service (which implements Resolve interface). It allows you to get all necessary data before activating the route:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { DataService } from 'path/to/data.service';

@Injectable()
export class ExampleResolverService implements Resolve<any> {
  constructor(private _dataService: DataService) { }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<any> {
    return this._dataService.anyAsyncCall()
      .then(response => {
        /* Let's imagine, that this method returns response with field "result", which can be equal to "true" or "false" */
        /* "setResult" just stores passed argument to "DataService" class property */
        this._dataService.setResult(response.result);
      })
      .catch(err => this._dataService.setResult(false););
  }
}

2. Here is how we can deal with AuthGuard, which implements CanActivate interface:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { DataService } from 'path/to/data.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private _dataService: DataService) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    /* "getResult" method operates with the same class property as setResult, it just returns the value of it */
    return this._dataService.getResult(); // will return "true" or "false"
  }
}

3. Then you can include the Resolver and the AuthGuard to your routes config, here is just a part (the structure of routes can differ, here is an example with activating the parent component):

const routes: Routes = [
  {
    path: 'app',
    component: AppComponent,
    resolve: {
      result: ExampleResolverService // your resolver
    },
    canActivate: [AuthGuard], // your AuthGuard with "canActivate" method
    children: [...] // child routes goes inside the array
  }
];

How it works

When you're navigating to /app, the ExampleResolverService starts, makes API call and stores the necessary part of response to class property in DataService via setResult method (it's the usual setter). Then, when the resolver finished the work, it's time for our AuthGuard. It gets stored result from DataService via getResult method (it's the usual getter), and returns this boolean result (our AuthGuard expects boolean to be returned, and the route will be activated if it returns true, and will not be activated if it returns false);

It's the simplest example without any additional operations with data, the logic is more complex usually, but this skeleton should be enough for basic understanding.

montrealist
  • 5,593
  • 12
  • 46
  • 68
P.S.
  • 15,970
  • 14
  • 62
  • 86
0

For me, I listened for ROUTE_NAVIGATED events in my app component like below

I am using ngrx/router-store to be able to listen to these router actions.

  // app.component.ts
  public ngOnInit(): void {
    // grab the action stream
    this.actions$.pipe(
      // Only pay attention to completed router 
      ofType(ROUTER_NAVIGATED),
      // Now I can guarantee that my resolve has completed, as the router has finsihed
      // Switch 
      switchMap(() => {
        // Now switch to get the stuff I was waiting for 
        return this.someService.getStuff();
      })
    // Need to subscribe to actions as we are in the component, not in an effect
    // I suppose we should unsubscribe, but the app component will never destroy as far as I am aware so will always be listening
    ).subscribe();
Ian Jamieson
  • 4,376
  • 2
  • 35
  • 55