Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(router): make RouterOutlet name an Input so it can be set dynami…
…cally (#46569) This commit updates the 'name' of `RouterOutlet` to be an `Input` rather than an attribute. Note that this change does not affect `[attr.name]=` because those already would not have worked. The static name was only read in the constructor and if it wasn't available, it would use 'PRIMARY' instead. fixes #12522 BREAKING CHANGE: Previously, the `RouterOutlet` would immediately instantiate the component being activated during navigation. Now the component is not instantiated until the change detection runs. This could affect tests which do not trigger change detection after a router navigation. In rarer cases, this can affect production code that relies on the exact timing of component availability. PR Close #46569
- Loading branch information
1 parent
07d9a27
commit c3f8579
Showing
4 changed files
with
224 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1892,9 +1892,6 @@ | |
{ | ||
"name": "ɵɵinject" | ||
}, | ||
{ | ||
"name": "ɵɵinjectAttribute" | ||
}, | ||
{ | ||
"name": "ɵɵlistener" | ||
}, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {CommonModule, NgForOf} from '@angular/common'; | ||
import {Component, Type} from '@angular/core'; | ||
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; | ||
import {Router, RouterModule, RouterOutlet} from '@angular/router/src'; | ||
import {RouterTestingModule} from '@angular/router/testing'; | ||
|
||
|
||
describe('router outlet name', () => { | ||
it('should support name binding', fakeAsync(() => { | ||
@Component({ | ||
standalone: true, | ||
template: '<router-outlet [name]="name"></router-outlet>', | ||
imports: [RouterOutlet], | ||
}) | ||
class RootCmp { | ||
name = 'popup'; | ||
} | ||
|
||
@Component({ | ||
template: 'popup component', | ||
standalone: true, | ||
}) | ||
class PopupCmp { | ||
} | ||
|
||
TestBed.configureTestingModule({ | ||
imports: | ||
[RouterTestingModule.withRoutes([{path: '', outlet: 'popup', component: PopupCmp}])] | ||
}); | ||
const router = TestBed.inject(Router); | ||
const fixture = createRoot(router, RootCmp); | ||
expect(fixture.nativeElement.innerHTML).toContain('popup component'); | ||
})); | ||
|
||
it('should be able to change the name of the outlet', fakeAsync(() => { | ||
@Component({ | ||
standalone: true, | ||
template: '<router-outlet [name]="name"></router-outlet>', | ||
imports: [RouterOutlet], | ||
}) | ||
class RootCmp { | ||
name = ''; | ||
} | ||
|
||
@Component({ | ||
template: 'hello world', | ||
standalone: true, | ||
}) | ||
class GreetingCmp { | ||
} | ||
|
||
@Component({ | ||
template: 'goodbye cruel world', | ||
standalone: true, | ||
}) | ||
class FarewellCmp { | ||
} | ||
|
||
TestBed.configureTestingModule({ | ||
imports: [RouterTestingModule.withRoutes([ | ||
{path: '', outlet: 'greeting', component: GreetingCmp}, | ||
{path: '', outlet: 'farewell', component: FarewellCmp}, | ||
])] | ||
}); | ||
const router = TestBed.inject(Router); | ||
const fixture = createRoot(router, RootCmp); | ||
|
||
expect(fixture.nativeElement.innerHTML).not.toContain('goodbye'); | ||
expect(fixture.nativeElement.innerHTML).not.toContain('hello'); | ||
|
||
fixture.componentInstance.name = 'greeting'; | ||
advance(fixture); | ||
expect(fixture.nativeElement.innerHTML).toContain('hello'); | ||
expect(fixture.nativeElement.innerHTML).not.toContain('goodbye'); | ||
|
||
fixture.componentInstance.name = 'goodbye'; | ||
advance(fixture); | ||
expect(fixture.nativeElement.innerHTML).toContain('goodbye'); | ||
expect(fixture.nativeElement.innerHTML).not.toContain('hello'); | ||
})); | ||
|
||
it('should support outlets in ngFor', fakeAsync(() => { | ||
@Component({ | ||
standalone: true, | ||
template: ` | ||
<div *ngFor="let outlet of outlets"> | ||
<router-outlet [name]="outlet"></router-outlet> | ||
</div> | ||
`, | ||
imports: [RouterOutlet, NgForOf], | ||
}) | ||
class RootCmp { | ||
outlets = ['outlet1', 'outlet2', 'outlet3']; | ||
} | ||
|
||
@Component({ | ||
template: 'component 1', | ||
standalone: true, | ||
}) | ||
class Cmp1 { | ||
} | ||
|
||
@Component({ | ||
template: 'component 2', | ||
standalone: true, | ||
}) | ||
class Cmp2 { | ||
} | ||
|
||
@Component({ | ||
template: 'component 3', | ||
standalone: true, | ||
}) | ||
class Cmp3 { | ||
} | ||
|
||
TestBed.configureTestingModule({ | ||
imports: [RouterTestingModule.withRoutes([ | ||
{path: '1', outlet: 'outlet1', component: Cmp1}, | ||
{path: '2', outlet: 'outlet2', component: Cmp2}, | ||
{path: '3', outlet: 'outlet3', component: Cmp3}, | ||
])] | ||
}); | ||
const router = TestBed.inject(Router); | ||
const fixture = createRoot(router, RootCmp); | ||
|
||
router.navigate([{outlets: {'outlet1': '1'}}]); | ||
advance(fixture); | ||
expect(fixture.nativeElement.innerHTML).toContain('component 1'); | ||
expect(fixture.nativeElement.innerHTML).not.toContain('component 2'); | ||
expect(fixture.nativeElement.innerHTML).not.toContain('component 3'); | ||
|
||
router.navigate([{outlets: {'outlet1': null, 'outlet2': '2', 'outlet3': '3'}}]); | ||
advance(fixture); | ||
expect(fixture.nativeElement.innerHTML).not.toContain('component 1'); | ||
expect(fixture.nativeElement.innerHTML).toMatch('.*component 2.*component 3'); | ||
|
||
// reverse the outlets | ||
fixture.componentInstance.outlets = ['outlet3', 'outlet2', 'outlet1']; | ||
router.navigate([{outlets: {'outlet1': '1', 'outlet2': '2', 'outlet3': '3'}}]); | ||
advance(fixture); | ||
expect(fixture.nativeElement.innerHTML).toMatch('.*component 3.*component 2.*component 1'); | ||
})); | ||
}); | ||
|
||
function advance(fixture: ComponentFixture<any>, millis?: number): void { | ||
tick(millis); | ||
fixture.detectChanges(); | ||
} | ||
|
||
function createRoot<T>(router: Router, type: Type<T>): ComponentFixture<T> { | ||
const f = TestBed.createComponent(type); | ||
advance(f); | ||
router.initialNavigation(); | ||
advance(f); | ||
return f; | ||
} |