feat(router): emit activate/deactivate events when an outlet gets attached/detached (#43333)

Previously the events of `RouterOutlet` (activate/deactivate) were not fired
when an outlet got attached/detached with `RouteReuseStrategy`. The changes configure
`RouterOutlet` to emit events when an outlet gets attached/detached.

Fixes #25521, #20501

PR Close #43333
This commit is contained in:
Dmitrij Kuba
2021-09-03 17:33:07 +03:00
committed by Alex Rickabaugh
parent 8d2b6affcd
commit 4f3beffdbf
3 changed files with 89 additions and 2 deletions
+4
View File
@@ -615,6 +615,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
// (undocumented)
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void;
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void;
attachEvents: EventEmitter<unknown>;
// (undocumented)
get component(): Object;
// (undocumented)
@@ -622,6 +623,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
// (undocumented)
deactivateEvents: EventEmitter<any>;
detach(): ComponentRef<any>;
detachEvents: EventEmitter<unknown>;
// (undocumented)
get isActivated(): boolean;
// (undocumented)
@@ -637,10 +639,12 @@ export interface RouterOutletContract {
activateEvents?: EventEmitter<unknown>;
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void;
attach(ref: ComponentRef<unknown>, activatedRoute: ActivatedRoute): void;
attachEvents?: EventEmitter<unknown>;
component: Object | null;
deactivate(): void;
deactivateEvents?: EventEmitter<unknown>;
detach(): ComponentRef<unknown>;
detachEvents?: EventEmitter<unknown>;
isActivated: boolean;
}
@@ -81,6 +81,18 @@ export interface RouterOutletContract {
* Emits a deactivate event when a component is destroyed.
*/
deactivateEvents?: EventEmitter<unknown>;
/**
* Emits an attached component instance when the `RouteReuseStrategy` instructs to re-attach a
* previously detached subtree.
**/
attachEvents?: EventEmitter<unknown>;
/**
* Emits a detached component instance when the `RouteReuseStrategy` instructs to detach the
* subtree.
*/
detachEvents?: EventEmitter<unknown>;
}
/**
@@ -113,12 +125,17 @@ export interface RouterOutletContract {
* `http://base-path/primary-route-path(outlet-name:route-path)`
*
* A router outlet emits an activate event when a new component is instantiated,
* and a deactivate event when a component is destroyed.
* deactivate event when a component is destroyed.
* An attached event emits when the `RouteReuseStrategy` instructs the outlet to reattach the
* subtree, and the detached event emits when the `RouteReuseStrategy` instructs the outlet to
* detach the subtree.
*
* ```
* <router-outlet
* (activate)='onActivate($event)'
* (deactivate)='onDeactivate($event)'></router-outlet>
* (deactivate)='onDeactivate($event)'
* (attach)='onAttach($event)'
* (detach)='onDetach($event)'></router-outlet>
* ```
*
* @see [Routing tutorial](guide/router-tutorial-toh#named-outlets "Example of a named
@@ -137,6 +154,16 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
@Output('activate') activateEvents = new EventEmitter<any>();
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
/**
* Emits an attached component instance when the `RouteReuseStrategy` instructs to re-attach a
* previously detached subtree.
**/
@Output('attach') attachEvents = new EventEmitter<unknown>();
/**
* Emits a detached component instance when the `RouteReuseStrategy` instructs to detach the
* subtree.
*/
@Output('detach') detachEvents = new EventEmitter<unknown>();
constructor(
private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
@@ -203,6 +230,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
const cmp = this.activated;
this.activated = null;
this._activatedRoute = null;
this.detachEvents.emit(cmp.instance);
return cmp;
}
@@ -213,6 +241,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
this.activated = ref;
this._activatedRoute = activatedRoute;
this.location.insert(ref.hostView);
this.attachEvents.emit(ref.instance);
}
deactivate(): void {
+54
View File
@@ -5831,6 +5831,60 @@ describe('Integration', () => {
expect(router.routeReuseStrategy).toBeInstanceOf(AttachDetachReuseStrategy);
});
it('should emit an event when an outlet gets attached/detached', fakeAsync(() => {
@Component({
selector: 'container',
template:
`<router-outlet (attach)="recordAttached($event)" (detach)="recordDetached($event)"></router-outlet>`
})
class Container {
attachedComponents: any[] = [];
detachedComponents: any[] = [];
recordAttached(component: any): void {
this.attachedComponents.push(component);
}
recordDetached(component: any): void {
this.detachedComponents.push(component);
}
}
TestBed.configureTestingModule({
declarations: [Container],
imports: [RouterTestingModule],
providers: [{provide: RouteReuseStrategy, useClass: AttachDetachReuseStrategy}]
});
const router = TestBed.inject(Router);
const fixture = createRoot(router, Container);
const cmp = fixture.componentInstance;
router.resetConfig([{path: 'a', component: BlankCmp}, {path: 'b', component: SimpleCmp}]);
cmp.attachedComponents = [];
cmp.detachedComponents = [];
router.navigateByUrl('/a');
advance(fixture);
expect(cmp.attachedComponents.length).toEqual(0);
expect(cmp.detachedComponents.length).toEqual(0);
router.navigateByUrl('/b');
advance(fixture);
expect(cmp.attachedComponents.length).toEqual(0);
expect(cmp.detachedComponents.length).toEqual(1);
expect(cmp.detachedComponents[0] instanceof BlankCmp).toBe(true);
// the route will be reused by the `RouteReuseStrategy`
router.navigateByUrl('/a');
advance(fixture);
expect(cmp.attachedComponents.length).toEqual(1);
expect(cmp.attachedComponents[0] instanceof BlankCmp).toBe(true);
expect(cmp.detachedComponents.length).toEqual(1);
expect(cmp.detachedComponents[0] instanceof BlankCmp).toBe(true);
}));
it('should support attaching & detaching fragments',
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
const fixture = createRoot(router, RootCmp);