Matteo Meil
Matteo Meil

Reputation: 1353

Angular canActivateChild parent component activation

I may have misunderstood how canActivateChild works in Angular 11's routing.

With a route definition like the below one and canActivateChild returning false, ParentComponent isn't constructed but I'm expecting to have ParentComponent rendered while the child component not initialized and shown.

Route definition:

{
  path: "parent",
  component: ParentComponent,
  canActivate: ["canActivate"],
  canActivateChild: ["canActivateChild"],
  children: [
    {
      path: "child",
      component: ChildComponent
    }
  ]
}

Here's a stackblitz url to a full working example to demonstrate what I'm asking for.

Is this an expected behavior? Must I change my route definition to the following in order to have ParentComponent showing?

{
  path: "parent",
  component: ParentComponent,
  canActivate: ["canActivate"],
  children: [
    {
      path: "",
      canActivateChild: ["canActivateChild"],
      children: [
        {
          path: "child",
          component: ChildComponent
        }
      ]
    }
  ]
}

Upvotes: 4

Views: 1505

Answers (1)

Andrei Gătej
Andrei Gătej

Reputation: 11979

TLDR;

The canActivateChild guards are run before canActivate guards and if at least one returns false, the canActivate ones won't be run anymore and the current navigation will be cancelled. In case they are helpful, here's a link to my notes on Angular Router, after spending some time reading its source code.


A more detailed explanation

Whenever a navigation is about to take place, before it completes, it must go through some phases. These are all described here, in a big RxJS stream.

One phase of this entire process is the guards checking.

The checkGuards function is defined here, as follows:

/* ... */
return runCanDeactivateChecks(
              canDeactivateChecks, targetSnapshot!, currentSnapshot, moduleInjector)
      .pipe(
          mergeMap(canDeactivate => {
            return canDeactivate && isBoolean(canDeactivate) ?
                runCanActivateChecks(
                    targetSnapshot!, canActivateChecks, moduleInjector, forwardEvent) :
                of(canDeactivate);
          }),
          map(guardsResult => ({...t, guardsResult})));
});
/* ... */

So, as probably expected, it will run canDeactivate guards and if one of them fails, then the navigation will be cancelled. But, if everything is alright with canDeactivate guards, then canActivate and canActivateChild guards can be run.

If you scroll down a bit in the check_guards.ts file, you should find the runCanActivateChecks function:

function runCanActivateChecks(
    futureSnapshot: RouterStateSnapshot, checks: CanActivate[], moduleInjector: Injector,
    forwardEvent?: (evt: Event) => void) {
  return from(checks).pipe(
      concatMap((check: CanActivate) => {
        return from([
                 fireChildActivationStart(check.route.parent, forwardEvent),
                 fireActivationStart(check.route, forwardEvent),
                 // !
                 runCanActivateChild(futureSnapshot, check.path, moduleInjector),
                 runCanActivate(futureSnapshot, check.route, moduleInjector)
               ])
            .pipe(concatAll(), first(result => {
                    return result !== true;
                  }, true as boolean | UrlTree));
      }),
      first(result => {
        return result !== true;
      }, true as boolean | UrlTree));
}

A few interesting things happen here. First, from(arrayOfObservables) will emit a next notification that consits of arrayOfObservables. The first 2 observables of this array(fire*) are not very important right now and they simply return of(true). Then, we have runCanActivateChild and, for now, it's important that its return type is an Observable<boolean|UrlTree>. If we look below, we have concatAll(), which subscribes to each observable and queues the new ones that arrive until the current inner observable completes.
Then, first(...) makes sure that if we encounter any value which is not true, then we should stop.

Let's take your example. If a canActivateChild guard returns false, then runCanActivate won't be run, and the guardResult value(2 snippets above) will be false. And if that happens, then the navigation will be cancelled;

 filter(t => {
  if (!t.guardsResult) {
    this.resetUrlToCurrentUrlTree();
    const navCancel =
        new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), '');
    eventsSubject.next(navCancel);
    t.resolve(false);
    return false;
  }
  return true;
}),

So, this is why your example is not working as expected. If you'd like to read more about the inner workings of Angular Router, I'd recommend checking out these articles:

Upvotes: 2

Related Questions