Skip to content

Commit fdfedce

Browse files
jasonadenmhevery
authored andcommitted
feat(router): add prioritizedGuardValue operator optimization and allowing UrlTree return from guard (angular#26478)
* If all guards return `true`, operator returns `true` * `false` and `UrlTree` are now both valid returns from a guard * Both these values wait for higher priority guards to resolve * Highest priority `false` or `UrlTree` value will be returned PR Close angular#26478
1 parent 9e5d440 commit fdfedce

File tree

3 files changed

+235
-0
lines changed

3 files changed

+235
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Observable, OperatorFunction, combineLatest} from 'rxjs';
10+
import {filter, scan, startWith, switchMap, take} from 'rxjs/operators';
11+
12+
import {UrlTree} from '../url_tree';
13+
14+
const INITIAL_VALUE = Symbol('INITIAL_VALUE');
15+
declare type INTERIM_VALUES = typeof INITIAL_VALUE | boolean | UrlTree;
16+
17+
export function prioritizedGuardValue():
18+
OperatorFunction<Observable<boolean|UrlTree>[], boolean|UrlTree> {
19+
return switchMap(obs => {
20+
return combineLatest(
21+
...obs.map(o => o.pipe(take(1), startWith(INITIAL_VALUE as INTERIM_VALUES))))
22+
.pipe(
23+
scan(
24+
(acc: INTERIM_VALUES, list: INTERIM_VALUES[]) => {
25+
let isPending = false;
26+
return list.reduce((innerAcc, val, i: number) => {
27+
if (innerAcc !== INITIAL_VALUE) return innerAcc;
28+
29+
// Toggle pending flag if any values haven't been set yet
30+
if (val === INITIAL_VALUE) isPending = true;
31+
32+
// Any other return values are only valid if we haven't yet hit a pending call.
33+
// This guarantees that in the case of a guard at the bottom of the tree that
34+
// returns a redirect, we will wait for the higher priority guard at the top to
35+
// finish before performing the redirect.
36+
if (!isPending) {
37+
// Early return when we hit a `false` value as that should always cancel
38+
// navigation
39+
if (val === false) return val;
40+
41+
if (i === list.length - 1 || val instanceof UrlTree) {
42+
return val;
43+
}
44+
}
45+
46+
return innerAcc;
47+
}, acc);
48+
},
49+
INITIAL_VALUE),
50+
filter(item => item !== INITIAL_VALUE), take(1)) as Observable<boolean|UrlTree>;
51+
});
52+
}

packages/router/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ts_library(
1616
"//packages/router/testing",
1717
"@rxjs",
1818
"@rxjs//operators",
19+
"@rxjs//testing",
1920
],
2021
)
2122

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
10+
import {TestBed} from '@angular/core/testing';
11+
import {Observable, Observer, of } from 'rxjs';
12+
import {every, mergeMap} from 'rxjs/operators';
13+
import {TestScheduler} from 'rxjs/testing';
14+
15+
import {prioritizedGuardValue} from '../../src/operators/prioritized_guard_value';
16+
import {Router} from '../../src/router';
17+
import {UrlTree} from '../../src/url_tree';
18+
import {RouterTestingModule} from '../../testing/src/router_testing_module';
19+
20+
21+
describe('prioritizedGuardValue operator', () => {
22+
let testScheduler: TestScheduler;
23+
let router: Router;
24+
const TF = {T: true, F: false};
25+
26+
beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); });
27+
beforeEach(() => { testScheduler = new TestScheduler(assertDeepEquals); });
28+
beforeEach(() => { router = TestBed.get(Router); });
29+
30+
it('should return true if all values are true', () => {
31+
testScheduler.run(({hot, cold, expectObservable}) => {
32+
33+
const a = cold(' --(T|)', TF);
34+
const b = cold(' ----------(T|)', TF);
35+
const c = cold(' ------(T|)', TF);
36+
const source = hot('---o--', {o: [a, b, c]});
37+
38+
const expected = ' -------------T--';
39+
40+
expectObservable(source.pipe(prioritizedGuardValue()))
41+
.toBe(expected, TF, /* an error here maybe */);
42+
});
43+
});
44+
45+
it('should return false if observables to the left of false have produced a value', () => {
46+
testScheduler.run(({hot, cold, expectObservable}) => {
47+
48+
const a = cold(' --(T|)', TF);
49+
const b = cold(' ----------(T|)', TF);
50+
const c = cold(' ------(F|)', TF);
51+
const source = hot('---o--', {o: [a, b, c]});
52+
53+
const expected = ' -------------F--';
54+
55+
expectObservable(source.pipe(prioritizedGuardValue()))
56+
.toBe(expected, TF, /* an error here maybe */);
57+
});
58+
});
59+
60+
it('should ignore results for unresolved sets of Observables', () => {
61+
testScheduler.run(({hot, cold, expectObservable}) => {
62+
63+
const a = cold(' --(T|)', TF);
64+
const b = cold(' -------------(T|)', TF);
65+
const c = cold(' ------(F|)', TF);
66+
67+
const z = cold(' ----(T|)', TF);
68+
69+
const source = hot('---o----p----', {o: [a, b, c], p: [z]});
70+
71+
const expected = ' ------------T---';
72+
73+
expectObservable(source.pipe(prioritizedGuardValue()))
74+
.toBe(expected, TF, /* an error here maybe */);
75+
});
76+
});
77+
78+
it('should return UrlTree if higher priority guards have resolved', () => {
79+
testScheduler.run(({hot, cold, expectObservable}) => {
80+
81+
const urlTree = router.parseUrl('/');
82+
83+
const urlLookup = {U: urlTree};
84+
85+
const a = cold(' --(T|)', TF);
86+
const b = cold(' ----------(U|)', urlLookup);
87+
const c = cold(' ------(T|)', TF);
88+
89+
const source = hot('---o---', {o: [a, b, c]});
90+
91+
const expected = ' -------------U---';
92+
93+
expectObservable(source.pipe(prioritizedGuardValue()))
94+
.toBe(expected, urlLookup, /* an error here maybe */);
95+
});
96+
});
97+
98+
it('should return false even with UrlTree if UrlTree is lower priority', () => {
99+
testScheduler.run(({hot, cold, expectObservable}) => {
100+
101+
const urlTree = router.parseUrl('/');
102+
103+
const urlLookup = {U: urlTree};
104+
105+
const a = cold(' --(T|)', TF);
106+
const b = cold(' ----------(F|)', TF);
107+
const c = cold(' ------(U|)', urlLookup);
108+
109+
const source = hot('---o---', {o: [a, b, c]});
110+
111+
const expected = ' -------------F---';
112+
113+
expectObservable(source.pipe(prioritizedGuardValue()))
114+
.toBe(expected, TF, /* an error here maybe */);
115+
});
116+
});
117+
118+
it('should return UrlTree even after a false if the false is lower priority', () => {
119+
testScheduler.run(({hot, cold, expectObservable}) => {
120+
121+
const urlTree = router.parseUrl('/');
122+
123+
const urlLookup = {U: urlTree};
124+
125+
const a = cold(' --(T|)', TF);
126+
const b = cold(' ----------(U|)', urlLookup);
127+
const c = cold(' ------(F|)', TF);
128+
129+
const source = hot('---o---', {o: [a, b, c]});
130+
131+
const expected = ' -------------U----';
132+
133+
expectObservable(source.pipe(prioritizedGuardValue()))
134+
.toBe(expected, urlLookup, /* an error here maybe */);
135+
});
136+
});
137+
138+
it('should return the highest priority UrlTree', () => {
139+
testScheduler.run(({hot, cold, expectObservable}) => {
140+
141+
const urlTreeU = router.parseUrl('/u');
142+
const urlTreeR = router.parseUrl('/r');
143+
const urlTreeL = router.parseUrl('/l');
144+
145+
const urlLookup = {U: urlTreeU, R: urlTreeR, L: urlTreeL};
146+
147+
const a = cold(' ----------(U|)', urlLookup);
148+
const b = cold(' -----(R|)', urlLookup);
149+
const c = cold(' --(L|)', urlLookup);
150+
151+
const source = hot('---o---', {o: [a, b, c]});
152+
153+
const expected = ' -------------U---';
154+
155+
expectObservable(source.pipe(prioritizedGuardValue()))
156+
.toBe(expected, urlLookup, /* an error here maybe */);
157+
});
158+
});
159+
160+
it('should propagate errors', () => {
161+
testScheduler.run(({hot, cold, expectObservable}) => {
162+
163+
const a = cold(' --(T|)', TF);
164+
const b = cold(' ------#', TF);
165+
const c = cold(' ----------(F|)', TF);
166+
const source = hot('---o------', {o: [a, b, c]});
167+
168+
const expected = ' ---------#';
169+
170+
expectObservable(source.pipe(prioritizedGuardValue()))
171+
.toBe(expected, TF, /* an error here maybe */);
172+
});
173+
});
174+
175+
176+
});
177+
178+
179+
180+
function assertDeepEquals(a: any, b: any) {
181+
return expect(a).toEqual(b);
182+
}

0 commit comments

Comments
 (0)