docs(router): Update router guide to use UrlTree for guard redirects (#37100)

The current implementation for redirecting users inside guards was in place
before the feature was added to allow `CanActivate` and `CanActivateChild` guards
to return `UrlTree` for redirecting users.

Returning `UrlTree` should be the default method, as it provides a more desirable
redirecting experience. When using `router.navigate` followed by `return false`,
the `Router` calls `resetUrlToCurrentUrlTree` (in the `finalize` operator) before
processing the navigation to the new route.  This can result in an undesirable
history if the navigation was the first navigation in the application - that is,
the route will briefly be reset to just `/` (see #36187).

Fixes #36187

PR Close #37100
This commit is contained in:
Andrew Scott 2020-05-13 14:13:43 -07:00 committed by Kara Erickson
parent 77c0ef38be
commit 8a56c99f87
4 changed files with 22 additions and 23 deletions

View File

@ -1,6 +1,6 @@
// #docregion // #docregion
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@ -12,21 +12,20 @@ export class AuthGuard implements CanActivate {
canActivate( canActivate(
next: ActivatedRouteSnapshot, next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean { state: RouterStateSnapshot): true|UrlTree {
let url: string = state.url; let url: string = state.url;
return this.checkLogin(url); return this.checkLogin(url);
} }
checkLogin(url: string): boolean { checkLogin(url: string): true|UrlTree {
if (this.authService.isLoggedIn) { return true; } if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting // Store the attempted URL for redirecting
this.authService.redirectUrl = url; this.authService.redirectUrl = url;
// Navigate to the login page with extras // Redirect to the login page
this.router.navigate(['/login']); return this.router.parseUrl('/login');
return false;
} }
} }
// #enddocregion // #enddocregion

View File

@ -4,7 +4,8 @@ import {
CanActivate, Router, CanActivate, Router,
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
RouterStateSnapshot, RouterStateSnapshot,
CanActivateChild CanActivateChild,
UrlTree
} from '@angular/router'; } from '@angular/router';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@ -16,7 +17,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
canActivate( canActivate(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean { state: RouterStateSnapshot): true|UrlTree {
let url: string = state.url; let url: string = state.url;
return this.checkLogin(url); return this.checkLogin(url);
@ -24,20 +25,19 @@ export class AuthGuard implements CanActivate, CanActivateChild {
canActivateChild( canActivateChild(
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean { state: RouterStateSnapshot): true|UrlTree {
return this.canActivate(route, state); return this.canActivate(route, state);
} }
// #enddocregion can-activate-child // #enddocregion can-activate-child
checkLogin(url: string): boolean { checkLogin(url: string): true|UrlTree {
if (this.authService.isLoggedIn) { return true; } if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting // Store the attempted URL for redirecting
this.authService.redirectUrl = url; this.authService.redirectUrl = url;
// Navigate to the login page // Redirect to the login page
this.router.navigate(['/login']); return this.router.parseUrl('/login');
return false;
} }
// #docregion can-activate-child // #docregion can-activate-child
} }

View File

@ -6,7 +6,8 @@ import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
RouterStateSnapshot, RouterStateSnapshot,
CanActivateChild, CanActivateChild,
NavigationExtras NavigationExtras,
UrlTree
} from '@angular/router'; } from '@angular/router';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@ -16,17 +17,17 @@ import { AuthService } from './auth.service';
export class AuthGuard implements CanActivate, CanActivateChild { export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {} constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
let url: string = state.url; let url: string = state.url;
return this.checkLogin(url); return this.checkLogin(url);
} }
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
return this.canActivate(route, state); return this.canActivate(route, state);
} }
checkLogin(url: string): boolean { checkLogin(url: string): true|UrlTree {
if (this.authService.isLoggedIn) { return true; } if (this.authService.isLoggedIn) { return true; }
// Store the attempted URL for redirecting // Store the attempted URL for redirecting
@ -42,8 +43,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
fragment: 'anchor' fragment: 'anchor'
}; };
// Navigate to the login page with extras // Redirect to the login page with extras
this.router.navigate(['/login'], navigationExtras); return this.router.createUrlTree(['/login'], navigationExtras);
return false;
} }
} }

View File

@ -2737,7 +2737,7 @@ If the user is logged in, it returns true and the navigation continues.
The `ActivatedRouteSnapshot` contains the _future_ route that will be activated and the `RouterStateSnapshot` contains the _future_ `RouterState` of the application, should you pass through the guard check. The `ActivatedRouteSnapshot` contains the _future_ route that will be activated and the `RouterStateSnapshot` contains the _future_ `RouterState` of the application, should you pass through the guard check.
If the user is not logged in, you store the attempted URL the user came from using the `RouterStateSnapshot.url` and tell the router to redirect to a login page—a page you haven't created yet. If the user is not logged in, you store the attempted URL the user came from using the `RouterStateSnapshot.url` and tell the router to redirect to a login page—a page you haven't created yet.
This secondary navigation automatically cancels the current navigation; `checkLogin()` returns `false`. Returning a `UrlTree` tells the `Router` to cancel the current navigation and schedule a new one to redirect the user.
{@a add-login-component} {@a add-login-component}
@ -2790,8 +2790,8 @@ Extend the `AuthGuard` to protect when navigating between the `admin` routes.
Open `auth.guard.ts` and add the `CanActivateChild` interface to the imported tokens from the router package. Open `auth.guard.ts` and add the `CanActivateChild` interface to the imported tokens from the router package.
Next, implement the `canActivateChild()` method which takes the same arguments as the `canActivate()` method: an `ActivatedRouteSnapshot` and `RouterStateSnapshot`. Next, implement the `canActivateChild()` method which takes the same arguments as the `canActivate()` method: an `ActivatedRouteSnapshot` and `RouterStateSnapshot`.
The `canActivateChild()` method can return an `Observable<boolean>` or `Promise<boolean>` for async checks and a `boolean` for sync checks. The `canActivateChild()` method can return an `Observable<boolean|UrlTree>` or `Promise<boolean|UrlTree>` for async checks and a `boolean` or `UrlTree` for sync checks.
This one returns a `boolean`: This one returns either `true` to allow the user to access the admin feature module or `UrlTree` to redirect the user to the login page instead:
<code-example path="router/src/app/auth/auth.guard.3.ts" header="src/app/auth/auth.guard.ts (excerpt)" region="can-activate-child"></code-example> <code-example path="router/src/app/auth/auth.guard.3.ts" header="src/app/auth/auth.guard.ts (excerpt)" region="can-activate-child"></code-example>