From 42cf06fa12924882d18d1167432214fdc66b1158 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Tue, 29 Nov 2016 23:21:41 -0800 Subject: [PATCH] feat(router): add support for custom route reuse strategies --- .../router/src/create_router_state.ts | 53 ++++++--- .../router/src/directives/router_outlet.ts | 16 +++ modules/@angular/router/src/index.ts | 1 + .../router/src/route_reuse_strategy.ts | 64 +++++++++++ modules/@angular/router/src/router.ts | 63 +++++++++-- modules/@angular/router/src/router_module.ts | 13 ++- .../router/test/create_router_state.spec.ts | 28 +++-- .../@angular/router/test/integration.spec.ts | 101 +++++++++++++++++- tools/public_api_guard/router/index.d.ts | 15 +++ 9 files changed, 319 insertions(+), 35 deletions(-) create mode 100644 modules/@angular/router/src/route_reuse_strategy.ts diff --git a/modules/@angular/router/src/create_router_state.ts b/modules/@angular/router/src/create_router_state.ts index c05b32d8bb..a858cf8e9a 100644 --- a/modules/@angular/router/src/create_router_state.ts +++ b/modules/@angular/router/src/create_router_state.ts @@ -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, prevState?: TreeNode): - TreeNode { - if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) { +function createNode( + routeReuseStrategy: RouteReuseStrategy, curr: TreeNode, + prevState?: TreeNode): TreeNode { + // 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(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 = + (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(value, children); } } +function setFutureSnapshotsOfActivatedRoutes( + curr: TreeNode, result: TreeNode): 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, prevState: TreeNode) { + routeReuseStrategy: RouteReuseStrategy, curr: TreeNode, + prevState: TreeNode) { 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; } \ No newline at end of file diff --git a/modules/@angular/router/src/directives/router_outlet.ts b/modules/@angular/router/src/directives/router_outlet.ts index 6ae5675c69..c0e3125963 100644 --- a/modules/@angular/router/src/directives/router_outlet.ts +++ b/modules/@angular/router/src/directives/router_outlet.ts @@ -67,11 +67,27 @@ export class RouterOutlet implements OnDestroy { return this._activatedRoute; } + detach(): ComponentRef { + 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, 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); } } diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index 911ea02989..3bfcdc9092 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -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'; diff --git a/modules/@angular/router/src/route_reuse_strategy.ts b/modules/@angular/router/src/route_reuse_strategy.ts new file mode 100644 index 0000000000..150d6d8084 --- /dev/null +++ b/modules/@angular/router/src/route_reuse_strategy.ts @@ -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, + route: TreeNode +}; + + +/** + * @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; +} \ No newline at end of file diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 5aea6eeef5..5b5cd3551b 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -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 }; + +/** + * 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 = + (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, parentOutletMap: RouterOutletMap): void { + if (this.routeReuseStrategy.shouldDetach(route.value.snapshot)) { + this.detachAndStoreRouteSubtree(route, parentOutletMap); + } else { + this.deactiveRouteAndOutlet(route, parentOutletMap); + } + } + + private detachAndStoreRouteSubtree( + route: TreeNode, 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, parentOutletMap: RouterOutletMap): + void { const prevChildren: {[key: string]: any} = nodeChildrenAsMap(route); let outlet: RouterOutlet = null; @@ -1151,6 +1197,11 @@ class ActivateRoutes { } } +function advanceActivatedRouteNodeAndItsChildren(node: TreeNode): void { + advanceActivatedRoute(node.value); + node.children.forEach(advanceActivatedRouteNodeAndItsChildren); +} + function parentLoadedConfig(snapshot: ActivatedRouteSnapshot): LoadedRouterConfig { let s = snapshot.parent; while (s) { diff --git a/modules/@angular/router/src/router_module.ts b/modules/@angular/router/src/router_module.ts index 77381b9547..f4863a3431 100644 --- a/modules/@angular/router/src/router_module.ts +++ b/modules/@angular/router/src/router_module.ts @@ -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; } diff --git a/modules/@angular/router/test/create_router_state.spec.ts b/modules/@angular/router/test/create_router_state.spec.ts index 9ef739f9f7..e5a79fad5a 100644 --- a/modules/@angular/router/test/create_router_state.spec.ts +++ b/modules/@angular/router/test/create_router_state.spec.ts @@ -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); diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index e81c74cc74..897d5451f5 100644 --- a/modules/@angular/router/test/integration.spec.ts +++ b/modules/@angular/router/test/integration.spec.ts @@ -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[]) { diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index e574983efe..88d558710c 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -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; 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, activatedRoute: ActivatedRoute): void; deactivate(): void; + detach(): ComponentRef; ngOnDestroy(): void; }