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
This commit is contained in:
parent
d0b6270990
commit
a82fddf1ce
|
@ -92,7 +92,7 @@ export declare class ChildActivationStart {
|
|||
export declare class ChildrenOutletContexts {
|
||||
getContext(childName: string): OutletContext | null;
|
||||
getOrCreateContext(childName: string): OutletContext;
|
||||
onChildOutletCreated(childName: string, outlet: RouterOutlet): void;
|
||||
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void;
|
||||
onChildOutletDestroyed(childName: string): void;
|
||||
onOutletDeactivated(): Map<string, OutletContext>;
|
||||
onOutletReAttached(contexts: Map<string, OutletContext>): void;
|
||||
|
@ -234,7 +234,7 @@ export declare class NoPreloading implements PreloadingStrategy {
|
|||
export declare class OutletContext {
|
||||
attachRef: ComponentRef<any> | null;
|
||||
children: ChildrenOutletContexts;
|
||||
outlet: RouterOutlet | null;
|
||||
outlet: RouterOutletContract | null;
|
||||
resolver: ComponentFactoryResolver | null;
|
||||
route: ActivatedRoute | null;
|
||||
}
|
||||
|
@ -434,7 +434,7 @@ export declare class RouterModule {
|
|||
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterModule>;
|
||||
}
|
||||
|
||||
export declare class RouterOutlet implements OnDestroy, OnInit {
|
||||
export declare class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
||||
activateEvents: EventEmitter<any>;
|
||||
get activatedRoute(): ActivatedRoute;
|
||||
get activatedRouteData(): Data;
|
||||
|
@ -450,6 +450,17 @@ export declare class RouterOutlet implements OnDestroy, OnInit {
|
|||
ngOnInit(): void;
|
||||
}
|
||||
|
||||
export declare interface RouterOutletContract {
|
||||
activatedRoute: ActivatedRoute | null;
|
||||
activatedRouteData: Data;
|
||||
component: Object | null;
|
||||
isActivated: boolean;
|
||||
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null): void;
|
||||
attach(ref: ComponentRef<unknown>, activatedRoute: ActivatedRoute): void;
|
||||
deactivate(): void;
|
||||
detach(): ComponentRef<unknown>;
|
||||
}
|
||||
|
||||
export declare class RouterPreloader implements OnDestroy {
|
||||
constructor(router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, preloadingStrategy: PreloadingStrategy);
|
||||
ngOnDestroy(): void;
|
||||
|
|
|
@ -13,6 +13,67 @@ import {ChildrenOutletContexts} from '../router_outlet_context';
|
|||
import {ActivatedRoute} from '../router_state';
|
||||
import {PRIMARY_OUTLET} from '../shared';
|
||||
|
||||
/**
|
||||
* An interface that defines the contract for developing a component outlet for the `Router`.
|
||||
*
|
||||
* An outlet acts as a placeholder that Angular dynamically fills based on the current router state.
|
||||
*
|
||||
* A router outlet should register itself with the `Router` via
|
||||
* `ChildrenOutletContexts#onChildOutletCreated` and unregister with
|
||||
* `ChildrenOutletContexts#onChildOutletDestroyed`. When the `Router` identifies a matched `Route`,
|
||||
* it looks for a registered outlet in the `ChildrenOutletContexts` and activates it.
|
||||
*
|
||||
* @see `ChildrenOutletContexts`
|
||||
* @publicApi
|
||||
*/
|
||||
export interface RouterOutletContract {
|
||||
/**
|
||||
* Whether the given outlet is activated.
|
||||
*
|
||||
* An outlet is considered "activated" if it has an active component.
|
||||
*/
|
||||
isActivated: boolean;
|
||||
|
||||
/** The instance of the activated component or `null` if the outlet is not activated. */
|
||||
component: Object|null;
|
||||
|
||||
/**
|
||||
* The `Data` of the `ActivatedRoute` snapshot.
|
||||
*/
|
||||
activatedRouteData: Data;
|
||||
|
||||
/**
|
||||
* The `ActivatedRoute` for the outlet or `null` if the outlet is not activated.
|
||||
*/
|
||||
activatedRoute: ActivatedRoute|null;
|
||||
|
||||
/**
|
||||
* Called by the `Router` when the outlet should activate (create a component).
|
||||
*/
|
||||
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null): void;
|
||||
|
||||
/**
|
||||
* A request to destroy the currently activated component.
|
||||
*
|
||||
* When a `RouteReuseStrategy` indicates that an `ActivatedRoute` should be removed but stored for
|
||||
* later re-use rather than destroyed, the `Router` will call `detach` instead.
|
||||
*/
|
||||
deactivate(): void;
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to detach the subtree.
|
||||
*
|
||||
* This is similar to `deactivate`, but the activated component should _not_ be destroyed.
|
||||
* Instead, it is returned so that it can be reattached later via the `attach` method.
|
||||
*/
|
||||
detach(): ComponentRef<unknown>;
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree.
|
||||
*/
|
||||
attach(ref: ComponentRef<unknown>, activatedRoute: ActivatedRoute): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
|
@ -60,7 +121,7 @@ import {PRIMARY_OUTLET} from '../shared';
|
|||
* @publicApi
|
||||
*/
|
||||
@Directive({selector: 'router-outlet', exportAs: 'outlet'})
|
||||
export class RouterOutlet implements OnDestroy, OnInit {
|
||||
export class RouterOutlet implements OnDestroy, OnInit, RouterOutletContract {
|
||||
private activated: ComponentRef<any>|null = null;
|
||||
private _activatedRoute: ActivatedRoute|null = null;
|
||||
private name: string;
|
||||
|
@ -103,6 +164,10 @@ export class RouterOutlet implements OnDestroy, OnInit {
|
|||
return !!this.activated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The currently activated component instance.
|
||||
* @throws An error if the outlet is not activated.
|
||||
*/
|
||||
get component(): Object {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
return this.activated.instance;
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
export {Data, DeprecatedLoadChildren, LoadChildren, LoadChildrenCallback, QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './config';
|
||||
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||
export {RouterLinkActive} from './directives/router_link_active';
|
||||
export {RouterOutlet} from './directives/router_outlet';
|
||||
export {RouterOutlet, RouterOutletContract} from './directives/router_outlet';
|
||||
export {ActivationEnd, ActivationStart, ChildActivationEnd, ChildActivationStart, Event, GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, RouterEvent, RoutesRecognized, Scroll} from './events';
|
||||
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
||||
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {ComponentFactoryResolver, ComponentRef} from '@angular/core';
|
||||
|
||||
import {RouterOutlet} from './directives/router_outlet';
|
||||
import {RouterOutletContract} from './directives/router_outlet';
|
||||
import {ActivatedRoute} from './router_state';
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@ import {ActivatedRoute} from './router_state';
|
|||
* @publicApi
|
||||
*/
|
||||
export class OutletContext {
|
||||
outlet: RouterOutlet|null = null;
|
||||
outlet: RouterOutletContract|null = null;
|
||||
route: ActivatedRoute|null = null;
|
||||
resolver: ComponentFactoryResolver|null = null;
|
||||
children = new ChildrenOutletContexts();
|
||||
|
@ -35,7 +35,7 @@ export class ChildrenOutletContexts {
|
|||
private contexts = new Map<string, OutletContext>();
|
||||
|
||||
/** Called when a `RouterOutlet` directive is instantiated */
|
||||
onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
|
||||
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void {
|
||||
const context = this.getOrCreateContext(childName);
|
||||
context.outlet = outlet;
|
||||
this.contexts.set(childName, context);
|
||||
|
|
Loading…
Reference in New Issue