Skip to content

Commit 081f95c

Browse files
jasonadenmatsko
authored andcommitted
feat(router): allow guards to return UrlTree as well as boolean (angular#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 angular#26521
1 parent 152ca66 commit 081f95c

File tree

7 files changed

+213
-77
lines changed

7 files changed

+213
-77
lines changed

packages/router/src/apply_redirects.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88

99
import {Injector, NgModuleRef} from '@angular/core';
1010
import {EmptyError, Observable, Observer, from, of } from 'rxjs';
11-
import {catchError, concatAll, first, map, mergeMap} from 'rxjs/operators';
11+
import {catchError, concatAll, every, first, map, mergeMap} from 'rxjs/operators';
1212

1313
import {LoadedRouterConfig, Route, Routes} from './config';
1414
import {CanLoadFn} from './interfaces';
1515
import {RouterConfigLoader} from './router_config_loader';
1616
import {PRIMARY_OUTLET, Params, defaultUrlMatcher, navigationCancelingError} from './shared';
1717
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
18-
import {andObservables, forEach, waitForMap, wrapIntoObservable} from './utils/collection';
18+
import {forEach, waitForMap, wrapIntoObservable} from './utils/collection';
1919
import {isCanLoad, isFunction} from './utils/type_guards';
2020

2121
class NoMatch {
@@ -421,7 +421,7 @@ function runCanLoadGuard(
421421
return wrapIntoObservable(guardVal);
422422
}));
423423

424-
return andObservables(obs);
424+
return obs.pipe(concatAll(), every(result => result === true));
425425
}
426426

427427
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): {

packages/router/src/operators/check_guards.ts

Lines changed: 66 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
*/
88

99
import {Injector} from '@angular/core';
10-
import {MonoTypeOperatorFunction, Observable, from, of } from 'rxjs';
11-
import {concatMap, every, first, map, mergeMap} from 'rxjs/operators';
10+
import {MonoTypeOperatorFunction, Observable, defer, from, of } from 'rxjs';
11+
import {concatAll, concatMap, first, map, mergeMap} from 'rxjs/operators';
1212

1313
import {ActivationStart, ChildActivationStart, Event} from '../events';
1414
import {CanActivateChildFn, CanActivateFn, CanDeactivateFn} from '../interfaces';
1515
import {NavigationTransition} from '../router';
1616
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../router_state';
1717
import {UrlTree} from '../url_tree';
18-
import {andObservables, wrapIntoObservable} from '../utils/collection';
18+
import {wrapIntoObservable} from '../utils/collection';
1919
import {CanActivate, CanDeactivate, getCanActivateChild, getToken} from '../utils/preactivation';
20-
import {isCanActivate, isCanActivateChild, isCanDeactivate, isFunction} from '../utils/type_guards';
20+
import {isCanActivate, isCanActivateChild, isCanDeactivate, isFunction, isBoolean} from '../utils/type_guards';
21+
22+
import {prioritizedGuardValue} from './prioritized_guard_value';
2123

2224
export function checkGuards(moduleInjector: Injector, forwardEvent?: (evt: Event) => void):
2325
MonoTypeOperatorFunction<NavigationTransition> {
@@ -33,10 +35,10 @@ export function checkGuards(moduleInjector: Injector, forwardEvent?: (evt: Event
3335
canDeactivateChecks, targetSnapshot !, currentSnapshot, moduleInjector)
3436
.pipe(
3537
mergeMap(canDeactivate => {
36-
return canDeactivate ?
38+
return canDeactivate && isBoolean(canDeactivate) ?
3739
runCanActivateChecks(
3840
targetSnapshot !, canActivateChecks, moduleInjector, forwardEvent) :
39-
of (false);
41+
of (canDeactivate);
4042
}),
4143
map(guardsResult => ({...t, guardsResult})));
4244
}));
@@ -45,25 +47,30 @@ export function checkGuards(moduleInjector: Injector, forwardEvent?: (evt: Event
4547

4648
function runCanDeactivateChecks(
4749
checks: CanDeactivate[], futureRSS: RouterStateSnapshot, currRSS: RouterStateSnapshot,
48-
moduleInjector: Injector): Observable<boolean|UrlTree> {
50+
moduleInjector: Injector) {
4951
return from(checks).pipe(
50-
mergeMap(check => {
51-
return runCanDeactivate(check.component, check.route, currRSS, futureRSS, moduleInjector);
52-
}),
53-
every(result => result === true));
52+
mergeMap(
53+
check =>
54+
runCanDeactivate(check.component, check.route, currRSS, futureRSS, moduleInjector)),
55+
first(result => { return result !== true; }, true as boolean | UrlTree));
5456
}
5557

5658
function runCanActivateChecks(
5759
futureSnapshot: RouterStateSnapshot, checks: CanActivate[], moduleInjector: Injector,
58-
forwardEvent?: (evt: Event) => void): Observable<boolean> {
60+
forwardEvent?: (evt: Event) => void) {
5961
return from(checks).pipe(
60-
concatMap((check: CanActivate) => andObservables(from([
61-
fireChildActivationStart(check.route.parent, forwardEvent),
62-
fireActivationStart(check.route, forwardEvent),
63-
runCanActivateChild(futureSnapshot, check.path, moduleInjector),
64-
runCanActivate(futureSnapshot, check.route, moduleInjector)
65-
]))),
66-
every((result: boolean) => result === true));
62+
concatMap((check: CanActivate) => {
63+
return from([
64+
fireChildActivationStart(check.route.parent, forwardEvent),
65+
fireActivationStart(check.route, forwardEvent),
66+
runCanActivateChild(futureSnapshot, check.path, moduleInjector),
67+
runCanActivate(futureSnapshot, check.route, moduleInjector)
68+
])
69+
.pipe(concatAll(), first(result => {
70+
return result !== true;
71+
}, true as boolean | UrlTree));
72+
}),
73+
first(result => { return result !== true; }, true as boolean | UrlTree));
6774
}
6875

6976
/**
@@ -102,57 +109,63 @@ function fireChildActivationStart(
102109

103110
function runCanActivate(
104111
futureRSS: RouterStateSnapshot, futureARS: ActivatedRouteSnapshot,
105-
moduleInjector: Injector): Observable<boolean> {
112+
moduleInjector: Injector): Observable<boolean|UrlTree> {
106113
const canActivate = futureARS.routeConfig ? futureARS.routeConfig.canActivate : null;
107114
if (!canActivate || canActivate.length === 0) return of (true);
108-
const obs = from(canActivate).pipe(map((c: any) => {
109-
const guard = getToken(c, futureARS, moduleInjector);
110-
let observable;
111-
if (isCanActivate(guard)) {
112-
observable = wrapIntoObservable(guard.canActivate(futureARS, futureRSS));
113-
} else if (isFunction<CanActivateFn>(guard)) {
114-
observable = wrapIntoObservable(guard(futureARS, futureRSS));
115-
} else {
116-
throw new Error('Invalid canActivate guard');
117-
}
118-
return observable.pipe(first());
119-
}));
120-
return andObservables(obs);
115+
116+
const canActivateObservables = canActivate.map((c: any) => {
117+
return defer(() => {
118+
const guard = getToken(c, futureARS, moduleInjector);
119+
let observable;
120+
if (isCanActivate(guard)) {
121+
observable = wrapIntoObservable(guard.canActivate(futureARS, futureRSS));
122+
} else if (isFunction<CanActivateFn>(guard)) {
123+
observable = wrapIntoObservable(guard(futureARS, futureRSS));
124+
} else {
125+
throw new Error('Invalid CanActivate guard');
126+
}
127+
return observable.pipe(first());
128+
});
129+
});
130+
return of (canActivateObservables).pipe(prioritizedGuardValue());
121131
}
122132

123133
function runCanActivateChild(
124134
futureRSS: RouterStateSnapshot, path: ActivatedRouteSnapshot[],
125-
moduleInjector: Injector): Observable<boolean> {
135+
moduleInjector: Injector): Observable<boolean|UrlTree> {
126136
const futureARS = path[path.length - 1];
127137

128138
const canActivateChildGuards = path.slice(0, path.length - 1)
129139
.reverse()
130140
.map(p => getCanActivateChild(p))
131141
.filter(_ => _ !== null);
132142

133-
return andObservables(from(canActivateChildGuards).pipe(map((d: any) => {
134-
const obs = from(d.guards).pipe(map((c: any) => {
135-
const guard = getToken(c, d.node, moduleInjector);
136-
let observable;
137-
if (isCanActivateChild(guard)) {
138-
observable = wrapIntoObservable(guard.canActivateChild(futureARS, futureRSS));
139-
} else if (isFunction<CanActivateChildFn>(guard)) {
140-
observable = wrapIntoObservable(guard(futureARS, futureRSS));
141-
} else {
142-
throw new Error('Invalid CanActivateChild guard');
143-
}
144-
return observable.pipe(first());
145-
}));
146-
return andObservables(obs);
147-
})));
143+
const canActivateChildGuardsMapped = canActivateChildGuards.map((d: any) => {
144+
return defer(() => {
145+
const guardsMapped = d.guards.map((c: any) => {
146+
const guard = getToken(c, d.node, moduleInjector);
147+
let observable;
148+
if (isCanActivateChild(guard)) {
149+
observable = wrapIntoObservable(guard.canActivateChild(futureARS, futureRSS));
150+
} else if (isFunction<CanActivateChildFn>(guard)) {
151+
observable = wrapIntoObservable(guard(futureARS, futureRSS));
152+
} else {
153+
throw new Error('Invalid CanActivateChild guard');
154+
}
155+
return observable.pipe(first());
156+
});
157+
return of (guardsMapped).pipe(prioritizedGuardValue());
158+
});
159+
});
160+
return of (canActivateChildGuardsMapped).pipe(prioritizedGuardValue());
148161
}
149162

150163
function runCanDeactivate(
151-
component: Object| null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot,
164+
component: Object | null, currARS: ActivatedRouteSnapshot, currRSS: RouterStateSnapshot,
152165
futureRSS: RouterStateSnapshot, moduleInjector: Injector): Observable<boolean|UrlTree> {
153166
const canDeactivate = currARS && currARS.routeConfig ? currARS.routeConfig.canDeactivate : null;
154167
if (!canDeactivate || canDeactivate.length === 0) return of (true);
155-
const canDeactivate$ = from(canDeactivate).pipe(mergeMap((c: any) => {
168+
const canDeactivateObservables = canDeactivate.map((c: any) => {
156169
const guard = getToken(c, currARS, moduleInjector);
157170
let observable;
158171
if (isCanDeactivate(guard)) {
@@ -164,6 +177,6 @@ function runCanDeactivate(
164177
throw new Error('Invalid CanDeactivate guard');
165178
}
166179
return observable.pipe(first());
167-
}));
168-
return canDeactivate$.pipe(every((result: any) => result === true));
180+
});
181+
return of (canDeactivateObservables).pipe(prioritizedGuardValue());
169182
}

packages/router/src/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export type NavigationTransition = {
186186
currentRouterState: RouterState,
187187
targetRouterState: RouterState | null,
188188
guards: Checks,
189-
guardsResult: boolean | null,
189+
guardsResult: boolean | UrlTree | null,
190190
};
191191

192192
/**

packages/router/src/utils/collection.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {NgModuleFactory, ɵisObservable as isObservable, ɵisPromise as isPromise} from '@angular/core';
1010
import {Observable, from, of } from 'rxjs';
11-
import {concatAll, every, last as lastValue, map, mergeAll} from 'rxjs/operators';
11+
import {concatAll, last as lastValue, map} from 'rxjs/operators';
1212

1313
import {PRIMARY_OUTLET} from '../shared';
1414

@@ -88,14 +88,6 @@ export function waitForMap<A, B>(
8888
return of .apply(null, waitHead.concat(waitTail)).pipe(concatAll(), lastValue(), map(() => res));
8989
}
9090

91-
/**
92-
* ANDs Observables by merging all input observables, reducing to an Observable verifying all
93-
* input Observables return `true`.
94-
*/
95-
export function andObservables(observables: Observable<Observable<any>>): Observable<boolean> {
96-
return observables.pipe(mergeAll(), every((result: any) => result === true));
97-
}
98-
9991
export function wrapIntoObservable<T>(value: T | NgModuleFactory<T>| Promise<T>| Observable<T>) {
10092
if (isObservable(value)) {
10193
return value;

packages/router/src/utils/type_guards.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Type} from '@angular/core';
109
import {CanActivate, CanActivateChild, CanDeactivate, CanLoad} from '../interfaces';
1110

1211
/**
@@ -26,6 +25,10 @@ export function isFunction<T>(v: any): v is T {
2625
return typeof v === 'function';
2726
}
2827

28+
export function isBoolean(v: any): v is boolean {
29+
return typeof v === 'boolean';
30+
}
31+
2932
export function isCanLoad(guard: any): guard is CanLoad {
3033
return guard && isFunction<CanLoad>(guard.canLoad);
3134
}

packages/router/test/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class Logger {
1919
empty() { this.logs.length = 0; }
2020
}
2121

22-
export function provideTokenLogger(token: string, returnValue = true) {
22+
export function provideTokenLogger(token: string, returnValue = true as boolean | UrlTree) {
2323
return {
2424
provide: token,
2525
useFactory: (logger: Logger) => () => (logger.add(token), returnValue),

0 commit comments

Comments
 (0)