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
This commit is contained in:
Andrew Scott 2021-04-09 14:58:55 -07:00 committed by Zach Arend
parent 1b43158af6
commit a1b2718b92
3 changed files with 55 additions and 4 deletions

View File

@ -251,6 +251,8 @@ function hasEmptyPathConfig(node: TreeNode<ActivatedRouteSnapshot>) {
function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
Array<TreeNode<ActivatedRouteSnapshot>> {
const result: Array<TreeNode<ActivatedRouteSnapshot>> = [];
// The set of nodes which contain children that were merged from two duplicate empty path nodes.
const mergedNodes: Set<TreeNode<ActivatedRouteSnapshot>> = new Set();
for (const node of nodes) {
if (!hasEmptyPathConfig(node)) {
@ -262,11 +264,20 @@ function mergeEmptyPathMatches(nodes: Array<TreeNode<ActivatedRouteSnapshot>>):
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<ActivatedRouteSnapshot>[]): void {

View File

@ -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',

View File

@ -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', () => {