Skip to content

Commit 7784ce8

Browse files
committed
fix(router): fix lazy loading of aux routes
Fixes #10981
1 parent 1135563 commit 7784ce8

File tree

8 files changed

+131
-17
lines changed

8 files changed

+131
-17
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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 {Component} from '@angular/core';
10+
11+
/**
12+
* This component is used internally within the router to be a placeholder when an empty
13+
* router-outlet is needed. For example, with a config such as:
14+
*
15+
* `{path: 'parent', outlet: 'nav', children: [...]}`
16+
*
17+
* In order to render, there needs to be a component on this config, which will default
18+
* to this `EmptyOutletComponent`.
19+
*/
20+
@Component({template: `<router-outlet></router-outlet>`})
21+
export class EmptyOutletComponent {
22+
}

packages/router/src/config.ts

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

99
import {NgModuleFactory, NgModuleRef, Type} from '@angular/core';
1010
import {Observable} from 'rxjs';
11+
import {EmptyOutletComponent} from './components/empty_outlet';
1112
import {PRIMARY_OUTLET} from './shared';
1213
import {UrlSegment, UrlSegmentGroup} from './url_tree';
1314

@@ -412,9 +413,10 @@ function validateNode(route: Route, fullPath: string): void {
412413
if (Array.isArray(route)) {
413414
throw new Error(`Invalid configuration of route '${fullPath}': Array cannot be specified`);
414415
}
415-
if (!route.component && (route.outlet && route.outlet !== PRIMARY_OUTLET)) {
416+
if (!route.component && !route.children && !route.loadChildren &&
417+
(route.outlet && route.outlet !== PRIMARY_OUTLET)) {
416418
throw new Error(
417-
`Invalid configuration of route '${fullPath}': a componentless route cannot have a named outlet set`);
419+
`Invalid configuration of route '${fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`);
418420
}
419421
if (route.redirectTo && route.children) {
420422
throw new Error(
@@ -477,8 +479,14 @@ function getFullPath(parentPath: string, currentRoute: Route): string {
477479
}
478480
}
479481

480-
481-
export function copyConfig(r: Route): Route {
482-
const children = r.children && r.children.map(copyConfig);
483-
return children ? {...r, children} : {...r};
482+
/**
483+
* Makes a copy of the config and adds any default required properties.
484+
*/
485+
export function standardizeConfig(r: Route): Route {
486+
const children = r.children && r.children.map(standardizeConfig);
487+
const c = children ? {...r, children} : {...r};
488+
if (!c.component && (children || c.loadChildren) && (c.outlet && c.outlet !== PRIMARY_OUTLET)) {
489+
c.component = EmptyOutletComponent;
490+
}
491+
return c;
484492
}

packages/router/src/private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
*/
88

99

10+
export {EmptyOutletComponent as ɵEmptyOutletComponent} from './components/empty_outlet';
1011
export {ROUTER_PROVIDERS as ɵROUTER_PROVIDERS} from './router_module';
1112
export {flatten as ɵflatten} from './utils/collection';

packages/router/src/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {BehaviorSubject, Observable, Subject, Subscription, of } from 'rxjs';
1212
import {concatMap, map, mergeMap} from 'rxjs/operators';
1313

1414
import {applyRedirects} from './apply_redirects';
15-
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, copyConfig, validateConfig} from './config';
15+
import {LoadedRouterConfig, QueryParamsHandling, Route, Routes, standardizeConfig, validateConfig} from './config';
1616
import {createRouterState} from './create_router_state';
1717
import {createUrlTree} from './create_url_tree';
1818
import {ActivationEnd, ChildActivationEnd, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, NavigationTrigger, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
@@ -357,7 +357,7 @@ export class Router {
357357
*/
358358
resetConfig(config: Routes): void {
359359
validateConfig(config);
360-
this.config = config.map(copyConfig);
360+
this.config = config.map(standardizeConfig);
361361
this.navigated = false;
362362
this.lastSuccessfulId = -1;
363363
}

packages/router/src/router_config_loader.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Compiler, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoad
1010
// TODO(i): switch to fromPromise once it's expored in rxjs
1111
import {Observable, from, of } from 'rxjs';
1212
import {map, mergeMap} from 'rxjs/operators';
13-
import {LoadChildren, LoadedRouterConfig, Route, copyConfig} from './config';
13+
import {LoadChildren, LoadedRouterConfig, Route, standardizeConfig} from './config';
1414
import {flatten, wrapIntoObservable} from './utils/collection';
1515

1616
/**
@@ -39,7 +39,8 @@ export class RouterConfigLoader {
3939

4040
const module = factory.create(parentInjector);
4141

42-
return new LoadedRouterConfig(flatten(module.injector.get(ROUTES)).map(copyConfig), module);
42+
return new LoadedRouterConfig(
43+
flatten(module.injector.get(ROUTES)).map(standardizeConfig), module);
4344
}));
4445
}
4546

packages/router/src/router_module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, A
1111
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
1212
import {Subject, of } from 'rxjs';
1313

14+
import {EmptyOutletComponent} from './components/empty_outlet';
1415
import {Route, Routes} from './config';
1516
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
1617
import {RouterLinkActive} from './directives/router_link_active';
@@ -35,7 +36,8 @@ import {flatten} from './utils/collection';
3536
*
3637
*
3738
*/
38-
const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkWithHref, RouterLinkActive];
39+
const ROUTER_DIRECTIVES =
40+
[RouterOutlet, RouterLink, RouterLinkWithHref, RouterLinkActive, EmptyOutletComponent];
3941

4042
/**
4143
* @description
@@ -127,7 +129,11 @@ export function routerNgProbeToken() {
127129
*
128130
*
129131
*/
130-
@NgModule({declarations: ROUTER_DIRECTIVES, exports: ROUTER_DIRECTIVES})
132+
@NgModule({
133+
declarations: ROUTER_DIRECTIVES,
134+
exports: ROUTER_DIRECTIVES,
135+
entryComponents: [EmptyOutletComponent]
136+
})
131137
export class RouterModule {
132138
// Note: We are injecting the Router so it gets created eagerly...
133139
constructor(@Optional() @Inject(ROUTER_FORROOT_GUARD) guard: any, @Optional() router: Router) {}

packages/router/test/config.spec.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,27 @@ describe('config', () => {
123123
}).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/);
124124
});
125125

126-
it('should throw when pathPatch is invalid', () => {
126+
it('should throw when pathMatch is invalid', () => {
127127
expect(() => { validateConfig([{path: 'a', pathMatch: 'invalid', component: ComponentB}]); })
128128
.toThrowError(
129129
/Invalid configuration of route 'a': pathMatch can only be set to 'prefix' or 'full'/);
130130
});
131131

132-
it('should throw when pathPatch is invalid', () => {
133-
expect(() => { validateConfig([{path: 'a', outlet: 'aux', children: []}]); })
132+
it('should throw when path/outlet combination is invalid', () => {
133+
expect(() => { validateConfig([{path: 'a', outlet: 'aux'}]); })
134134
.toThrowError(
135-
/Invalid configuration of route 'a': a componentless route cannot have a named outlet set/);
136-
135+
/Invalid configuration of route 'a': a componentless route without children or loadChildren cannot have a named outlet set/);
137136
expect(() => validateConfig([{path: 'a', outlet: '', children: []}])).not.toThrow();
138137
expect(() => validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}]))
139138
.not.toThrow();
140139
});
140+
141+
it('should not throw when path/outlet combination is valid', () => {
142+
expect(() => { validateConfig([{path: 'a', outlet: 'aux', children: []}]); }).not.toThrow();
143+
expect(() => {
144+
validateConfig([{path: 'a', outlet: 'aux', loadChildren: 'child'}]);
145+
}).not.toThrow();
146+
});
141147
});
142148
});
143149

packages/router/test/integration.spec.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3366,6 +3366,72 @@ describe('Integration', () => {
33663366
expect(location.path()).toEqual('/lazy2/loaded');
33673367
})));
33683368

3369+
it('should allow lazy loaded module in named outlet',
3370+
fakeAsync(inject(
3371+
[Router, NgModuleFactoryLoader], (router: Router, loader: SpyNgModuleFactoryLoader) => {
3372+
3373+
@Component({selector: 'lazy', template: 'lazy-loaded'})
3374+
class LazyComponent {
3375+
}
3376+
3377+
@NgModule({
3378+
declarations: [LazyComponent],
3379+
imports: [RouterModule.forChild([{path: '', component: LazyComponent}])]
3380+
})
3381+
class LazyLoadedModule {
3382+
}
3383+
3384+
loader.stubbedModules = {lazyModule: LazyLoadedModule};
3385+
3386+
const fixture = createRoot(router, RootCmp);
3387+
3388+
router.resetConfig([{
3389+
path: 'team/:id',
3390+
component: TeamCmp,
3391+
children: [
3392+
{path: 'user/:name', component: UserCmp},
3393+
{path: 'lazy', loadChildren: 'lazyModule', outlet: 'right'},
3394+
]
3395+
}]);
3396+
3397+
3398+
router.navigateByUrl('/team/22/user/john');
3399+
advance(fixture);
3400+
3401+
expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: ]');
3402+
3403+
router.navigateByUrl('/team/22/(user/john//right:lazy)');
3404+
advance(fixture);
3405+
3406+
expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: lazy-loaded ]');
3407+
})));
3408+
3409+
it('should allow componentless named outlet to render children',
3410+
fakeAsync(inject(
3411+
[Router, NgModuleFactoryLoader], (router: Router, loader: SpyNgModuleFactoryLoader) => {
3412+
3413+
const fixture = createRoot(router, RootCmp);
3414+
3415+
router.resetConfig([{
3416+
path: 'team/:id',
3417+
component: TeamCmp,
3418+
children: [
3419+
{path: 'user/:name', component: UserCmp},
3420+
{path: 'simple', outlet: 'right', children: [{path: '', component: SimpleCmp}]},
3421+
]
3422+
}]);
3423+
3424+
3425+
router.navigateByUrl('/team/22/user/john');
3426+
advance(fixture);
3427+
3428+
expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: ]');
3429+
3430+
router.navigateByUrl('/team/22/(user/john//right:simple)');
3431+
advance(fixture);
3432+
3433+
expect(fixture.nativeElement).toHaveText('team 22 [ user john, right: simple ]');
3434+
})));
33693435

33703436
describe('should use the injector of the lazily-loaded configuration', () => {
33713437
class LazyLoadedServiceDefinedInModule {}
@@ -4102,6 +4168,10 @@ function createRoot(router: Router, type: any): ComponentFixture<any> {
41024168
return f;
41034169
}
41044170

4171+
@Component({selector: 'lazy', template: 'lazy-loaded'})
4172+
class LazyComponent {
4173+
}
4174+
41054175

41064176
@NgModule({
41074177
imports: [RouterTestingModule, CommonModule],

0 commit comments

Comments
 (0)