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 {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||
import {TreeNode} from './utils/tree';
|
||||
|
||||
export function createRouterState(curr: RouterStateSnapshot, prevState: RouterState): RouterState {
|
||||
const root = createNode(curr._root, prevState ? prevState._root : undefined);
|
||||
export function createRouterState(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot,
|
||||
prevState: RouterState): RouterState {
|
||||
const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined);
|
||||
return new RouterState(root, curr);
|
||||
}
|
||||
|
||||
function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode<ActivatedRoute>):
|
||||
TreeNode<ActivatedRoute> {
|
||||
if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) {
|
||||
function createNode(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
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;
|
||||
value._futureSnapshot = curr.value;
|
||||
const children = createOrReuseChildren(curr, prevState);
|
||||
const children = createOrReuseChildren(routeReuseStrategy, curr, prevState);
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
curr: TreeNode<ActivatedRouteSnapshot>, prevState: TreeNode<ActivatedRoute>) {
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState: TreeNode<ActivatedRoute>) {
|
||||
return curr.children.map(child => {
|
||||
for (const p of prevState.children) {
|
||||
if (equalRouteSnapshots(p.value.snapshot, child.value)) {
|
||||
return createNode(child, p);
|
||||
if (routeReuseStrategy.shouldReuseRoute(p.value.snapshot, child.value)) {
|
||||
return createNode(routeReuseStrategy, child, p);
|
||||
}
|
||||
}
|
||||
return createNode(child);
|
||||
return createNode(routeReuseStrategy, child);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -47,8 +74,4 @@ function createActivatedRoute(c: ActivatedRouteSnapshot) {
|
|||
return new ActivatedRoute(
|
||||
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);
|
||||
}
|
||||
|
||||
function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
|
||||
return a._routeConfig === b._routeConfig;
|
||||
}
|
|
@ -67,11 +67,27 @@ export class RouterOutlet implements OnDestroy {
|
|||
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 {
|
||||
if (this.activated) {
|
||||
const c = this.component;
|
||||
this.activated.destroy();
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
this.deactivateEvents.emit(c);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
|||
export {RouterLinkActive} from './directives/router_link_active';
|
||||
export {RouterOutlet} from './directives/router_outlet';
|
||||
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 {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
|
||||
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 {RouterOutlet} from './directives/router_outlet';
|
||||
import {recognize} from './recognize';
|
||||
import {DetachedRouteHandle, DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
|
||||
import {RouterOutletMap} from './router_outlet_map';
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState, equalParamsAndUrlSegments, inheritedParamsDataResolve} from './router_state';
|
||||
|
@ -287,6 +288,20 @@ type NavigationParams = {
|
|||
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.
|
||||
*
|
||||
|
@ -326,6 +341,8 @@ export class Router {
|
|||
*/
|
||||
urlHandlingStrategy: UrlHandlingStrategy = new DefaultUrlHandlingStrategy();
|
||||
|
||||
routeReuseStrategy: RouteReuseStrategy = new DefaultRouteReuseStrategy();
|
||||
|
||||
/**
|
||||
* Creates the router service.
|
||||
*/
|
||||
|
@ -703,7 +720,8 @@ export class Router {
|
|||
const routerState$ =
|
||||
map.call(preactivationResolveData$, ({appliedUrl, snapshot, shouldActivate}: any) => {
|
||||
if (shouldActivate) {
|
||||
const state = createRouterState(snapshot, this.currentRouterState);
|
||||
const state =
|
||||
createRouterState(this.routeReuseStrategy, snapshot, this.currentRouterState);
|
||||
return {appliedUrl, state, shouldActivate};
|
||||
} else {
|
||||
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;
|
||||
})
|
||||
|
@ -1007,7 +1026,9 @@ export class PreActivation {
|
|||
}
|
||||
|
||||
class ActivateRoutes {
|
||||
constructor(private futureState: RouterState, private currState: RouterState) {}
|
||||
constructor(
|
||||
private routeReuseStrategy: RouteReuseStrategy, private futureState: RouterState,
|
||||
private currState: RouterState) {}
|
||||
|
||||
activate(parentOutletMap: RouterOutletMap): void {
|
||||
const futureRoot = this.futureState._root;
|
||||
|
@ -1087,9 +1108,18 @@ class ActivateRoutes {
|
|||
if (future.component) {
|
||||
advanceActivatedRoute(future);
|
||||
const outlet = getOutlet(parentOutletMap, futureNode.value);
|
||||
const outletMap = new RouterOutletMap();
|
||||
this.placeComponentIntoOutlet(outletMap, future, outlet);
|
||||
this.activateChildRoutes(futureNode, null, outletMap);
|
||||
|
||||
if (this.routeReuseStrategy.shouldAttach(future.snapshot)) {
|
||||
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.
|
||||
} else {
|
||||
|
@ -1125,6 +1155,22 @@ class ActivateRoutes {
|
|||
|
||||
private deactiveRouteAndItsChildren(
|
||||
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);
|
||||
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 {
|
||||
let s = snapshot.parent;
|
||||
while (s) {
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
|
||||
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 {Route, Routes} from './config';
|
||||
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||
import {RouterLinkActive} from './directives/router_link_active';
|
||||
import {RouterOutlet} from './directives/router_outlet';
|
||||
import {getDOM} from './private_import_platform-browser';
|
||||
import {RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {ErrorHandler, Router} from './router';
|
||||
import {ROUTES} from './router_config_loader';
|
||||
import {RouterOutletMap} from './router_outlet_map';
|
||||
|
@ -23,6 +25,7 @@ import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
|||
import {flatten} from './utils/collection';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Contains a list of directives
|
||||
* @stable
|
||||
|
@ -48,7 +51,8 @@ export const ROUTER_PROVIDERS: Provider[] = [
|
|||
useFactory: setupRouter,
|
||||
deps: [
|
||||
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
|
||||
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()]
|
||||
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()],
|
||||
[RouteReuseStrategy, new Optional()]
|
||||
]
|
||||
},
|
||||
RouterOutletMap,
|
||||
|
@ -240,7 +244,8 @@ export interface ExtraOptions {
|
|||
export function setupRouter(
|
||||
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
|
||||
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(
|
||||
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
|
||||
|
||||
|
@ -248,6 +253,10 @@ export function setupRouter(
|
|||
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||
}
|
||||
|
||||
if (routeReuseStrategy) {
|
||||
router.routeReuseStrategy = routeReuseStrategy;
|
||||
}
|
||||
|
||||
if (opts.errorHandler) {
|
||||
router.errorHandler = opts.errorHandler;
|
||||
}
|
||||
|
|
|
@ -9,24 +9,27 @@
|
|||
import {Routes} from '../src/config';
|
||||
import {createRouterState} from '../src/create_router_state';
|
||||
import {recognize} from '../src/recognize';
|
||||
import {DefaultRouteReuseStrategy} from '../src/router';
|
||||
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
|
||||
import {TreeNode} from '../src/utils/tree';
|
||||
|
||||
describe('create router state', () => {
|
||||
const reuseStrategy = new DefaultRouteReuseStrategy();
|
||||
|
||||
const emptyState = () =>
|
||||
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
|
||||
|
||||
it('should work create new state', () => {
|
||||
const state = createRouterState(
|
||||
createState(
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)'),
|
||||
reuseStrategy, createState(
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)'),
|
||||
emptyState());
|
||||
|
||||
checkActivatedRoute(state.root, RootComponent);
|
||||
|
@ -43,9 +46,10 @@ describe('create router state', () => {
|
|||
{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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
const prevP = prevState.firstChild(prevState.root);
|
||||
|
|
|
@ -13,7 +13,7 @@ import {expect} from '@angular/platform-browser/testing/matchers';
|
|||
import {Observable} from 'rxjs/Observable';
|
||||
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 {forEach} from '../src/utils/collection';
|
||||
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[]) {
|
||||
|
|
|
@ -66,6 +66,9 @@ export declare class DefaultUrlSerializer implements UrlSerializer {
|
|||
serialize(tree: UrlTree): string;
|
||||
}
|
||||
|
||||
/** @experimental */
|
||||
export declare type DetachedRouteHandle = {};
|
||||
|
||||
/** @stable */
|
||||
export declare type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError | RoutesRecognized;
|
||||
|
||||
|
@ -201,6 +204,7 @@ export declare class Router {
|
|||
errorHandler: ErrorHandler;
|
||||
events: Observable<Event>;
|
||||
navigated: boolean;
|
||||
routeReuseStrategy: RouteReuseStrategy;
|
||||
routerState: RouterState;
|
||||
url: string;
|
||||
urlHandlingStrategy: UrlHandlingStrategy;
|
||||
|
@ -224,6 +228,15 @@ export declare const ROUTER_CONFIGURATION: OpaqueToken;
|
|||
/** @experimental */
|
||||
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 */
|
||||
export declare class RouterLink {
|
||||
fragment: string;
|
||||
|
@ -298,7 +311,9 @@ export declare class RouterOutlet implements OnDestroy {
|
|||
outletMap: RouterOutletMap;
|
||||
constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string);
|
||||
activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void;
|
||||
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void;
|
||||
deactivate(): void;
|
||||
detach(): ComponentRef<any>;
|
||||
ngOnDestroy(): void;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue