feat(router): allow guards to return UrlTree as well as boolean (#26521)

* Removed `andObservable` helper function in favor of inline implementation
* Flow `boolean | UrlTree` through guards check
* Add tests to verify behavior of `checkGuards` function flowing `UrlTree` properly

PR Close #26521
This commit is contained in:
Jason Aden
2018-10-16 19:50:48 -07:00
committed by Matias Niemelä
parent 152ca66eba
commit 081f95c812
7 changed files with 213 additions and 77 deletions

View File

@ -7,17 +7,19 @@
*/
import {Injector} from '@angular/core';
import {MonoTypeOperatorFunction, Observable, from, of } from 'rxjs';
import {concatMap, every, first, map, mergeMap} from 'rxjs/operators';
import {MonoTypeOperatorFunction, Observable, defer, from, of } from 'rxjs';
import {concatAll, concatMap, first, map, mergeMap} from 'rxjs/operators';
import {ActivationStart, ChildActivationStart, Event} from '../events';
import {CanActivateChildFn, CanActivateFn, CanDeactivateFn} from '../interfaces';
import {NavigationTransition} from '../router';
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
import {UrlTree} from '../url_tree';
import {andObservables, wrapIntoObservable} from '../utils/collection';
import {wrapIntoObservable} from '../utils/collection';
import {CanActivate, CanDeactivate, getCanActivateChild, getToken} from '../utils/preactivation';
import {isCanActivate, isCanActivateChild, isCanDeactivate, isFunction} from '../utils/type_guards';
import {isCanActivate, isCanActivateChild, isCanDeactivate, isFunction, isBoolean} from '../utils/type_guards';
import {prioritizedGuardValue} from './prioritized_guard_value';
export function checkGuards(moduleInjector: Injector, forwardEvent?: (evt: Event) => void):
MonoTypeOperatorFunction<NavigationTransition> {
@ -33,10 +35,10 @@ export function checkGuards(moduleInjector: Injector, forwardEvent?: (evt: Event
canDeactivateChecks, targetSnapshot !, currentSnapshot, moduleInjector)
.pipe(
mergeMap(canDeactivate => {
return canDeactivate ?
return canDeactivate && isBoolean(canDeactivate) ?
runCanActivateChecks(
targetSnapshot !, canActivateChecks, moduleInjector, forwardEvent) :
of (false);
of (canDeactivate);
}),
map(guardsResult => ({...t, guardsResult})));
}));
@ -45,25 +47,30 @@ export function checkGuards(moduleInjector: Injector, forwardEvent?: (evt: Event
function runCanDeactivateChecks(
checks: CanDeactivate[], futureRSS: RouterStateSnapshot, currRSS: RouterStateSnapshot,
moduleInjector: Injector): Observable<boolean|UrlTree> {
moduleInjector: Injector) {
return from(checks).pipe(
mergeMap(check => {
return runCanDeactivate(check.component, check.route, currRSS, futureRSS, moduleInjector);
}),
every(result => result === true));
mergeMap(
check =>
runCanDeactivate(check.component, check.route, currRSS, futureRSS, moduleInjector)),
first(result => { return result !== true; }, true as boolean | UrlTree));
}
function runCanActivateChecks(
futureSnapshot: RouterStateSnapshot, checks: CanActivate[], moduleInjector: Injector,
forwardEvent?: (evt: Event) => void): Observable<boolean> {
forwardEvent?: (evt: Event) => void) {
return from(checks).pipe(
concatMap((check: CanActivate) => andObservables(from([
fireChildActivationStart(check.route.parent, forwardEvent),
fireActivationStart(check.route, forwardEvent),
runCanActivateChild(futureSnapshot, check.path, moduleInjector),
runCanActivate(futureSnapshot, check.route, moduleInjector)
]))),
every((result: boolean) => result === true));
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));
}
/**
@ -102,27 +109,30 @@ function fireChildActivationStart(
function runCanActivate(
futureRSS: RouterStateSnapshot, futureARS: ActivatedRouteSnapshot,
moduleInjector: Injector): Observable<boolean> {
moduleInjector: Injector): Observable<boolean|UrlTree> {
const canActivate = futureARS.routeConfig ? futureARS.routeConfig.canActivate : null;
if (!canActivate || canActivate.length === 0) return of (true);
const obs = from(canActivate).pipe(map((c: any) => {
const guard = getToken(c, futureARS, moduleInjector);
let observable;
if (isCanActivate(guard)) {
observable = wrapIntoObservable(guard.canActivate(futureARS, futureRSS));
} else if (isFunction<CanActivateFn>(guard)) {
observable = wrapIntoObservable(guard(futureARS, futureRSS));
} else {
throw new Error('Invalid canActivate guard');
}
return observable.pipe(first());
}));
return andObservables(obs);
const canActivateObservables = canActivate.map((c: any) => {
return defer(() => {
const guard = getToken(c, futureARS, moduleInjector);
let observable;
if (isCanActivate(guard)) {
observable = wrapIntoObservable(guard.canActivate(futureARS, futureRSS));
} else if (isFunction<CanActivateFn>(guard)) {
observable = wrapIntoObservable(guard(futureARS, futureRSS));
} else {
throw new Error('Invalid CanActivate guard');
}
return observable.pipe(first());
});
});
return of (canActivateObservables).pipe(prioritizedGuardValue());
}
function runCanActivateChild(
futureRSS: RouterStateSnapshot, path: ActivatedRouteSnapshot[],
moduleInjector: Injector): Observable<boolean> {
moduleInjector: Injector): Observable<boolean|UrlTree> {
const futureARS = path[path.length - 1];
const canActivateChildGuards = path.slice(0, path.length - 1)
@ -130,29 +140,32 @@ function runCanActivateChild(
.map(p => getCanActivateChild(p))
.filter(_ => _ !== null);
return andObservables(from(canActivateChildGuards).pipe(map((d: any) => {
const obs = from(d.guards).pipe(map((c: any) => {
const guard = getToken(c, d.node, moduleInjector);
let observable;
if (isCanActivateChild(guard)) {
observable = wrapIntoObservable(guard.canActivateChild(futureARS, futureRSS));
} else if (isFunction<CanActivateChildFn>(guard)) {
observable = wrapIntoObservable(guard(futureARS, futureRSS));
} else {
throw new Error('Invalid CanActivateChild guard');
}
return observable.pipe(first());
}));
return andObservables(obs);
})));
const canActivateChildGuardsMapped = canActivateChildGuards.map((d: any) => {
return defer(() => {
const guardsMapped = d.guards.map((c: any) => {
const guard = getToken(c, d.node, moduleInjector);
let observable;
if (isCanActivateChild(guard)) {
observable = wrapIntoObservable(guard.canActivateChild(futureARS, futureRSS));
} else if (isFunction<CanActivateChildFn>(guard)) {
observable = wrapIntoObservable(guard(futureARS, futureRSS));
} else {
throw new Error('Invalid CanActivateChild guard');
}
return observable.pipe(first());
});
return of (guardsMapped).pipe(prioritizedGuardValue());
});
});
return of (canActivateChildGuardsMapped).pipe(prioritizedGuardValue());
}
function runCanDeactivate(
component: Object| null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot,
component: Object | null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot,
futureRSS: RouterStateSnapshot, moduleInjector: Injector): Observable<boolean|UrlTree> {
const canDeactivate = currARS && currARS.routeConfig ? currARS.routeConfig.canDeactivate : null;
if (!canDeactivate || canDeactivate.length === 0) return of (true);
const canDeactivate$ = from(canDeactivate).pipe(mergeMap((c: any) => {
const canDeactivateObservables = canDeactivate.map((c: any) => {
const guard = getToken(c, currARS, moduleInjector);
let observable;
if (isCanDeactivate(guard)) {
@ -164,6 +177,6 @@ function runCanDeactivate(
throw new Error('Invalid CanDeactivate guard');
}
return observable.pipe(first());
}));
return canDeactivate$.pipe(every((result: any) => result === true));
});
return of (canDeactivateObservables).pipe(prioritizedGuardValue());
}