Skip to content

Commit d6fac9e

Browse files
atscottdylhunn
authored andcommitted
fix(router): Ensure that new RouterOutlet instances work after old ones are destroyed (#46554)
There can be timing issues with removing an old outlet and creating a new one to replace it. Before calling `onChildOutletDestroyed`, the `RouterOutlet` will first check to ensure that it is still the one registered for that outlet name. Fixes #36711 Fixes #32453 PR Close #46554
1 parent 7478722 commit d6fac9e

File tree

2 files changed

+48
-2
lines changed

2 files changed

+48
-2
lines changed

packages/router/src/directives/router_outlet.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,10 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
183183

184184
/** @nodoc */
185185
ngOnDestroy(): void {
186-
this.parentContexts.onChildOutletDestroyed(this.name);
186+
// Ensure that the registered outlet is this one before removing it on the context.
187+
if (this.parentContexts.getContext(this.name)?.outlet === this) {
188+
this.parentContexts.onChildOutletDestroyed(this.name);
189+
}
187190
}
188191

189192
/** @nodoc */

packages/router/test/regression_integration.spec.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {CommonModule, Location} from '@angular/common';
1010
import {SpyLocation} from '@angular/common/testing';
1111
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core';
1212
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
13-
import {Resolve, Router} from '@angular/router';
13+
import {ChildrenOutletContexts, Resolve, Router} from '@angular/router';
1414
import {RouterTestingModule} from '@angular/router/testing';
1515
import {of} from 'rxjs';
1616
import {delay, mapTo} from 'rxjs/operators';
@@ -339,6 +339,49 @@ describe('Integration', () => {
339339
expect(fixture.nativeElement.innerHTML).toContain('one');
340340
}));
341341
});
342+
343+
it('should not unregister outlet if a different one already exists #36711, 32453', async () => {
344+
@Component({
345+
template: `
346+
<router-outlet *ngIf="outlet1"></router-outlet>
347+
<router-outlet *ngIf="outlet2"></router-outlet>
348+
`,
349+
})
350+
class TestCmp {
351+
outlet1 = true;
352+
outlet2 = false;
353+
}
354+
355+
@Component({template: ''})
356+
class EmptyCmp {
357+
}
358+
359+
TestBed.configureTestingModule({
360+
imports: [CommonModule, RouterTestingModule.withRoutes([{path: '**', component: EmptyCmp}])],
361+
declarations: [TestCmp, EmptyCmp]
362+
});
363+
const fixture = TestBed.createComponent(TestCmp);
364+
const contexts = TestBed.inject(ChildrenOutletContexts);
365+
await TestBed.inject(Router).navigateByUrl('/');
366+
fixture.detectChanges();
367+
368+
expect(contexts.getContext('primary')).toBeDefined();
369+
expect(contexts.getContext('primary')?.outlet).not.toBeNull();
370+
371+
// Show the second outlet. Applications shouldn't really have more than one outlet but there can
372+
// be timing issues between destroying and recreating a second one in some cases:
373+
// https://github.com/angular/angular/issues/36711,
374+
// https://github.com/angular/angular/issues/32453
375+
fixture.componentInstance.outlet2 = true;
376+
fixture.detectChanges();
377+
expect(contexts.getContext('primary')?.outlet).not.toBeNull();
378+
379+
fixture.componentInstance.outlet1 = false;
380+
fixture.detectChanges();
381+
// Destroying the first one show not clear the outlet context because the second one takes over
382+
// as the registered outlet.
383+
expect(contexts.getContext('primary')?.outlet).not.toBeNull();
384+
});
342385
});
343386

344387
function advance<T>(fixture: ComponentFixture<T>): void {

0 commit comments

Comments
 (0)