feat(router): guard returning UrlTree cancels current navigation and redirects (#26521)

Fixes #24618
FW-153 #resolve

PR Close #26521
This commit is contained in:
Jason Aden
2018-10-17 09:30:45 -07:00
committed by Matias Niemelä
parent 081f95c812
commit 4e9f2e5895
8 changed files with 116 additions and 23 deletions

View File

@ -17,6 +17,10 @@ import {UrlSegment, UrlTree} from './url_tree';
* @description
*
* Interface that a class can implement to be a guard deciding if a route can be activated.
* If all guards return `true`, navigation will continue. If any guard returns `false`,
* navigation will be cancelled. If any guard returns a `UrlTree`, current navigation will
* be cancelled and a new navigation will be kicked off to the `UrlTree` returned from the
* guard.
*
* ```
* class UserToken {}
@ -33,7 +37,7 @@ import {UrlSegment, UrlTree} from './url_tree';
* canActivate(
* route: ActivatedRouteSnapshot,
* state: RouterStateSnapshot
* ): Observable<boolean>|Promise<boolean>|boolean|UrlTree {
* ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
* return this.permissions.canActivate(this.currentUser, route.params.id);
* }
* }
@ -90,7 +94,11 @@ export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSn
* @description
*
* Interface that a class can implement to be a guard deciding if a child route can be activated.
*
* If all guards return `true`, navigation will continue. If any guard returns `false`,
* navigation will be cancelled. If any guard returns a `UrlTree`, current navigation will
* be cancelled and a new navigation will be kicked off to the `UrlTree` returned from the
* guard.
*
* ```
* class UserToken {}
* class Permissions {
@ -106,7 +114,7 @@ export type CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSn
* canActivateChild(
* route: ActivatedRouteSnapshot,
* state: RouterStateSnapshot
* ): Observable<boolean>|Promise<boolean>|boolean|UrlTree {
* ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
* return this.permissions.canActivate(this.currentUser, route.params.id);
* }
* }
@ -173,7 +181,11 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou
* @description
*
* Interface that a class can implement to be a guard deciding if a route can be deactivated.
*
* If all guards return `true`, navigation will continue. If any guard returns `false`,
* navigation will be cancelled. If any guard returns a `UrlTree`, current navigation will
* be cancelled and a new navigation will be kicked off to the `UrlTree` returned from the
* guard.
*
* ```
* class UserToken {}
* class Permissions {
@ -191,7 +203,7 @@ export type CanActivateChildFn = (childRoute: ActivatedRouteSnapshot, state: Rou
* currentRoute: ActivatedRouteSnapshot,
* currentState: RouterStateSnapshot,
* nextState: RouterStateSnapshot
* ): Observable<boolean>|Promise<boolean>|boolean|UrlTree {
* ): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
* return this.permissions.canDeactivate(this.currentUser, route.params.id);
* }
* }

View File

@ -17,7 +17,7 @@ import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
import {UrlTree} from '../url_tree';
import {wrapIntoObservable} from '../utils/collection';
import {CanActivate, CanDeactivate, getCanActivateChild, getToken} from '../utils/preactivation';
import {isCanActivate, isCanActivateChild, isCanDeactivate, isFunction, isBoolean} from '../utils/type_guards';
import {isBoolean, isCanActivate, isCanActivateChild, isCanDeactivate, isFunction} from '../utils/type_guards';
import {prioritizedGuardValue} from './prioritized_guard_value';

View File

@ -7,9 +7,10 @@
*/
import {Observable, OperatorFunction, combineLatest} from 'rxjs';
import {filter, scan, startWith, switchMap, take} from 'rxjs/operators';
import {filter, map, scan, startWith, switchMap, take} from 'rxjs/operators';
import {UrlTree} from '../url_tree';
import {isUrlTree} from '../utils/type_guards';
const INITIAL_VALUE = Symbol('INITIAL_VALUE');
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree;
@ -38,7 +39,7 @@ export function prioritizedGuardValue():
// navigation
if (val === false) return val;
if (i === list.length - 1 || val instanceof UrlTree) {
if (i === list.length - 1 || isUrlTree(val)) {
return val;
}
}
@ -47,6 +48,8 @@ export function prioritizedGuardValue():
}, acc);
},
INITIAL_VALUE),
filter(item => item !== INITIAL_VALUE), take(1)) as Observable<boolean|UrlTree>;
filter(item => item !== INITIAL_VALUE),
map(item => isUrlTree(item) ? item : item === true), //
take(1)) as Observable<boolean|UrlTree>;
});
}

View File

@ -8,7 +8,7 @@
import {Location} from '@angular/common';
import {Compiler, Injector, NgModuleFactoryLoader, NgModuleRef, NgZone, Type, isDevMode, ɵConsole as Console} from '@angular/core';
import {BehaviorSubject, EMPTY, Observable, Subject, Subscription, of } from 'rxjs';
import {BehaviorSubject, EMPTY, Observable, Subject, Subscription, defer, of } from 'rxjs';
import {catchError, filter, finalize, map, switchMap, tap} from 'rxjs/operators';
import {QueryParamsHandling, Route, Routes, standardizeConfig, validateConfig} from './config';
@ -25,10 +25,11 @@ import {DefaultRouteReuseStrategy, RouteReuseStrategy} from './route_reuse_strat
import {RouterConfigLoader} from './router_config_loader';
import {ChildrenOutletContexts} from './router_outlet_context';
import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState} from './router_state';
import {Params, isNavigationCancelingError} from './shared';
import {Params, isNavigationCancelingError, navigationCancelingError} from './shared';
import {DefaultUrlHandlingStrategy, UrlHandlingStrategy} from './url_handling_strategy';
import {UrlSerializer, UrlTree, containsTree, createEmptyUrlTree} from './url_tree';
import {Checks, getAllRouteGuards} from './utils/preactivation';
import {isUrlTree} from './utils/type_guards';
@ -487,6 +488,14 @@ export class Router {
})),
checkGuards(this.ngModule.injector, (evt: Event) => this.triggerEvent(evt)),
tap(t => {
if (isUrlTree(t.guardsResult)) {
const error: Error&{url?: UrlTree} = navigationCancelingError(
`Redirecting to "${this.serializeUrl(t.guardsResult)}"`);
error.url = t.guardsResult;
throw error;
}
}),
tap(t => {
const guardsEnd = new GuardsCheckEnd(
@ -602,11 +611,19 @@ export class Router {
* rather than an error. */
if (isNavigationCancelingError(e)) {
this.navigated = true;
this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl);
const redirecting = isUrlTree(e.url);
if (!redirecting) {
this.resetStateAndUrl(t.currentRouterState, t.currentUrlTree, t.rawUrl);
}
const navCancel =
new NavigationCancel(t.id, this.serializeUrl(t.extractedUrl), e.message);
eventsSubject.next(navCancel);
t.resolve(false);
if (redirecting) {
this.navigateByUrl(e.url);
}
/* All other errors should reset to the router's internal URL reference to the
* pre-error state. */
} else {
@ -815,7 +832,7 @@ export class Router {
`Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?`);
}
const urlTree = url instanceof UrlTree ? url : this.parseUrl(url);
const urlTree = isUrlTree(url) ? url : this.parseUrl(url);
const mergedTree = this.urlHandlingStrategy.merge(urlTree, this.rawUrlTree);
return this.scheduleNavigation(mergedTree, 'imperative', null, extras);
@ -867,7 +884,7 @@ export class Router {
/** Returns whether the url is activated */
isActive(url: string|UrlTree, exact: boolean): boolean {
if (url instanceof UrlTree) {
if (isUrlTree(url)) {
return containsTree(this.currentUrlTree, url, exact);
}

View File

@ -7,6 +7,7 @@
*/
import {CanActivate, CanActivateChild, CanDeactivate, CanLoad} from '../interfaces';
import {UrlTree} from '../url_tree';
/**
* Simple function check, but generic so type inference will flow. Example:
@ -29,6 +30,10 @@ export function isBoolean(v: any): v is boolean {
return typeof v === 'boolean';
}
export function isUrlTree(v: any): v is UrlTree {
return v instanceof UrlTree;
}
export function isCanLoad(guard: any): guard is CanLoad {
return guard && isFunction<CanLoad>(guard.canLoad);
}
@ -38,7 +43,7 @@ export function isCanActivate(guard: any): guard is CanActivate {
}
export function isCanActivateChild(guard: any): guard is CanActivateChild {
return guard && isFunction<CanActivateChild>(guard.canActivate);
return guard && isFunction<CanActivateChild>(guard.canActivateChild);
}
export function isCanDeactivate<T>(guard: any): guard is CanDeactivate<T> {