Skip to content

Commit

Permalink
feat(router): emit activate/deactivate events when an outlet gets att…
Browse files Browse the repository at this point in the history
…ached/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
  • Loading branch information
dimakuba authored and alxhub committed Sep 28, 2021
1 parent 8d2b6af commit 4f3beff
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 2 deletions.
4 changes: 4 additions & 0 deletions goldens/public-api/router/router.md
Expand Up @@ -615,13 +615,15 @@ 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)
deactivate(): void;
// (undocumented)
deactivateEvents: EventEmitter<any>;
detach(): ComponentRef<any>;
detachEvents: EventEmitter<unknown>;
// (undocumented)
get isActivated(): boolean;
// (undocumented)
Expand All @@ -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;
}

Expand Down
33 changes: 31 additions & 2 deletions packages/router/src/directives/router_outlet.ts
Expand Up @@ -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>;
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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 {
Expand Down
54 changes: 54 additions & 0 deletions packages/router/test/integration.spec.ts
Expand Up @@ -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);
Expand Down

0 comments on commit 4f3beff

Please sign in to comment.