Skip to content

Commit 1179dc8

Browse files
atscottzarend
authored andcommitted
fix(router): recursively merge empty path matches (#41584)
When recognizing routes, the router merges nodes which map to the same empty path config. This is because auxiliary outlets under empty path parents need to match the parent config. This would result in two outlet matches for that parent which need to be combined into a single node: The regular 'primary' match and the match for the auxiliary outlet. In addition, the children of the merged nodes should also be merged to account for multiple levels of empty path parents. Fixes #41481 PR Close #41584
1 parent a5fe8b9 commit 1179dc8

File tree

3 files changed

+55
-4
lines changed

3 files changed

+55
-4
lines changed

packages/router/src/recognize.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ function hasEmptyPathConfig(node: TreeNode<ActivatedRouteSnapshot>) {
251251
function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
252252
Array<TreeNode<ActivatedRouteSnapshot>> {
253253
const result: Array<TreeNode<ActivatedRouteSnapshot>> = [];
254+
// The set of nodes which contain children that were merged from two duplicate empty path nodes.
255+
const mergedNodes: Set<TreeNode<ActivatedRouteSnapshot>> = new Set();
254256

255257
for (const node of nodes) {
256258
if (!hasEmptyPathConfig(node)) {
@@ -262,11 +264,20 @@ function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
262264
result.find(resultNode => node.value.routeConfig === resultNode.value.routeConfig);
263265
if (duplicateEmptyPathNode !== undefined) {
264266
duplicateEmptyPathNode.children.push(...node.children);
267+
mergedNodes.add(duplicateEmptyPathNode);
265268
} else {
266269
result.push(node);
267270
}
268271
}
269-
return result;
272+
// For each node which has children from multiple sources, we need to recompute a new `TreeNode`
273+
// by also merging those children. This is necessary when there are multiple empty path configs in
274+
// a row. Put another way: whenever we combine children of two nodes, we need to also check if any
275+
// of those children can be combined into a single node as well.
276+
for (const mergedNode of mergedNodes) {
277+
const mergedChildren = mergeEmptyPathMatches(mergedNode.children);
278+
result.push(new TreeNode(mergedNode.value, mergedChildren));
279+
}
280+
return result.filter(n => !mergedNodes.has(n));
270281
}
271282

272283
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {

packages/router/test/integration.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,39 @@ describe('Integration', () => {
595595
{},
596596
]);
597597
}));
598+
599+
it('should work between aux outlets under two levels of empty path parents', fakeAsync(() => {
600+
TestBed.configureTestingModule({imports: [TestModule]});
601+
const router = TestBed.inject(Router);
602+
router.resetConfig([{
603+
path: '',
604+
children: [
605+
{
606+
path: '',
607+
component: NamedOutletHost,
608+
children: [
609+
{path: 'one', component: Child1, outlet: 'first'},
610+
{path: 'two', component: Child2, outlet: 'first'},
611+
]
612+
},
613+
]
614+
}]);
615+
616+
const fixture = createRoot(router, RootCmp);
617+
618+
router.navigateByUrl('/(first:one)');
619+
advance(fixture);
620+
expect(log).toEqual(['child1 constructor']);
621+
622+
log.length = 0;
623+
router.navigateByUrl('/(first:two)');
624+
advance(fixture);
625+
expect(log).toEqual([
626+
'child1 destroy',
627+
'first deactivate',
628+
'child2 constructor',
629+
]);
630+
}));
598631
});
599632

600633
it('should not wait for prior navigations to start a new navigation',

packages/router/test/recognize.spec.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,16 @@ describe('recognize', () => {
626626
}]
627627
}],
628628
'(c:c)');
629-
checkActivatedRoute(s.root.children[0], '', {}, ComponentA);
630-
checkActivatedRoute(s.root.children[0].children[0], '', {}, ComponentB);
631-
checkActivatedRoute(s.root.children[0].children[0].children[0], 'c', {}, ComponentC, 'c');
629+
const [compAConfig] = s.root.children;
630+
checkActivatedRoute(compAConfig, '', {}, ComponentA);
631+
expect(compAConfig.children.length).toBe(1);
632+
633+
const [compBConfig] = compAConfig.children;
634+
checkActivatedRoute(compBConfig, '', {}, ComponentB);
635+
expect(compBConfig.children.length).toBe(1);
636+
637+
const [compCConfig] = compBConfig.children;
638+
checkActivatedRoute(compCConfig, 'c', {}, ComponentC, 'c');
632639
});
633640

634641
it('should not persist a primary segment beyond the boundary of a named outlet match', () => {

0 commit comments

Comments
 (0)