feat(router): add support for custom route reuse strategies
This commit is contained in:
parent
c4bbafc291
commit
42cf06fa12
|
@ -8,38 +8,65 @@
|
||||||
|
|
||||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||||
|
|
||||||
|
import {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||||
import {TreeNode} from './utils/tree';
|
import {TreeNode} from './utils/tree';
|
||||||
|
|
||||||
export function createRouterState(curr: RouterStateSnapshot, prevState: RouterState): RouterState {
|
export function createRouterState(
|
||||||
const root = createNode(curr._root, prevState ? prevState._root : undefined);
|
routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot,
|
||||||
|
prevState: RouterState): RouterState {
|
||||||
|
const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined);
|
||||||
return new RouterState(root, curr);
|
return new RouterState(root, curr);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode<ActivatedRoute>):
|
function createNode(
|
||||||
TreeNode<ActivatedRoute> {
|
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||||
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
|
prevState?: TreeNode<ActivatedRoute>): TreeNode<ActivatedRoute> {
|
||||||
|
// reuse an activated route that is currently displayed on the screen
|
||||||
|
if (prevState && routeReuseStrategy.shouldReuseRoute(curr.value, prevState.value.snapshot)) {
|
||||||
const value = prevState.value;
|
const value = prevState.value;
|
||||||
value._futureSnapshot = curr.value;
|
value._futureSnapshot = curr.value;
|
||||||
const children = createOrReuseChildren(curr, prevState);
|
const children = createOrReuseChildren(routeReuseStrategy, curr, prevState);
|
||||||
return new TreeNode<ActivatedRoute>(value, children);
|
return new TreeNode<ActivatedRoute>(value, children);
|
||||||
|
|
||||||
|
// retrieve an activated route that is used to be displayed, but is not currently displayed
|
||||||
|
} else if (routeReuseStrategy.retrieve(curr.value)) {
|
||||||
|
const tree: TreeNode<ActivatedRoute> =
|
||||||
|
(<DetachedRouteHandleInternal>routeReuseStrategy.retrieve(curr.value)).route;
|
||||||
|
setFutureSnapshotsOfActivatedRoutes(curr, tree);
|
||||||
|
return tree;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const value = createActivatedRoute(curr.value);
|
const value = createActivatedRoute(curr.value);
|
||||||
const children = curr.children.map(c => createNode(c));
|
const children = curr.children.map(c => createNode(routeReuseStrategy, c));
|
||||||
return new TreeNode<ActivatedRoute>(value, children);
|
return new TreeNode<ActivatedRoute>(value, children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFutureSnapshotsOfActivatedRoutes(
|
||||||
|
curr: TreeNode<ActivatedRouteSnapshot>, result: TreeNode<ActivatedRoute>): void {
|
||||||
|
if (curr.value.routeConfig !== result.value.routeConfig) {
|
||||||
|
throw new Error('Cannot reattach ActivatedRouteSnapshot created from a different route');
|
||||||
|
}
|
||||||
|
if (curr.children.length !== result.children.length) {
|
||||||
|
throw new Error('Cannot reattach ActivatedRouteSnapshot with a different number of children');
|
||||||
|
}
|
||||||
|
result.value._futureSnapshot = curr.value;
|
||||||
|
for (let i = 0; i < curr.children.length; ++i) {
|
||||||
|
setFutureSnapshotsOfActivatedRoutes(curr.children[i], result.children[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createOrReuseChildren(
|
function createOrReuseChildren(
|
||||||
curr: TreeNode<ActivatedRouteSnapshot>, prevState: TreeNode<ActivatedRoute>) {
|
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||||
|
prevState: TreeNode<ActivatedRoute>) {
|
||||||
return curr.children.map(child => {
|
return curr.children.map(child => {
|
||||||
for (const p of prevState.children) {
|
for (const p of prevState.children) {
|
||||||
if (equalRouteSnapshots(p.value.snapshot, child.value)) {
|
if (routeReuseStrategy.shouldReuseRoute(p.value.snapshot, child.value)) {
|
||||||
return createNode(child, p);
|
return createNode(routeReuseStrategy, child, p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createNode(child);
|
return createNode(routeReuseStrategy, child);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +74,4 @@ function createActivatedRoute(c: ActivatedRouteSnapshot) {
|
||||||
return new ActivatedRoute(
|
return new ActivatedRoute(
|
||||||
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
|
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
|
||||||
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
|
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
|
||||||
}
|
|
||||||
|
|
||||||
function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
|
|
||||||
return a._routeConfig === b._routeConfig;
|
|
||||||
}
|
}
|
|
@ -67,11 +67,27 @@ export class RouterOutlet implements OnDestroy {
|
||||||
return this._activatedRoute;
|
return this._activatedRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detach(): ComponentRef<any> {
|
||||||
|
if (!this.activated) throw new Error('Outlet is not activated');
|
||||||
|
this.location.detach();
|
||||||
|
const r = this.activated;
|
||||||
|
this.activated = null;
|
||||||
|
this._activatedRoute = null;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||||
|
this.activated = ref;
|
||||||
|
this._activatedRoute = activatedRoute;
|
||||||
|
this.location.insert(ref.hostView);
|
||||||
|
}
|
||||||
|
|
||||||
deactivate(): void {
|
deactivate(): void {
|
||||||
if (this.activated) {
|
if (this.activated) {
|
||||||
const c = this.component;
|
const c = this.component;
|
||||||
this.activated.destroy();
|
this.activated.destroy();
|
||||||
this.activated = null;
|
this.activated = null;
|
||||||
|
this._activatedRoute = null;
|
||||||
this.deactivateEvents.emit(c);
|
this.deactivateEvents.emit(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ 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} from './directives/router_outlet';
|
||||||
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
||||||
|
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationExtras, NavigationStart, Router, RoutesRecognized} from './router';
|
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationExtras, NavigationStart, Router, RoutesRecognized} from './router';
|
||||||
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
|
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
|
||||||
export {RouterOutletMap} from './router_outlet_map';
|
export {RouterOutletMap} from './router_outlet_map';
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {ComponentRef} from '@angular/core';
|
||||||
|
|
||||||
|
import {ActivatedRoute, ActivatedRouteSnapshot} from './router_state';
|
||||||
|
import {TreeNode} from './utils/tree';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Represents the detached route tree.
|
||||||
|
*
|
||||||
|
* This is an opaque value the router will give to a custom route reuse strategy
|
||||||
|
* to store and retrieve later on.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export type DetachedRouteHandle = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export type DetachedRouteHandleInternal = {
|
||||||
|
componentRef: ComponentRef<any>,
|
||||||
|
route: TreeNode<ActivatedRoute>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @whatItDoes Provides a way to customize when activated routes get reused.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export abstract class RouteReuseStrategy {
|
||||||
|
/**
|
||||||
|
* Determines if this route (and its subtree) should be detached to be reused later.
|
||||||
|
*/
|
||||||
|
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the detached route.
|
||||||
|
*/
|
||||||
|
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this route (and its subtree) should be reattached.
|
||||||
|
*/
|
||||||
|
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the previously stored route.
|
||||||
|
*/
|
||||||
|
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a route should be reused.
|
||||||
|
*/
|
||||||
|
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import {createRouterState} from './create_router_state';
|
||||||
import {createUrlTree} from './create_url_tree';
|
import {createUrlTree} from './create_url_tree';
|
||||||
import {RouterOutlet} from './directives/router_outlet';
|
import {RouterOutlet} from './directives/router_outlet';
|
||||||
import {recognize} from './recognize';
|
import {recognize} from './recognize';
|
||||||
|
import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
|
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
|
||||||
import {RouterOutletMap} from './router_outlet_map';
|
import {RouterOutletMap} from './router_outlet_map';
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
|
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
|
||||||
|
@ -287,6 +288,20 @@ type NavigationParams = {
|
||||||
promise: Promise<boolean>
|
promise: Promise<boolean>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does not detach any subtrees. Reuses routes as long as their route config is the same.
|
||||||
|
*/
|
||||||
|
export class DefaultRouteReuseStrategy implements RouteReuseStrategy {
|
||||||
|
shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; }
|
||||||
|
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}
|
||||||
|
shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; }
|
||||||
|
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return null; }
|
||||||
|
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
|
||||||
|
return future.routeConfig === curr.routeConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @whatItDoes Provides the navigation and url manipulation capabilities.
|
* @whatItDoes Provides the navigation and url manipulation capabilities.
|
||||||
*
|
*
|
||||||
|
@ -326,6 +341,8 @@ export class Router {
|
||||||
*/
|
*/
|
||||||
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
|
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
|
||||||
|
|
||||||
|
routeReuseStrategy: RouteReuseStrategy = new DefaultRouteReuseStrategy();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the router service.
|
* Creates the router service.
|
||||||
*/
|
*/
|
||||||
|
@ -703,7 +720,8 @@ export class Router {
|
||||||
const routerState$ =
|
const routerState$ =
|
||||||
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
|
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
|
||||||
if (shouldActivate) {
|
if (shouldActivate) {
|
||||||
const state = createRouterState(snapshot, this.currentRouterState);
|
const state =
|
||||||
|
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);
|
||||||
return {appliedUrl, state, shouldActivate};
|
return {appliedUrl, state, shouldActivate};
|
||||||
} else {
|
} else {
|
||||||
return {appliedUrl, state: null, shouldActivate};
|
return {appliedUrl, state: null, shouldActivate};
|
||||||
|
@ -738,7 +756,8 @@ export class Router {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new ActivateRoutes(state, storedState).activate(this.outletMap);
|
new ActivateRoutes(this.routeReuseStrategy, state, storedState)
|
||||||
|
.activate(this.outletMap);
|
||||||
|
|
||||||
navigationIsSuccessful = true;
|
navigationIsSuccessful = true;
|
||||||
})
|
})
|
||||||
|
@ -1007,7 +1026,9 @@ export class PreActivation {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActivateRoutes {
|
class ActivateRoutes {
|
||||||
constructor(private futureState: RouterState, private currState: RouterState) {}
|
constructor(
|
||||||
|
private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState,
|
||||||
|
private currState: RouterState) {}
|
||||||
|
|
||||||
activate(parentOutletMap: RouterOutletMap): void {
|
activate(parentOutletMap: RouterOutletMap): void {
|
||||||
const futureRoot = this.futureState._root;
|
const futureRoot = this.futureState._root;
|
||||||
|
@ -1087,9 +1108,18 @@ class ActivateRoutes {
|
||||||
if (future.component) {
|
if (future.component) {
|
||||||
advanceActivatedRoute(future);
|
advanceActivatedRoute(future);
|
||||||
const outlet = getOutlet(parentOutletMap, futureNode.value);
|
const outlet = getOutlet(parentOutletMap, futureNode.value);
|
||||||
const outletMap = new RouterOutletMap();
|
|
||||||
this.placeComponentIntoOutlet(outletMap, future, outlet);
|
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
|
||||||
this.activateChildRoutes(futureNode, null, outletMap);
|
const stored =
|
||||||
|
(<DetachedRouteHandleInternal>this.routeReuseStrategy.retrieve(future.snapshot));
|
||||||
|
this.routeReuseStrategy.store(future.snapshot, null);
|
||||||
|
outlet.attach(stored.componentRef, stored.route.value);
|
||||||
|
advanceActivatedRouteNodeAndItsChildren(stored.route);
|
||||||
|
} else {
|
||||||
|
const outletMap = new RouterOutletMap();
|
||||||
|
this.placeComponentIntoOutlet(outletMap, future, outlet);
|
||||||
|
this.activateChildRoutes(futureNode, null, outletMap);
|
||||||
|
}
|
||||||
|
|
||||||
// if we have a componentless route, we recurse but keep the same outlet map.
|
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||||
} else {
|
} else {
|
||||||
|
@ -1125,6 +1155,22 @@ class ActivateRoutes {
|
||||||
|
|
||||||
private deactiveRouteAndItsChildren(
|
private deactiveRouteAndItsChildren(
|
||||||
route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap): void {
|
route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap): void {
|
||||||
|
if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) {
|
||||||
|
this.detachAndStoreRouteSubtree(route, parentOutletMap);
|
||||||
|
} else {
|
||||||
|
this.deactiveRouteAndOutlet(route, parentOutletMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private detachAndStoreRouteSubtree(
|
||||||
|
route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap): void {
|
||||||
|
const outlet = getOutlet(parentOutletMap, route.value);
|
||||||
|
const componentRef = outlet.detach();
|
||||||
|
this.routeReuseStrategy.store(route.value.snapshot, {componentRef, route});
|
||||||
|
}
|
||||||
|
|
||||||
|
private deactiveRouteAndOutlet(route: TreeNode<ActivatedRoute>, parentOutletMap: RouterOutletMap):
|
||||||
|
void {
|
||||||
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route);
|
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route);
|
||||||
let outlet: RouterOutlet = null;
|
let outlet: RouterOutlet = null;
|
||||||
|
|
||||||
|
@ -1151,6 +1197,11 @@ class ActivateRoutes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function advanceActivatedRouteNodeAndItsChildren(node: TreeNode<ActivatedRoute>): void {
|
||||||
|
advanceActivatedRoute(node.value);
|
||||||
|
node.children.forEach(advanceActivatedRouteNodeAndItsChildren);
|
||||||
|
}
|
||||||
|
|
||||||
function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig {
|
function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig {
|
||||||
let s = snapshot.parent;
|
let s = snapshot.parent;
|
||||||
while (s) {
|
while (s) {
|
||||||
|
|
|
@ -8,11 +8,13 @@
|
||||||
|
|
||||||
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
|
import {APP_BASE_HREF, HashLocationStrategy, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
|
||||||
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, ApplicationRef, Compiler, ComponentRef, Inject, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, OpaqueToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
||||||
|
|
||||||
import {Route, Routes} from './config';
|
import {Route, Routes} from './config';
|
||||||
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||||
import {RouterLinkActive} from './directives/router_link_active';
|
import {RouterLinkActive} from './directives/router_link_active';
|
||||||
import {RouterOutlet} from './directives/router_outlet';
|
import {RouterOutlet} from './directives/router_outlet';
|
||||||
import {getDOM} from './private_import_platform-browser';
|
import {getDOM} from './private_import_platform-browser';
|
||||||
|
import {RouteReuseStrategy} from './route_reuse_strategy';
|
||||||
import {ErrorHandler, Router} from './router';
|
import {ErrorHandler, Router} from './router';
|
||||||
import {ROUTES} from './router_config_loader';
|
import {ROUTES} from './router_config_loader';
|
||||||
import {RouterOutletMap} from './router_outlet_map';
|
import {RouterOutletMap} from './router_outlet_map';
|
||||||
|
@ -23,6 +25,7 @@ import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
||||||
import {flatten} from './utils/collection';
|
import {flatten} from './utils/collection';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @whatItDoes Contains a list of directives
|
* @whatItDoes Contains a list of directives
|
||||||
* @stable
|
* @stable
|
||||||
|
@ -48,7 +51,8 @@ export const ROUTER_PROVIDERS: Provider[] = [
|
||||||
useFactory: setupRouter,
|
useFactory: setupRouter,
|
||||||
deps: [
|
deps: [
|
||||||
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
|
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
|
||||||
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
|
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()],
|
||||||
|
[RouteReuseStrategy, new Optional()]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
RouterOutletMap,
|
RouterOutletMap,
|
||||||
|
@ -240,7 +244,8 @@ export interface ExtraOptions {
|
||||||
export function setupRouter(
|
export function setupRouter(
|
||||||
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
|
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
|
||||||
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
|
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
|
||||||
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy) {
|
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy,
|
||||||
|
routeReuseStrategy?: RouteReuseStrategy) {
|
||||||
const router = new Router(
|
const router = new Router(
|
||||||
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
|
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
|
||||||
|
|
||||||
|
@ -248,6 +253,10 @@ export function setupRouter(
|
||||||
router.urlHandlingStrategy = urlHandlingStrategy;
|
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (routeReuseStrategy) {
|
||||||
|
router.routeReuseStrategy = routeReuseStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.errorHandler) {
|
if (opts.errorHandler) {
|
||||||
router.errorHandler = opts.errorHandler;
|
router.errorHandler = opts.errorHandler;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,24 +9,27 @@
|
||||||
import {Routes} from '../src/config';
|
import {Routes} from '../src/config';
|
||||||
import {createRouterState} from '../src/create_router_state';
|
import {createRouterState} from '../src/create_router_state';
|
||||||
import {recognize} from '../src/recognize';
|
import {recognize} from '../src/recognize';
|
||||||
|
import {DefaultRouteReuseStrategy} from '../src/router';
|
||||||
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
|
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
|
||||||
import {PRIMARY_OUTLET} from '../src/shared';
|
import {PRIMARY_OUTLET} from '../src/shared';
|
||||||
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
|
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
|
||||||
import {TreeNode} from '../src/utils/tree';
|
import {TreeNode} from '../src/utils/tree';
|
||||||
|
|
||||||
describe('create router state', () => {
|
describe('create router state', () => {
|
||||||
|
const reuseStrategy = new DefaultRouteReuseStrategy();
|
||||||
|
|
||||||
const emptyState = () =>
|
const emptyState = () =>
|
||||||
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
|
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
|
||||||
|
|
||||||
it('should work create new state', () => {
|
it('should work create new state', () => {
|
||||||
const state = createRouterState(
|
const state = createRouterState(
|
||||||
createState(
|
reuseStrategy, createState(
|
||||||
[
|
[
|
||||||
{path: 'a', component: ComponentA},
|
{path: 'a', component: ComponentA},
|
||||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||||
],
|
],
|
||||||
'a(left:b//right:c)'),
|
'a(left:b//right:c)'),
|
||||||
emptyState());
|
emptyState());
|
||||||
|
|
||||||
checkActivatedRoute(state.root, RootComponent);
|
checkActivatedRoute(state.root, RootComponent);
|
||||||
|
@ -43,9 +46,10 @@ describe('create router state', () => {
|
||||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||||
];
|
];
|
||||||
|
|
||||||
const prevState = createRouterState(createState(config, 'a(left:b)'), emptyState());
|
const prevState =
|
||||||
|
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
|
||||||
advanceState(prevState);
|
advanceState(prevState);
|
||||||
const state = createRouterState(createState(config, 'a(left:c)'), prevState);
|
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
|
||||||
|
|
||||||
expect(prevState.root).toBe(state.root);
|
expect(prevState.root).toBe(state.root);
|
||||||
const prevC = prevState.children(prevState.root);
|
const prevC = prevState.children(prevState.root);
|
||||||
|
@ -65,9 +69,11 @@ describe('create router state', () => {
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
|
||||||
const prevState = createRouterState(createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
|
const prevState = createRouterState(
|
||||||
|
reuseStrategy, createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
|
||||||
advanceState(prevState);
|
advanceState(prevState);
|
||||||
const state = createRouterState(createState(config, 'a/2;p=22/(b//right:c)'), prevState);
|
const state =
|
||||||
|
createRouterState(reuseStrategy, createState(config, 'a/2;p=22/(b//right:c)'), prevState);
|
||||||
|
|
||||||
expect(prevState.root).toBe(state.root);
|
expect(prevState.root).toBe(state.root);
|
||||||
const prevP = prevState.firstChild(prevState.root);
|
const prevP = prevState.firstChild(prevState.root);
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {expect} from '@angular/platform-browser/testing/matchers';
|
||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
import {map} from 'rxjs/operator/map';
|
import {map} from 'rxjs/operator/map';
|
||||||
|
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
|
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
|
||||||
import {RouterPreloader} from '../src/router_preloader';
|
import {RouterPreloader} from '../src/router_preloader';
|
||||||
import {forEach} from '../src/utils/collection';
|
import {forEach} from '../src/utils/collection';
|
||||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||||
|
@ -2392,6 +2392,105 @@ describe('Integration', () => {
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Custom Route Reuse Strategy', () => {
|
||||||
|
class AttachDetachReuseStrategy implements RouteReuseStrategy {
|
||||||
|
stored: {[k: string]: DetachedRouteHandle} = {};
|
||||||
|
|
||||||
|
shouldDetach(route: ActivatedRouteSnapshot): boolean {
|
||||||
|
return route.routeConfig.path === 'a';
|
||||||
|
}
|
||||||
|
|
||||||
|
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {
|
||||||
|
this.stored[route.routeConfig.path] = detachedTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAttach(route: ActivatedRouteSnapshot): boolean {
|
||||||
|
return !!this.stored[route.routeConfig.path];
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
|
||||||
|
return this.stored[route.routeConfig.path];
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
|
||||||
|
return future.routeConfig === curr.routeConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShortLifecycle implements RouteReuseStrategy {
|
||||||
|
shouldDetach(route: ActivatedRouteSnapshot): boolean { return false; }
|
||||||
|
store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void {}
|
||||||
|
shouldAttach(route: ActivatedRouteSnapshot): boolean { return false; }
|
||||||
|
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return null; }
|
||||||
|
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
|
||||||
|
if (future.routeConfig !== curr.routeConfig) {
|
||||||
|
return false;
|
||||||
|
} else if (Object.keys(future.params).length !== Object.keys(curr.params).length) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return Object.keys(future.params).every(k => future.params[k] === curr.params[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should support attaching & detaching fragments',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
|
||||||
|
router.routeReuseStrategy = new AttachDetachReuseStrategy();
|
||||||
|
|
||||||
|
router.resetConfig([
|
||||||
|
{path: 'a', component: TeamCmp, children: [{path: 'b', component: SimpleCmp}]},
|
||||||
|
{path: 'c', component: UserCmp}
|
||||||
|
]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/a/b');
|
||||||
|
advance(fixture);
|
||||||
|
const teamCmp = fixture.debugElement.children[1].componentInstance;
|
||||||
|
const simpleCmp = fixture.debugElement.children[1].children[1].componentInstance;
|
||||||
|
expect(location.path()).toEqual('/a/b');
|
||||||
|
expect(teamCmp).toBeDefined();
|
||||||
|
expect(simpleCmp).toBeDefined();
|
||||||
|
|
||||||
|
router.navigateByUrl('/c');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/c');
|
||||||
|
expect(fixture.debugElement.children[1].componentInstance).toBeAnInstanceOf(UserCmp);
|
||||||
|
|
||||||
|
router.navigateByUrl('/a;p=1/b;p=2');
|
||||||
|
advance(fixture);
|
||||||
|
const teamCmp2 = fixture.debugElement.children[1].componentInstance;
|
||||||
|
const simpleCmp2 = fixture.debugElement.children[1].children[1].componentInstance;
|
||||||
|
expect(location.path()).toEqual('/a;p=1/b;p=2');
|
||||||
|
expect(teamCmp2).toBe(teamCmp);
|
||||||
|
expect(simpleCmp2).toBe(simpleCmp);
|
||||||
|
|
||||||
|
expect(teamCmp.route).toBe(router.routerState.root.firstChild);
|
||||||
|
expect(teamCmp.route.snapshot).toBe(router.routerState.snapshot.root.firstChild);
|
||||||
|
expect(teamCmp.route.snapshot.params).toEqual({p: '1'});
|
||||||
|
expect(teamCmp.route.firstChild.snapshot.params).toEqual({p: '2'});
|
||||||
|
})));
|
||||||
|
|
||||||
|
it('should support shorter lifecycles',
|
||||||
|
fakeAsync(inject([Router, Location], (router: Router, location: Location) => {
|
||||||
|
const fixture = createRoot(router, RootCmp);
|
||||||
|
router.routeReuseStrategy = new ShortLifecycle();
|
||||||
|
|
||||||
|
router.resetConfig([{path: 'a', component: SimpleCmp}]);
|
||||||
|
|
||||||
|
router.navigateByUrl('/a');
|
||||||
|
advance(fixture);
|
||||||
|
const simpleCmp1 = fixture.debugElement.children[1].componentInstance;
|
||||||
|
expect(location.path()).toEqual('/a');
|
||||||
|
|
||||||
|
router.navigateByUrl('/a;p=1');
|
||||||
|
advance(fixture);
|
||||||
|
expect(location.path()).toEqual('/a;p=1');
|
||||||
|
const simpleCmp2 = fixture.debugElement.children[1].componentInstance;
|
||||||
|
expect(simpleCmp1).not.toBe(simpleCmp2);
|
||||||
|
})));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectEvents(events: Event[], pairs: any[]) {
|
function expectEvents(events: Event[], pairs: any[]) {
|
||||||
|
|
|
@ -66,6 +66,9 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
|
||||||
serialize(tree: UrlTree): string;
|
serialize(tree: UrlTree): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare type DetachedRouteHandle = {};
|
||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized;
|
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized;
|
||||||
|
|
||||||
|
@ -201,6 +204,7 @@ export declare class Router {
|
||||||
errorHandler: ErrorHandler;
|
errorHandler: ErrorHandler;
|
||||||
events: Observable<Event>;
|
events: Observable<Event>;
|
||||||
navigated: boolean;
|
navigated: boolean;
|
||||||
|
routeReuseStrategy: RouteReuseStrategy;
|
||||||
routerState: RouterState;
|
routerState: RouterState;
|
||||||
url: string;
|
url: string;
|
||||||
urlHandlingStrategy: UrlHandlingStrategy;
|
urlHandlingStrategy: UrlHandlingStrategy;
|
||||||
|
@ -224,6 +228,15 @@ export declare const ROUTER_CONFIGURATION: OpaqueToken;
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare const ROUTER_INITIALIZER: OpaqueToken;
|
export declare const ROUTER_INITIALIZER: OpaqueToken;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare abstract class RouteReuseStrategy {
|
||||||
|
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle;
|
||||||
|
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
|
||||||
|
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
|
||||||
|
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
||||||
|
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void;
|
||||||
|
}
|
||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare class RouterLink {
|
export declare class RouterLink {
|
||||||
fragment: string;
|
fragment: string;
|
||||||
|
@ -298,7 +311,9 @@ export declare class RouterOutlet implements OnDestroy {
|
||||||
outletMap: RouterOutletMap;
|
outletMap: RouterOutletMap;
|
||||||
constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string);
|
constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string);
|
||||||
activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void;
|
activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void;
|
||||||
|
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void;
|
||||||
deactivate(): void;
|
deactivate(): void;
|
||||||
|
detach(): ComponentRef<any>;
|
||||||
ngOnDestroy(): void;
|
ngOnDestroy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue