Skip to content

Async guards should have the option to wait for previous guards to finish #21702

Closed

Description

@MickL

I'm submitting a...


[ ] Regression
[ ] Bug report
[X] Feature request
[ ] Documentation issue or request
[ ] Support request

Current behavior

When having multiple async guards(returning promise or observable) for the same path they execute at the same time:

{
  path: '',
  canActivate: [
        AuthGuard,
        DetectLanguage,
        GetAppdata,
  ],
  children: [ ... ]
}

Expected behavior

  • There should be an option to let them execute synchronous: guard[i+1] should wait for guard[i] to finish
  • If guard[i] returns false all following guards should not execute

What is the motivation / use case for changing the behavior?

The example above has an authentication which needs to be done first before getting the app data. Currently it will request the App-Data immediatly without having a token.

The only solution is to use some hacky way with only one guard that executes them in sync. The feature having an array of guards is useless then.

Environment

Angular version: 5.2.x (i'm optimistic you implement this in 30minutes ;) )

Activity

  1. MickL commented on Jan 22, 2018

    @MickL
    Author

    Here is another (ugly) workaround i found aside for creating one Guard which combines them manually. Use nested routes:

       {
          path: '',
          canActivate: [
             AuthGuard,
          ],
          children: [
            {
              path: '',
              canActivate: [
                 DetectLanguage,
              ],
              children: [
                 {
                    path: '',
                    canActivate: [
                        GetAppdata,
                    ],
                    component: YourComponent
                    // or redirectTo
                    // or children
                    // or loadChildren
                 }
             }
          ]
        }
    
  2. jotatoledo commented on Jan 22, 2018

    @jotatoledo
    Contributor

    You could wrap them in a new Guard that implements your logic.

  3. MickL commented on Jan 22, 2018

    @MickL
    Author

    That is the "hacky way" i meant within "What is the motivation"

  4. jasonaden commented on Feb 6, 2018

    @jasonaden
    Contributor

    Thanks, and while I agree this would be a cool feature (similar to how ui-router for AngularJS did resolves), it's not planned for this version of the router.

    We will be adding better Observable support though, which should allow you to compose data to be mapped to components with whatever delays/dependencies you want.

  5. csnate commented on Sep 11, 2018

    @csnate

    I ran into this issue recently as well, and thought I'd share my solution in case it helps anyone else. Assuming the canActivate methods on all of your RouteGuards return Observables.

    1. Create a Combinator abstract generic class that'll combine two other route guards
    export abstract class Combinator<T1 extends CanActivate, T2 extends CanActivate> implements CanActivate {
        constructor(private one: T1, private two: T2) {}
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
            // Use your favorite/appropriate RxJS combinator
            return combineLatest(<Observable<boolean>>this.one.canActivate(route, state), <Observable<boolean>>this.two.canActivate(route, state))
                .pipe(map(([oneVal, twoVal]: [boolean, boolean]) => {
                    return oneVal && twoVal;
                }));
        }
    }
    
    1. Create the implementation of the Combinator for your two route guards
    @Injectable()
    export class MyCombinedGuard extends Combinator<AuthGuard, DetectLanguage> {
        constructor(one: AuthGuard, two: DetectLanguage) {
            super(one, two);
        }
    }
    
    1. Provide it and add it to the guard:
    @NgModule({
       imports: [RouterModule.forChild([
            { path: '/somewhere', component: MyComponent, canActivate: [MyCombinedGuard] }
        ])],
        providers: [AuthGuard, DetectLanguage, MyCombinedGuard]
    })
    export class MyRoutingModule { }
    

    The Combinator can be made to include additional generic types as needed.

  6. jwhenry3 commented on Apr 19, 2019

    @jwhenry3

    I have an idea for a not-so-nasty way to accomplish this and it seems to work just fine:

    import {Injectable, Injector}                                              from "@angular/core";
    import {ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, UrlTree} from "@angular/router";
    
    @Injectable()
    export class AllGuard implements CanActivate {
    
        constructor(private injector: Injector) {
    
        }
    
        async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree> {
            let guards = route.data.guards || [];
            for (let guard of guards) {
                let instance: CanActivate = this.injector.get(guard);
                let result                = await instance.canActivate(route, state);
                if (result === false || result instanceof UrlTree) {
                    return result;
                }
            }
            return true;
        }
    
    }
    

    and its implementation would be

    @NgModule({
        imports: [
            SomeOtherModule,
            RouterModule.forRoot([
                 { path: '', data: {guards: [SomeGuard]}, canActivate: [AllGuard]
             ])
        ],
        providers: [AllGuard]
    })
    export class SomeModule {}
    
  7. marchrius commented on Jul 25, 2019

    @marchrius

    This is an implementation supporting Promise | Observable return from await (Thanks to @jwhenry3)

    export class CombinedGuard implements CanActivate, CanActivateChild {
    
      constructor(private injector: Injector) { }
    
      async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree>/* | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree*/ {
        const guards = (((route || {data: {}}).data || {guards: {}}).guards || {activate: []}).activate || [];
    
        for (const guard of guards) {
          const instance: CanActivate = this.injector.get(guard);
          let result                = await instance.canActivate(route, state);
          if (result instanceof Observable) {
            result = await result.toPromise();
          }
          if (result === false || result instanceof UrlTree) {
            return result;
          }
        }
        return true;
      }
    
      async canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean | UrlTree>/* | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree*/ {
        const guards = (((childRoute || {data: {}}).data || {guards: {}}).guards || {activate: []}).activateChild || [];
    
        for (const guard of guards) {
          const instance: CanActivateChild = this.injector.get(guard);
          let result                = await instance.canActivateChild(childRoute, state);
          if (result instanceof Observable) {
            result = await result.toPromise();
          }
          if (result === false || result instanceof UrlTree) {
            return result;
          }
        }
        return true;
      }
    }
    
  8. pratik-joshi-cactus commented on Aug 26, 2019

    @pratik-joshi-cactus

    @jwhenry3 tried your solution, but throwing error.

    ERROR in resources/angular/app/guards/all.guard.ts(7,14): error TS2420: Class 'AllGuard' incorrectly implements interface 'CanActivate'.
      Types of property 'canActivate' are incompatible.
        Type '(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Promise<boolean | (Observable<bool...' is not assignable to type '(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => boolean | Promise<boolean> | Obser...'.
          Type 'Promise<boolean | (Observable<boolean> & UrlTree)>' is not assignable to type 'boolean | Promise<boolean> | Observable<boolean>'.
            Type 'Promise<boolean | (Observable<boolean> & UrlTree)>' is not assignable to type 'Observable<boolean>'.
              Property '_isScalar' is missing in type 'Promise<boolean | (Observable<boolean> & UrlTree)>'.
    
    

    I am using Angular 5.

  9. angular-automatic-lock-bot commented on Sep 26, 2019

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  10. locked and limited conversation to collaborators on Sep 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions