diff --git a/packages/router/src/recognize.ts b/packages/router/src/recognize.ts index 64d3f25459..cb12aa8172 100644 --- a/packages/router/src/recognize.ts +++ b/packages/router/src/recognize.ts @@ -251,6 +251,8 @@ function hasEmptyPathConfig(node: TreeNode) { function mergeEmptyPathMatches(nodes: Array>): Array> { const result: Array> = []; + // The set of nodes which contain children that were merged from two duplicate empty path nodes. + const mergedNodes: Set> = new Set(); for (const node of nodes) { if (!hasEmptyPathConfig(node)) { @@ -262,11 +264,20 @@ function mergeEmptyPathMatches(nodes: Array>): result.find(resultNode => node.value.routeConfig === resultNode.value.routeConfig); if (duplicateEmptyPathNode !== undefined) { duplicateEmptyPathNode.children.push(...node.children); + mergedNodes.add(duplicateEmptyPathNode); } else { result.push(node); } } - return result; + // For each node which has children from multiple sources, we need to recompute a new `TreeNode` + // by also merging those children. This is necessary when there are multiple empty path configs in + // a row. Put another way: whenever we combine children of two nodes, we need to also check if any + // of those children can be combined into a single node as well. + for (const mergedNode of mergedNodes) { + const mergedChildren = mergeEmptyPathMatches(mergedNode.children); + result.push(new TreeNode(mergedNode.value, mergedChildren)); + } + return result.filter(n => !mergedNodes.has(n)); } function checkOutletNameUniqueness(nodes: TreeNode[]): void { diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index ca5b677f54..01ccd5495e 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -595,6 +595,39 @@ describe('Integration', () => { {}, ]); })); + + it('should work between aux outlets under two levels of empty path parents', fakeAsync(() => { + TestBed.configureTestingModule({imports: [TestModule]}); + const router = TestBed.inject(Router); + router.resetConfig([{ + path: '', + children: [ + { + path: '', + component: NamedOutletHost, + children: [ + {path: 'one', component: Child1, outlet: 'first'}, + {path: 'two', component: Child2, outlet: 'first'}, + ] + }, + ] + }]); + + const fixture = createRoot(router, RootCmp); + + router.navigateByUrl('/(first:one)'); + advance(fixture); + expect(log).toEqual(['child1 constructor']); + + log.length = 0; + router.navigateByUrl('/(first:two)'); + advance(fixture); + expect(log).toEqual([ + 'child1 destroy', + 'first deactivate', + 'child2 constructor', + ]); + })); }); it('should not wait for prior navigations to start a new navigation', diff --git a/packages/router/test/recognize.spec.ts b/packages/router/test/recognize.spec.ts index e692097332..eff0951df6 100644 --- a/packages/router/test/recognize.spec.ts +++ b/packages/router/test/recognize.spec.ts @@ -626,9 +626,16 @@ describe('recognize', () => { }] }], '(c:c)'); - checkActivatedRoute(s.root.children[0], '', {}, ComponentA); - checkActivatedRoute(s.root.children[0].children[0], '', {}, ComponentB); - checkActivatedRoute(s.root.children[0].children[0].children[0], 'c', {}, ComponentC, 'c'); + const [compAConfig] = s.root.children; + checkActivatedRoute(compAConfig, '', {}, ComponentA); + expect(compAConfig.children.length).toBe(1); + + const [compBConfig] = compAConfig.children; + checkActivatedRoute(compBConfig, '', {}, ComponentB); + expect(compBConfig.children.length).toBe(1); + + const [compCConfig] = compBConfig.children; + checkActivatedRoute(compCConfig, 'c', {}, ComponentC, 'c'); }); it('should not persist a primary segment beyond the boundary of a named outlet match', () => {