fix(router): correctly deactivate children with componentless parent (#40196)
During route activation, a componentless route will not have a context created
for it, but the logic continues to recurse so that children are still
activated. This can be seen here:
362f45c4bf/packages/router/src/operators/activate_routes.ts (L151-L158)
The current deactivation logic does not currently account for componentless routes.
This commit adjusts the deactivation logic so that if a context cannot
be retrieved for a given route (because it is componentless), we
continue to recurse and deactivate the children using the same
`parentContexts` in the same way that activation does.
Fixes #20694
PR Close #40196
This commit is contained in:
parent
bff48a0885
commit
13020f904f
|
@ -109,26 +109,27 @@ export class ActivateRoutes {
|
||||||
private deactivateRouteAndOutlet(
|
private deactivateRouteAndOutlet(
|
||||||
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
|
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
|
||||||
const context = parentContexts.getContext(route.value.outlet);
|
const context = parentContexts.getContext(route.value.outlet);
|
||||||
|
// The context could be `null` if we are on a componentless route but there may still be
|
||||||
|
// children that need deactivating.
|
||||||
|
const contexts = context && route.value.component ? context.children : parentContexts;
|
||||||
|
const children: {[outletName: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(route);
|
||||||
|
|
||||||
if (context) {
|
for (const child of Object.values(children)) {
|
||||||
const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
|
this.deactivateRouteAndItsChildren(child, contexts);
|
||||||
const contexts = route.value.component ? context.children : parentContexts;
|
}
|
||||||
|
|
||||||
forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts));
|
if (context && context.outlet) {
|
||||||
|
// Destroy the component
|
||||||
if (context.outlet) {
|
context.outlet.deactivate();
|
||||||
// Destroy the component
|
// Destroy the contexts for all the outlets that were in the component
|
||||||
context.outlet.deactivate();
|
context.children.onOutletDeactivated();
|
||||||
// Destroy the contexts for all the outlets that were in the component
|
|
||||||
context.children.onOutletDeactivated();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private activateChildRoutes(
|
private activateChildRoutes(
|
||||||
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
|
futureNode: TreeNode<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>|null,
|
||||||
contexts: ChildrenOutletContexts): void {
|
contexts: ChildrenOutletContexts): void {
|
||||||
const children: {[outlet: string]: any} = nodeChildrenAsMap(currNode);
|
const children: {[outlet: string]: TreeNode<ActivatedRoute>} = nodeChildrenAsMap(currNode);
|
||||||
futureNode.children.forEach(c => {
|
futureNode.children.forEach(c => {
|
||||||
this.activateRoutes(c, children[c.value.outlet], contexts);
|
this.activateRoutes(c, children[c.value.outlet], contexts);
|
||||||
this.forwardEvent(new ActivationEnd(c.value.snapshot));
|
this.forwardEvent(new ActivationEnd(c.value.snapshot));
|
||||||
|
|
|
@ -390,7 +390,7 @@ describe('Integration', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should advance the parent route after deactivating its children', () => {
|
describe('route activation', () => {
|
||||||
@Component({template: '<router-outlet></router-outlet>'})
|
@Component({template: '<router-outlet></router-outlet>'})
|
||||||
class Parent {
|
class Parent {
|
||||||
constructor(route: ActivatedRoute) {
|
constructor(route: ActivatedRoute) {
|
||||||
|
@ -400,8 +400,24 @@ describe('Integration', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<router-outlet (deactivate)="logDeactivate('primary')"></router-outlet>
|
||||||
|
<router-outlet name="first" (deactivate)="logDeactivate('first')"></router-outlet>
|
||||||
|
<router-outlet name="second" (deactivate)="logDeactivate('second')"></router-outlet>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class NamedOutletHost {
|
||||||
|
logDeactivate(route: string) {
|
||||||
|
log.push(route + ' deactivate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Component({template: 'child1'})
|
@Component({template: 'child1'})
|
||||||
class Child1 {
|
class Child1 {
|
||||||
|
constructor() {
|
||||||
|
log.push('child1 constructor');
|
||||||
|
}
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
log.push('child1 destroy');
|
log.push('child1 destroy');
|
||||||
}
|
}
|
||||||
|
@ -412,20 +428,33 @@ describe('Integration', () => {
|
||||||
constructor() {
|
constructor() {
|
||||||
log.push('child2 constructor');
|
log.push('child2 constructor');
|
||||||
}
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
log.push('child2 destroy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({template: 'child3'})
|
||||||
|
class Child3 {
|
||||||
|
constructor() {
|
||||||
|
log.push('child3 constructor');
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
log.push('child3 destroy');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [Parent, Child1, Child2],
|
declarations: [Parent, NamedOutletHost, Child1, Child2, Child3],
|
||||||
entryComponents: [Parent, Child1, Child2],
|
entryComponents: [Parent, NamedOutletHost, Child1, Child2, Child3],
|
||||||
imports: [RouterModule]
|
imports: [RouterModule]
|
||||||
})
|
})
|
||||||
class TestModule {
|
class TestModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => TestBed.configureTestingModule({imports: [TestModule]}));
|
it('should advance the parent route after deactivating its children', fakeAsync(() => {
|
||||||
|
TestBed.configureTestingModule({imports: [TestModule]});
|
||||||
it('should work',
|
const router = TestBed.inject(Router);
|
||||||
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
const location = TestBed.inject(Location);
|
||||||
const fixture = createRoot(router, RootCmp);
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
router.resetConfig([{
|
router.resetConfig([{
|
||||||
|
@ -446,11 +475,81 @@ describe('Integration', () => {
|
||||||
expect(location.path()).toEqual('/parent/2/child2');
|
expect(location.path()).toEqual('/parent/2/child2');
|
||||||
expect(log).toEqual([
|
expect(log).toEqual([
|
||||||
{id: '1'},
|
{id: '1'},
|
||||||
|
'child1 constructor',
|
||||||
'child1 destroy',
|
'child1 destroy',
|
||||||
{id: '2'},
|
{id: '2'},
|
||||||
'child2 constructor',
|
'child2 constructor',
|
||||||
]);
|
]);
|
||||||
})));
|
}));
|
||||||
|
|
||||||
|
it('should deactivate outlet children with componentless parent', fakeAsync(() => {
|
||||||
|
TestBed.configureTestingModule({imports: [TestModule]});
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.resetConfig([
|
||||||
|
{
|
||||||
|
path: 'named-outlets',
|
||||||
|
component: NamedOutletHost,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
children: [
|
||||||
|
{path: '', component: Child1, outlet: 'first'},
|
||||||
|
{path: '', component: Child2, outlet: 'second'},
|
||||||
|
{path: 'primary', component: Child3},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'about',
|
||||||
|
children: [
|
||||||
|
{path: '', component: Child1, outlet: 'first'},
|
||||||
|
{path: '', component: Child2, outlet: 'second'},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'other',
|
||||||
|
component: Parent,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/named-outlets/home/primary');
|
||||||
|
advance(fixture);
|
||||||
|
expect(log).toEqual([
|
||||||
|
'child3 constructor', // primary outlet always first
|
||||||
|
'child1 constructor',
|
||||||
|
'child2 constructor',
|
||||||
|
]);
|
||||||
|
log.length = 0;
|
||||||
|
|
||||||
|
router.navigateByUrl('/named-outlets/about');
|
||||||
|
advance(fixture);
|
||||||
|
expect(log).toEqual([
|
||||||
|
'child3 destroy',
|
||||||
|
'primary deactivate',
|
||||||
|
'child1 destroy',
|
||||||
|
'first deactivate',
|
||||||
|
'child2 destroy',
|
||||||
|
'second deactivate',
|
||||||
|
'child1 constructor',
|
||||||
|
'child2 constructor',
|
||||||
|
]);
|
||||||
|
log.length = 0;
|
||||||
|
|
||||||
|
router.navigateByUrl('/other');
|
||||||
|
advance(fixture);
|
||||||
|
expect(log).toEqual([
|
||||||
|
'child1 destroy',
|
||||||
|
'first deactivate',
|
||||||
|
'child2 destroy',
|
||||||
|
'second deactivate',
|
||||||
|
// route param subscription from 'Parent' component
|
||||||
|
{},
|
||||||
|
]);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not wait for prior navigations to start a new navigation',
|
it('should not wait for prior navigations to start a new navigation',
|
||||||
|
|
Loading…
Reference in New Issue