fix(router): support lazy loading for empty path named outlets (#38379)
In general, the router only matches and loads a single Route config tree. However, named outlets with empty paths are a special case where the router can and should actually match two different `Route`s and ensure that the modules are loaded for each match. This change updates the "ApplyRedirects" stage to ensure that named outlets with empty paths finish loading their configs before proceeding to the next stage in the routing pipe. This is necessary because if the named outlet has `loadChildren` but the associated lazy config is not loaded before following stages attempt to match and activate relevant `Route`s, an error will occur. fixes #12842 PR Close #38379
This commit is contained in:

committed by
Misko Hevery

parent
9ad69c1503
commit
7ad32649c0
@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
import {NgModuleRef} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {fakeAsync, TestBed, tick} from '@angular/core/testing';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {delay} from 'rxjs/operators';
|
||||
import {delay, tap} from 'rxjs/operators';
|
||||
|
||||
import {applyRedirects} from '../src/apply_redirects';
|
||||
import {LoadedRouterConfig, Route, Routes} from '../src/config';
|
||||
@ -482,6 +482,89 @@ describe('applyRedirects', () => {
|
||||
expect((config[0] as any)._loadedConfig).toBe(loadedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
it('should load all matching configurations of empty path, including an auxiliary outlets',
|
||||
fakeAsync(() => {
|
||||
const loadedConfig =
|
||||
new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||
let loadCalls = 0;
|
||||
let loaded: string[] = [];
|
||||
const loader = {
|
||||
load: (injector: any, p: Route) => {
|
||||
loadCalls++;
|
||||
return of(loadedConfig)
|
||||
.pipe(
|
||||
delay(100 * loadCalls),
|
||||
tap(() => loaded.push(p.loadChildren! as string)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const config: Routes =
|
||||
[{path: '', loadChildren: 'root'}, {path: '', loadChildren: 'aux', outlet: 'popup'}];
|
||||
|
||||
applyRedirects(testModule.injector, <any>loader, serializer, tree(''), config).subscribe();
|
||||
expect(loadCalls).toBe(1);
|
||||
tick(100);
|
||||
expect(loaded).toEqual(['root']);
|
||||
tick(200);
|
||||
expect(loadCalls).toBe(2);
|
||||
expect(loaded).toEqual(['root', 'aux']);
|
||||
}));
|
||||
|
||||
it('loads only the first match when two Routes with the same outlet have the same path', () => {
|
||||
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||
let loadCalls = 0;
|
||||
let loaded: string[] = [];
|
||||
const loader = {
|
||||
load: (injector: any, p: Route) => {
|
||||
loadCalls++;
|
||||
return of(loadedConfig)
|
||||
.pipe(
|
||||
tap(() => loaded.push(p.loadChildren! as string)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const config: Routes =
|
||||
[{path: 'a', loadChildren: 'first'}, {path: 'a', loadChildren: 'second'}];
|
||||
|
||||
applyRedirects(testModule.injector, <any>loader, serializer, tree('a'), config).subscribe();
|
||||
expect(loadCalls).toBe(1);
|
||||
expect(loaded).toEqual(['first']);
|
||||
});
|
||||
|
||||
it('should load the configuration of empty root path if the entry is an aux outlet',
|
||||
fakeAsync(() => {
|
||||
const loadedConfig =
|
||||
new LoadedRouterConfig([{path: '', component: ComponentA}], testModule);
|
||||
let loaded: string[] = [];
|
||||
const rootDelay = 100;
|
||||
const auxDelay = 1;
|
||||
const loader = {
|
||||
load: (injector: any, p: Route) => {
|
||||
const delayMs = p.loadChildren! as string === 'aux' ? auxDelay : rootDelay;
|
||||
return of(loadedConfig)
|
||||
.pipe(
|
||||
delay(delayMs),
|
||||
tap(() => loaded.push(p.loadChildren! as string)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const config: Routes = [
|
||||
// Define aux route first so it matches before the primary outlet
|
||||
{path: 'modal', loadChildren: 'aux', outlet: 'popup'},
|
||||
{path: '', loadChildren: 'root'},
|
||||
];
|
||||
|
||||
applyRedirects(testModule.injector, <any>loader, serializer, tree('(popup:modal)'), config)
|
||||
.subscribe();
|
||||
tick(auxDelay);
|
||||
expect(loaded).toEqual(['aux']);
|
||||
tick(rootDelay);
|
||||
expect(loaded).toEqual(['aux', 'root']);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('empty paths', () => {
|
||||
@ -754,6 +837,46 @@ describe('applyRedirects', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple matches with empty path named outlets', () => {
|
||||
it('should work with redirects when other outlet comes before the one being activated', () => {
|
||||
applyRedirects(
|
||||
testModule.injector, null!, serializer, tree(''),
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
children: [
|
||||
{path: '', component: ComponentA, outlet: 'aux'},
|
||||
{path: '', redirectTo: 'b', pathMatch: 'full'},
|
||||
{path: 'b', component: ComponentB},
|
||||
],
|
||||
},
|
||||
])
|
||||
.subscribe(
|
||||
(tree: UrlTree) => {
|
||||
expect(tree.toString()).toEqual('/b');
|
||||
},
|
||||
() => {
|
||||
fail('should not be reached');
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when entry point is named outlet', () => {
|
||||
applyRedirects(
|
||||
testModule.injector, null!, serializer, tree('(popup:modal)'),
|
||||
[
|
||||
{path: '', component: ComponentA},
|
||||
{path: 'modal', component: ComponentB, outlet: 'popup'},
|
||||
])
|
||||
.subscribe(
|
||||
(tree: UrlTree) => {
|
||||
expect(tree.toString()).toEqual('/(popup:modal)');
|
||||
},
|
||||
(e) => {
|
||||
fail('should not be reached' + e.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirecting to named outlets', () => {
|
||||
it('should work when using absolute redirects', () => {
|
||||
checkRedirect(
|
||||
|
Reference in New Issue
Block a user