Skip to content

Commit a82fddf

Browse files
committed
feat(router): Allow for custom router outlet implementations (#40827)
This PR formalizes, documents, and makes public the router outlet contract. The set of `RouterOutlet` methods used by the `Router` has not changed in over 4 years, since the introduction of route reuse strategies. Creation of custom router outlets is already possible and is used by the Ionic framework (https://github.com/ionic-team/ionic-framework/blob/master/angular/src/directives/navigation/ion-router-outlet.ts). There is a small "hack" that is needed to make this work, which is that outlets must register with `ChildrenOutletContexts`, but it currently only accepts our `RouterOutlet`. By exposing the interface the `Router` uses to activate and deactivate routes through outlets, we allow for developers to more easily and safely extend the `Router` and have fine-tuned control over navigation and component activation that fits project requirements. PR Close #40827
1 parent d0b6270 commit a82fddf

File tree

4 files changed

+84
-8
lines changed

4 files changed

+84
-8
lines changed

goldens/public-api/router/router.d.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export declare class ChildActivationStart {
9292
export declare class ChildrenOutletContexts {
9393
getContext(childName: string): OutletContext | null;
9494
getOrCreateContext(childName: string): OutletContext;
95-
onChildOutletCreated(childName: string, outlet: RouterOutlet): void;
95+
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void;
9696
onChildOutletDestroyed(childName: string): void;
9797
onOutletDeactivated(): Map<string, OutletContext>;
9898
onOutletReAttached(contexts: Map<string, OutletContext>): void;
@@ -234,7 +234,7 @@ export declare class NoPreloading implements PreloadingStrategy {
234234
export declare class OutletContext {
235235
attachRef: ComponentRef<any> | null;
236236
children: ChildrenOutletContexts;
237-
outlet: RouterOutlet | null;
237+
outlet: RouterOutletContract | null;
238238
resolver: ComponentFactoryResolver | null;
239239
route: ActivatedRoute | null;
240240
}
@@ -434,7 +434,7 @@ export declare class RouterModule {
434434
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterModule>;
435435
}
436436

437-
export declare class RouterOutlet implements OnDestroy, OnInit {
437+
export declare class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
438438
activateEvents: EventEmitter<any>;
439439
get activatedRoute(): ActivatedRoute;
440440
get activatedRouteData(): Data;
@@ -450,6 +450,17 @@ export declare class RouterOutlet implements OnDestroy, OnInit {
450450
ngOnInit(): void;
451451
}
452452

453+
export declare interface RouterOutletContract {
454+
activatedRoute: ActivatedRoute | null;
455+
activatedRouteData: Data;
456+
component: Object | null;
457+
isActivated: boolean;
458+
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void;
459+
attach(ref: ComponentRef<unknown>, activatedRoute: ActivatedRoute): void;
460+
deactivate(): void;
461+
detach(): ComponentRef<unknown>;
462+
}
463+
453464
export declare class RouterPreloader implements OnDestroy {
454465
constructor(router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, preloadingStrategy: PreloadingStrategy);
455466
ngOnDestroy(): void;

packages/router/src/directives/router_outlet.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,67 @@ import {ChildrenOutletContexts} from '../router_outlet_context';
1313
import {ActivatedRoute} from '../router_state';
1414
import {PRIMARY_OUTLET} from '../shared';
1515

16+
/**
17+
* An interface that defines the contract for developing a component outlet for the `Router`.
18+
*
19+
* An outlet acts as a placeholder that Angular dynamically fills based on the current router state.
20+
*
21+
* A router outlet should register itself with the `Router` via
22+
* `ChildrenOutletContexts#onChildOutletCreated` and unregister with
23+
* `ChildrenOutletContexts#onChildOutletDestroyed`. When the `Router` identifies a matched `Route`,
24+
* it looks for a registered outlet in the `ChildrenOutletContexts` and activates it.
25+
*
26+
* @see `ChildrenOutletContexts`
27+
* @publicApi
28+
*/
29+
export interface RouterOutletContract {
30+
/**
31+
* Whether the given outlet is activated.
32+
*
33+
* An outlet is considered "activated" if it has an active component.
34+
*/
35+
isActivated: boolean;
36+
37+
/** The instance of the activated component or `null` if the outlet is not activated. */
38+
component: Object|null;
39+
40+
/**
41+
* The `Data` of the `ActivatedRoute` snapshot.
42+
*/
43+
activatedRouteData: Data;
44+
45+
/**
46+
* The `ActivatedRoute` for the outlet or `null` if the outlet is not activated.
47+
*/
48+
activatedRoute: ActivatedRoute|null;
49+
50+
/**
51+
* Called by the `Router` when the outlet should activate (create a component).
52+
*/
53+
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null): void;
54+
55+
/**
56+
* A request to destroy the currently activated component.
57+
*
58+
* When a `RouteReuseStrategy` indicates that an `ActivatedRoute` should be removed but stored for
59+
* later re-use rather than destroyed, the `Router` will call `detach` instead.
60+
*/
61+
deactivate(): void;
62+
63+
/**
64+
* Called when the `RouteReuseStrategy` instructs to detach the subtree.
65+
*
66+
* This is similar to `deactivate`, but the activated component should _not_ be destroyed.
67+
* Instead, it is returned so that it can be reattached later via the `attach` method.
68+
*/
69+
detach(): ComponentRef<unknown>;
70+
71+
/**
72+
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree.
73+
*/
74+
attach(ref: ComponentRef<unknown>, activatedRoute: ActivatedRoute): void;
75+
}
76+
1677
/**
1778
* @description
1879
*
@@ -60,7 +121,7 @@ import {PRIMARY_OUTLET} from '../shared';
60121
* @publicApi
61122
*/
62123
@Directive({selector: 'router-outlet', exportAs: 'outlet'})
63-
export class RouterOutlet implements OnDestroy, OnInit {
124+
export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
64125
private activated: ComponentRef<any>|null = null;
65126
private _activatedRoute: ActivatedRoute|null = null;
66127
private name: string;
@@ -103,6 +164,10 @@ export class RouterOutlet implements OnDestroy, OnInit {
103164
return !!this.activated;
104165
}
105166

167+
/**
168+
* @returns The currently activated component instance.
169+
* @throws An error if the outlet is not activated.
170+
*/
106171
get component(): Object {
107172
if (!this.activated) throw new Error('Outlet is not activated');
108173
return this.activated.instance;

packages/router/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
export {Data, DeprecatedLoadChildren, LoadChildren, LoadChildrenCallback, QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './config';
1111
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
1212
export {RouterLinkActive} from './directives/router_link_active';
13-
export {RouterOutlet} from './directives/router_outlet';
13+
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
1414
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
1515
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
1616
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';

packages/router/src/router_outlet_context.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {ComponentFactoryResolver, ComponentRef} from '@angular/core';
1010

11-
import {RouterOutlet} from './directives/router_outlet';
11+
import {RouterOutletContract} from './directives/router_outlet';
1212
import {ActivatedRoute} from './router_state';
1313

1414

@@ -18,7 +18,7 @@ import {ActivatedRoute} from './router_state';
1818
* @publicApi
1919
*/
2020
export class OutletContext {
21-
outlet: RouterOutlet|null = null;
21+
outlet: RouterOutletContract|null = null;
2222
route: ActivatedRoute|null = null;
2323
resolver: ComponentFactoryResolver|null = null;
2424
children = new ChildrenOutletContexts();
@@ -35,7 +35,7 @@ export class ChildrenOutletContexts {
3535
private contexts = new Map<string, OutletContext>();
3636

3737
/** Called when a `RouterOutlet` directive is instantiated */
38-
onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
38+
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void {
3939
const context = this.getOrCreateContext(childName);
4040
context.outlet = outlet;
4141
this.contexts.set(childName, context);

0 commit comments

Comments
 (0)