From ee39cf62bc3f3d60d1dfe7064fe34565e5429b84 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 28 Jun 2022 10:37:04 -0700 Subject: [PATCH] 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 --- .../router/src/directives/router_outlet.ts | 5 ++- .../test/regression_integration.spec.ts | 45 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/router/src/directives/router_outlet.ts b/packages/router/src/directives/router_outlet.ts index d1aa228e20c..3eac17a6af1 100644 --- a/packages/router/src/directives/router_outlet.ts +++ b/packages/router/src/directives/router_outlet.ts @@ -183,7 +183,10 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { /** @nodoc */ ngOnDestroy(): void { - this.parentContexts.onChildOutletDestroyed(this.name); + // Ensure that the registered outlet is this one before removing it on the context. + if (this.parentContexts.getContext(this.name)?.outlet === this) { + this.parentContexts.onChildOutletDestroyed(this.name); + } } /** @nodoc */ diff --git a/packages/router/test/regression_integration.spec.ts b/packages/router/test/regression_integration.spec.ts index 5a7702afddc..2cac36e029b 100644 --- a/packages/router/test/regression_integration.spec.ts +++ b/packages/router/test/regression_integration.spec.ts @@ -10,7 +10,7 @@ import {CommonModule, Location} from '@angular/common'; import {SpyLocation} from '@angular/common/testing'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, NgModule, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {Resolve, Router} from '@angular/router'; +import {ChildrenOutletContexts, Resolve, Router} from '@angular/router'; import {RouterTestingModule} from '@angular/router/testing'; import {of} from 'rxjs'; import {delay, mapTo} from 'rxjs/operators'; @@ -339,6 +339,49 @@ describe('Integration', () => { expect(fixture.nativeElement.innerHTML).toContain('one'); })); }); + + it('should not unregister outlet if a different one already exists #36711, 32453', async () => { + @Component({ + template: ` + + + `, + }) + class TestCmp { + outlet1 = true; + outlet2 = false; + } + + @Component({template: ''}) + class EmptyCmp { + } + + TestBed.configureTestingModule({ + imports: [CommonModule, RouterTestingModule.withRoutes([{path: '**', component: EmptyCmp}])], + declarations: [TestCmp, EmptyCmp] + }); + const fixture = TestBed.createComponent(TestCmp); + const contexts = TestBed.inject(ChildrenOutletContexts); + await TestBed.inject(Router).navigateByUrl('/'); + fixture.detectChanges(); + + expect(contexts.getContext('primary')).toBeDefined(); + expect(contexts.getContext('primary')?.outlet).not.toBeNull(); + + // Show the second outlet. Applications shouldn't really have more than one outlet but there can + // be timing issues between destroying and recreating a second one in some cases: + // https://github.com/angular/angular/issues/36711, + // https://github.com/angular/angular/issues/32453 + fixture.componentInstance.outlet2 = true; + fixture.detectChanges(); + expect(contexts.getContext('primary')?.outlet).not.toBeNull(); + + fixture.componentInstance.outlet1 = false; + fixture.detectChanges(); + // Destroying the first one show not clear the outlet context because the second one takes over + // as the registered outlet. + expect(contexts.getContext('primary')?.outlet).not.toBeNull(); + }); }); function advance(fixture: ComponentFixture): void {