From 4f3beffdbfa974b380b2225f163d363dd17e10bd Mon Sep 17 00:00:00 2001 From: Dmitrij Kuba Date: Fri, 3 Sep 2021 17:33:07 +0300 Subject: [PATCH] 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 --- goldens/public-api/router/router.md | 4 ++ .../router/src/directives/router_outlet.ts | 33 +++++++++++- packages/router/test/integration.spec.ts | 54 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/goldens/public-api/router/router.md b/goldens/public-api/router/router.md index 41b1855ba483b..36954bd2cc7e8 100644 --- a/goldens/public-api/router/router.md +++ b/goldens/public-api/router/router.md @@ -615,6 +615,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { // (undocumented) activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void; attach(ref: ComponentRef, activatedRoute: ActivatedRoute): void; + attachEvents: EventEmitter; // (undocumented) get component(): Object; // (undocumented) @@ -622,6 +623,7 @@ export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract { // (undocumented) deactivateEvents: EventEmitter; detach(): ComponentRef; + detachEvents: EventEmitter; // (undocumented) get isActivated(): boolean; // (undocumented) @@ -637,10 +639,12 @@ export interface RouterOutletContract { activateEvents?: EventEmitter; activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void; attach(ref: ComponentRef, activatedRoute: ActivatedRoute): void; + attachEvents?: EventEmitter; component: Object | null; deactivate(): void; deactivateEvents?: EventEmitter; detach(): ComponentRef; + detachEvents?: EventEmitter; isActivated: boolean; } diff --git a/packages/router/src/directives/router_outlet.ts b/packages/router/src/directives/router_outlet.ts index 6e363e7565a1d..b2f1a7c04355d 100644 --- a/packages/router/src/directives/router_outlet.ts +++ b/packages/router/src/directives/router_outlet.ts @@ -81,6 +81,18 @@ export interface RouterOutletContract { * Emits a deactivate event when a component is destroyed. */ deactivateEvents?: EventEmitter; + + /** + * Emits an attached component instance when the `RouteReuseStrategy` instructs to re-attach a + * previously detached subtree. + **/ + attachEvents?: EventEmitter; + + /** + * Emits a detached component instance when the `RouteReuseStrategy` instructs to detach the + * subtree. + */ + detachEvents?: EventEmitter; } /** @@ -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. * * ``` * + * (deactivate)='onDeactivate($event)' + * (attach)='onAttach($event)' + * (detach)='onDetach($event)'> * ``` * * @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(); @Output('deactivate') deactivateEvents = new EventEmitter(); + /** + * Emits an attached component instance when the `RouteReuseStrategy` instructs to re-attach a + * previously detached subtree. + **/ + @Output('attach') attachEvents = new EventEmitter(); + /** + * Emits a detached component instance when the `RouteReuseStrategy` instructs to detach the + * subtree. + */ + @Output('detach') detachEvents = new EventEmitter(); 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 { diff --git a/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 562058e8a759a..aea3536ffcebe 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/test/integration.spec.ts @@ -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: + `` + }) + 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);