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 {
|
export declare class ChildrenOutletContexts {
|
||||||
getContext(childName: string): OutletContext | null;
|
getContext(childName: string): OutletContext | null;
|
||||||
getOrCreateContext(childName: string): OutletContext;
|
getOrCreateContext(childName: string): OutletContext;
|
||||||
onChildOutletCreated(childName: string, outlet: RouterOutlet): void;
|
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void;
|
||||||
onChildOutletDestroyed(childName: string): void;
|
onChildOutletDestroyed(childName: string): void;
|
||||||
onOutletDeactivated(): Map<string, OutletContext>;
|
onOutletDeactivated(): Map<string, OutletContext>;
|
||||||
onOutletReAttached(contexts: Map<string, OutletContext>): void;
|
onOutletReAttached(contexts: Map<string, OutletContext>): void;
|
||||||
|
@ -234,7 +234,7 @@ export declare class NoPreloading implements PreloadingStrategy {
|
||||||
export declare class OutletContext {
|
export declare class OutletContext {
|
||||||
attachRef: ComponentRef<any> | null;
|
attachRef: ComponentRef<any> | null;
|
||||||
children: ChildrenOutletContexts;
|
children: ChildrenOutletContexts;
|
||||||
outlet: RouterOutlet | null;
|
outlet: RouterOutletContract | null;
|
||||||
resolver: ComponentFactoryResolver | null;
|
resolver: ComponentFactoryResolver | null;
|
||||||
route: ActivatedRoute | null;
|
route: ActivatedRoute | null;
|
||||||
}
|
}
|
||||||
|
@ -434,7 +434,7 @@ export declare class RouterModule {
|
||||||
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<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>;
|
activateEvents: EventEmitter<any>;
|
||||||
get activatedRoute(): ActivatedRoute;
|
get activatedRoute(): ActivatedRoute;
|
||||||
get activatedRouteData(): Data;
|
get activatedRouteData(): Data;
|
||||||
|
@ -450,6 +450,17 @@ export declare class RouterOutlet implements OnDestroy, OnInit {
|
||||||
ngOnInit(): void;
|
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 {
|
export declare class RouterPreloader implements OnDestroy {
|
||||||
constructor(router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, preloadingStrategy: PreloadingStrategy);
|
constructor(router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, preloadingStrategy: PreloadingStrategy);
|
||||||
ngOnDestroy(): void;
|
ngOnDestroy(): void;
|
||||||
|
|
|
@ -13,6 +13,67 @@ import {ChildrenOutletContexts} from '../router_outlet_context';
|
||||||
import {ActivatedRoute} from '../router_state';
|
import {ActivatedRoute} from '../router_state';
|
||||||
import {PRIMARY_OUTLET} from '../shared';
|
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
|
* @description
|
||||||
*
|
*
|
||||||
|
@ -60,7 +121,7 @@ import {PRIMARY_OUTLET} from '../shared';
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
@Directive({selector: 'router-outlet', exportAs: 'outlet'})
|
@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 activated: ComponentRef<any>|null = null;
|
||||||
private _activatedRoute: ActivatedRoute|null = null;
|
private _activatedRoute: ActivatedRoute|null = null;
|
||||||
private name: string;
|
private name: string;
|
||||||
|
@ -103,6 +164,10 @@ export class RouterOutlet implements OnDestroy, OnInit {
|
||||||
return !!this.activated;
|
return !!this.activated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The currently activated component instance.
|
||||||
|
* @throws An error if the outlet is not activated.
|
||||||
|
*/
|
||||||
get component(): Object {
|
get component(): Object {
|
||||||
if (!this.activated) throw new Error('Outlet is not activated');
|
if (!this.activated) throw new Error('Outlet is not activated');
|
||||||
return this.activated.instance;
|
return this.activated.instance;
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
export {Data, DeprecatedLoadChildren, LoadChildren, LoadChildrenCallback, QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './config';
|
export {Data, DeprecatedLoadChildren, LoadChildren, LoadChildrenCallback, QueryParamsHandling, ResolveData, Route, Routes, RunGuardsAndResolvers, UrlMatcher, UrlMatchResult} from './config';
|
||||||
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||||
export {RouterLinkActive} from './directives/router_link_active';
|
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 {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 {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
||||||
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
export {BaseRouteReuseStrategy, DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import {ComponentFactoryResolver, ComponentRef} from '@angular/core';
|
import {ComponentFactoryResolver, ComponentRef} from '@angular/core';
|
||||||
|
|
||||||
import {RouterOutlet} from './directives/router_outlet';
|
import {RouterOutletContract} from './directives/router_outlet';
|
||||||
import {ActivatedRoute} from './router_state';
|
import {ActivatedRoute} from './router_state';
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import {ActivatedRoute} from './router_state';
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
export class OutletContext {
|
export class OutletContext {
|
||||||
outlet: RouterOutlet|null = null;
|
outlet: RouterOutletContract|null = null;
|
||||||
route: ActivatedRoute|null = null;
|
route: ActivatedRoute|null = null;
|
||||||
resolver: ComponentFactoryResolver|null = null;
|
resolver: ComponentFactoryResolver|null = null;
|
||||||
children = new ChildrenOutletContexts();
|
children = new ChildrenOutletContexts();
|
||||||
|
@ -35,7 +35,7 @@ export class ChildrenOutletContexts {
|
||||||
private contexts = new Map<string, OutletContext>();
|
private contexts = new Map<string, OutletContext>();
|
||||||
|
|
||||||
/** Called when a `RouterOutlet` directive is instantiated */
|
/** Called when a `RouterOutlet` directive is instantiated */
|
||||||
onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
|
onChildOutletCreated(childName: string, outlet: RouterOutletContract): void {
|
||||||
const context = this.getOrCreateContext(childName);
|
const context = this.getOrCreateContext(childName);
|
||||||
context.outlet = outlet;
|
context.outlet = outlet;
|
||||||
this.contexts.set(childName, context);
|
this.contexts.set(childName, context);
|
||||||
|
|
Loading…
Reference in New Issue