Skip to content

Commit 2c11093

Browse files
vsavkinvicb
authored andcommitted
fix(router): do not require the creation of empty-path routes when no url left
Closes #12133
1 parent 2ced2a8 commit 2c11093

File tree

5 files changed

+107
-10
lines changed

5 files changed

+107
-10
lines changed

modules/@angular/router/src/apply_redirects.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,22 @@ class ApplyRedirects {
146146
const first$ = first.call(concattedProcessedRoutes$, (s: any) => !!s);
147147
return _catch.call(first$, (e: any, _: any): Observable<UrlSegmentGroup> => {
148148
if (e instanceof EmptyError) {
149-
throw new NoMatch(segmentGroup);
149+
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
150+
return of (new UrlSegmentGroup([], {}));
151+
} else {
152+
throw new NoMatch(segmentGroup);
153+
}
150154
} else {
151155
throw e;
152156
}
153157
});
154158
}
155159

160+
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
161+
boolean {
162+
return segments.length === 0 && !segmentGroup.children[outlet];
163+
}
164+
156165
private expandSegmentAgainstRoute(
157166
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
158167
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {

modules/@angular/router/src/recognize.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,16 @@ class Recognizer {
9090
if (!(e instanceof NoMatch)) throw e;
9191
}
9292
}
93-
throw new NoMatch();
93+
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
94+
return [];
95+
} else {
96+
throw new NoMatch();
97+
}
98+
}
99+
100+
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
101+
boolean {
102+
return segments.length === 0 && !segmentGroup.children[outlet];
94103
}
95104

96105
processSegmentAgainstRoute(

modules/@angular/router/test/apply_redirects.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,37 @@ describe('applyRedirects', () => {
515515
});
516516
});
517517
});
518+
519+
describe('empty URL leftovers', () => {
520+
it('should not error when no children matching and no url is left', () => {
521+
checkRedirect(
522+
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
523+
'/a', (t: UrlTree) => { compareTrees(t, tree('a')); });
524+
});
525+
526+
it('should not error when no children matching and no url is left (aux routes)', () => {
527+
checkRedirect(
528+
[{
529+
path: 'a',
530+
component: ComponentA,
531+
children: [
532+
{path: 'b', component: ComponentB},
533+
{path: '', redirectTo: 'c', outlet: 'aux'},
534+
{path: 'c', component: ComponentC, outlet: 'aux'},
535+
]
536+
}],
537+
'/a', (t: UrlTree) => { compareTrees(t, tree('a/(aux:c)')); });
538+
});
539+
540+
it('should error when no children matching and some url is left', () => {
541+
applyRedirects(
542+
null, null, tree('/a/c'),
543+
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}])
544+
.subscribe(
545+
(_) => { throw 'Should not be reached'; },
546+
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a/c\''); });
547+
});
548+
});
518549
});
519550

520551
function checkRedirect(config: Routes, url: string, callback: any): void {

modules/@angular/router/test/integration.spec.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,8 @@ import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
2222
describe('Integration', () => {
2323
beforeEach(() => {
2424
TestBed.configureTestingModule({
25-
imports: [
26-
RouterTestingModule.withRoutes(
27-
[{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}]),
28-
TestModule
29-
]
25+
imports:
26+
[RouterTestingModule.withRoutes([{path: 'simple', component: SimpleCmp}]), TestModule]
3027
});
3128
});
3229

@@ -165,6 +162,29 @@ describe('Integration', () => {
165162
})));
166163
});
167164

165+
it('should not error when no url left and no children are matching',
166+
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
167+
const fixture = createRoot(router, RootCmp);
168+
169+
router.resetConfig([{
170+
path: 'team/:id',
171+
component: TeamCmp,
172+
children: [{path: 'simple', component: SimpleCmp}]
173+
}]);
174+
175+
router.navigateByUrl('/team/33/simple');
176+
advance(fixture);
177+
178+
expect(location.path()).toEqual('/team/33/simple');
179+
expect(fixture.nativeElement).toHaveText('team 33 [ simple, right: ]');
180+
181+
router.navigateByUrl('/team/33');
182+
advance(fixture);
183+
184+
expect(location.path()).toEqual('/team/33');
185+
expect(fixture.nativeElement).toHaveText('team 33 [ , right: ]');
186+
})));
187+
168188
it('should work when an outlet is in an ngIf',
169189
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
170190
const fixture = createRoot(router, RootCmp);
@@ -711,13 +731,13 @@ describe('Integration', () => {
711731
expect(cmp.activations.length).toEqual(1);
712732
expect(cmp.activations[0] instanceof BlankCmp).toBe(true);
713733

714-
router.navigateByUrl('/simple');
734+
router.navigateByUrl('/simple').catch(e => console.log(e));
715735
advance(fixture);
716736

717737
expect(cmp.activations.length).toEqual(2);
718738
expect(cmp.activations[1] instanceof SimpleCmp).toBe(true);
719-
expect(cmp.deactivations.length).toEqual(2);
720-
expect(cmp.deactivations[1] instanceof BlankCmp).toBe(true);
739+
expect(cmp.deactivations.length).toEqual(1);
740+
expect(cmp.deactivations[0] instanceof BlankCmp).toBe(true);
721741
}));
722742

723743
it('should update url and router state before activating components',

modules/@angular/router/test/recognize.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,34 @@ describe('recognize', () => {
608608
});
609609
});
610610

611+
describe('empty URL leftovers', () => {
612+
it('should not throw when no children matching', () => {
613+
checkRecognize(
614+
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
615+
'/a', (s: RouterStateSnapshot) => {
616+
const a = s.firstChild(s.root);
617+
checkActivatedRoute(a, 'a', {}, ComponentA);
618+
});
619+
});
620+
621+
it('should not throw when no children matching (aux routes)', () => {
622+
checkRecognize(
623+
[{
624+
path: 'a',
625+
component: ComponentA,
626+
children: [
627+
{path: 'b', component: ComponentB},
628+
{path: '', component: ComponentC, outlet: 'aux'},
629+
]
630+
}],
631+
'/a', (s: RouterStateSnapshot) => {
632+
const a = s.firstChild(s.root);
633+
checkActivatedRoute(a, 'a', {}, ComponentA);
634+
checkActivatedRoute(a.children[0], '', {}, ComponentC, 'aux');
635+
});
636+
});
637+
});
638+
611639
describe('query parameters', () => {
612640
it('should support query params', () => {
613641
const config = [{path: 'a', component: ComponentA}];

0 commit comments

Comments
 (0)