feat(router): implement CandDeactivate
This commit is contained in:
		
							parent
							
								
									1914847e72
								
							
						
					
					
						commit
						ab958598d7
					
				| @ -8,5 +8,6 @@ export interface Route { | ||||
|   component: Type | string; | ||||
|   outlet?: string; | ||||
|   canActivate?: any[], | ||||
|   canDeactivate?: any[], | ||||
|   children?: Route[]; | ||||
| } | ||||
| @ -12,7 +12,7 @@ export function createRouterState(curr: RouterStateSnapshot, prevState: RouterSt | ||||
| function createNode(curr:TreeNode<ActivatedRouteSnapshot>, prevState?:TreeNode<ActivatedRoute>):TreeNode<ActivatedRoute> { | ||||
|   if (prevState && equalRouteSnapshots(prevState.value.snapshot, curr.value)) { | ||||
|     const value = prevState.value; | ||||
|     value.snapshot = curr.value; | ||||
|     value._futureSnapshot = curr.value; | ||||
|      | ||||
|     const children = createOrReuseChildren(curr, prevState); | ||||
|     return new TreeNode<ActivatedRoute>(value, children); | ||||
|  | ||||
| @ -13,7 +13,11 @@ export class RouterOutlet { | ||||
|   } | ||||
| 
 | ||||
|   get isActivated(): boolean { return !!this.activated; } | ||||
|    | ||||
|   get component(): Object { | ||||
|     if (!this.activated) throw new Error("Outlet is not activated"); | ||||
|     return this.activated.instance; | ||||
|   } | ||||
| 
 | ||||
|   deactivate(): void { | ||||
|     if (this.activated) { | ||||
|       this.activated.destroy(); | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| export { Router } from './router'; | ||||
| export { UrlSerializer, DefaultUrlSerializer } from './url_serializer'; | ||||
| export { RouterState, ActivatedRoute } from './router_state'; | ||||
| export { RouterState, ActivatedRoute, RouterStateSnapshot, ActivatedRouteSnapshot } from './router_state'; | ||||
| export { UrlTree, UrlSegment} from './url_tree'; | ||||
| export { RouterOutletMap } from './router_outlet_map'; | ||||
| export { RouterConfig, Route } from './config'; | ||||
|  | ||||
| @ -8,7 +8,7 @@ import { createRouterState } from './create_router_state'; | ||||
| import { TreeNode } from './utils/tree'; | ||||
| import { UrlTree, createEmptyUrlTree } from './url_tree'; | ||||
| import { PRIMARY_OUTLET, Params } from './shared'; | ||||
| import { createEmptyState, RouterState, RouterStateSnapshot, ActivatedRoute, ActivatedRouteSnapshot} from './router_state'; | ||||
| import { createEmptyState, RouterState, RouterStateSnapshot, ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from './router_state'; | ||||
| import { RouterConfig } from './config'; | ||||
| import { RouterOutlet } from './directives/router_outlet'; | ||||
| import { createUrlTree } from './create_url_tree'; | ||||
| @ -190,6 +190,9 @@ export class Router { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class CanActivate { constructor(public route: ActivatedRouteSnapshot) {}} | ||||
| class CanDeactivate { constructor(public component: Object, public route: ActivatedRouteSnapshot) {}} | ||||
| 
 | ||||
| class GuardChecks { | ||||
|   private checks = []; | ||||
|   constructor(private future: RouterStateSnapshot, private curr: RouterStateSnapshot, private injector: Injector) {} | ||||
| @ -199,38 +202,52 @@ class GuardChecks { | ||||
|     const currRoot = this.curr ? this.curr._root : null; | ||||
|     this.traverseChildRoutes(futureRoot, currRoot, parentOutletMap); | ||||
|     if (this.checks.length === 0) return of(true); | ||||
|     return forkJoin(this.checks.map(s => this.runCanActivate(s))).map(and); | ||||
|     return forkJoin(this.checks.map(s => { | ||||
|       if (s instanceof CanActivate) { | ||||
|         return this.runCanActivate(s.route) | ||||
|       } else if (s instanceof CanDeactivate) { | ||||
|         return this.runCanDeactivate(s.component, s.route); | ||||
|       } else { | ||||
|         throw new Error("Cannot be reached"); | ||||
|       } | ||||
|     })).map(and); | ||||
|   } | ||||
| 
 | ||||
|   private traverseChildRoutes(futureNode: TreeNode<ActivatedRouteSnapshot>, | ||||
|                               currNode: TreeNode<ActivatedRouteSnapshot> | null, | ||||
|                               outletMap: RouterOutletMap): void { | ||||
|                               outletMap: RouterOutletMap | null): void { | ||||
|     const prevChildren = nodeChildrenAsMap(currNode); | ||||
|     futureNode.children.forEach(c => { | ||||
|       this.traverseRoutes(c, prevChildren[c.value.outlet], outletMap); | ||||
|       delete prevChildren[c.value.outlet]; | ||||
|     }); | ||||
|     forEach(prevChildren, (v, k) => this.deactivateOutletAndItChildren(outletMap._outlets[k])); | ||||
|     forEach(prevChildren, (v, k) => this.deactivateOutletAndItChildren(v, outletMap._outlets[k])); | ||||
|   } | ||||
| 
 | ||||
|   traverseRoutes(futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot> | null, | ||||
|                  parentOutletMap: RouterOutletMap): void { | ||||
|                  parentOutletMap: RouterOutletMap | null): void { | ||||
|     const future = futureNode.value; | ||||
|     const curr = currNode ? currNode.value : null; | ||||
|     const outlet = parentOutletMap ? parentOutletMap._outlets[futureNode.value.outlet] : null; | ||||
| 
 | ||||
|     if (curr && future === curr) { | ||||
|     if (curr && future._routeConfig === curr._routeConfig) { | ||||
|       if (!shallowEqual(future.params, curr.params)) { | ||||
|         this.checks.push(future); | ||||
|         this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(future)); | ||||
|       } | ||||
|       this.traverseChildRoutes(futureNode, currNode, <any>null); | ||||
|       this.traverseChildRoutes(futureNode, currNode, outlet ? outlet.outletMap : null); | ||||
|     } else { | ||||
|       this.deactivateOutletAndItChildren(<any>null); | ||||
|       this.checks.push(future); | ||||
|       this.traverseChildRoutes(futureNode, null, <any>null); | ||||
|       this.deactivateOutletAndItChildren(curr, outlet); | ||||
|       this.checks.push(new CanActivate(future)); | ||||
|       this.traverseChildRoutes(futureNode, null, outlet ? outlet.outletMap : null); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private deactivateOutletAndItChildren(outlet: RouterOutlet): void {} | ||||
|   private deactivateOutletAndItChildren(route: ActivatedRouteSnapshot, outlet: RouterOutlet): void { | ||||
|     if (outlet && outlet.isActivated) { | ||||
|       forEach(outlet.outletMap._outlets, (v, k) => this.deactivateOutletAndItChildren(v, outlet.outletMap._outlets[k])); | ||||
|       this.checks.push(new CanDeactivate(outlet.component, route)) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> { | ||||
|     const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; | ||||
| @ -240,6 +257,15 @@ class GuardChecks { | ||||
|       return of(guard(future, this.future)); | ||||
|     })).map(and); | ||||
|   } | ||||
| 
 | ||||
|   private runCanDeactivate(component: Object, curr: ActivatedRouteSnapshot): Observable<boolean> { | ||||
|     const canDeactivate = curr._routeConfig ? curr._routeConfig.canDeactivate : null; | ||||
|     if (!canDeactivate || canDeactivate.length === 0) return of(true); | ||||
|     return forkJoin(canDeactivate.map(c => { | ||||
|       const guard = this.injector.get(c); | ||||
|       return of(guard(component, curr, this.curr)); | ||||
|     })).map(and); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ActivateRoutes { | ||||
| @ -271,7 +297,7 @@ class ActivateRoutes { | ||||
|     const outlet = getOutlet(parentOutletMap, futureNode.value); | ||||
| 
 | ||||
|     if (future === curr) { | ||||
|       pushValues(future); | ||||
|       advanceActivatedRoute(future); | ||||
|       this.activateChildRoutes(futureNode, currNode, outlet.outletMap); | ||||
|     } else { | ||||
|       this.deactivateOutletAndItChildren(outlet); | ||||
| @ -286,8 +312,8 @@ class ActivateRoutes { | ||||
|       {provide: ActivatedRoute, useValue: future}, | ||||
|       {provide: RouterOutletMap, useValue: outletMap} | ||||
|     ]); | ||||
|     outlet.activate(future.snapshot._resolvedComponentFactory, resolved, outletMap); | ||||
|     pushValues(future); | ||||
|     outlet.activate(future._futureSnapshot._resolvedComponentFactory, resolved, outletMap); | ||||
|     advanceActivatedRoute(future); | ||||
|   } | ||||
| 
 | ||||
|   private deactivateOutletAndItChildren(outlet: RouterOutlet): void { | ||||
| @ -298,13 +324,6 @@ class ActivateRoutes { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function pushValues(route: ActivatedRoute): void { | ||||
|   if (!shallowEqual(route.snapshot.params, (<any>route.params).value)) { | ||||
|     (<any>route.urlSegments).next(route.snapshot.urlSegments); | ||||
|     (<any>route.params).next(route.snapshot.params); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function pushQueryParamsAndFragment(state: RouterState): void { | ||||
|   if (!shallowEqual(state.snapshot.queryParams, (<any>state.queryParams).value)) { | ||||
|     (<any>state.queryParams).next(state.snapshot.queryParams); | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { Tree, TreeNode } from './utils/tree'; | ||||
| import { shallowEqual } from './utils/collection'; | ||||
| import { UrlSegment } from './url_tree'; | ||||
| import { Route } from './config'; | ||||
| import { Params, PRIMARY_OUTLET } from './shared'; | ||||
| @ -34,6 +35,7 @@ export function createEmptyState(rootComponent: Type): RouterState { | ||||
|   const emptyQueryParams = new BehaviorSubject({}); | ||||
|   const fragment = new BehaviorSubject(""); | ||||
|   const activated = new ActivatedRoute(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, snapshot.root); | ||||
|   activated.snapshot = snapshot.root; | ||||
|   return new RouterState(new TreeNode<ActivatedRoute>(activated, []), emptyQueryParams, fragment, snapshot); | ||||
| } | ||||
| 
 | ||||
| @ -62,12 +64,18 @@ function createEmptyStateSnapshot(rootComponent: Type): RouterStateSnapshot { | ||||
|  * ``` | ||||
|  */ | ||||
| export class ActivatedRoute { | ||||
|   /** @internal */ | ||||
|   _futureSnapshot: ActivatedRouteSnapshot; | ||||
|   snapshot: ActivatedRouteSnapshot; | ||||
| 
 | ||||
|   constructor(public urlSegments: Observable<UrlSegment[]>, | ||||
|               public params: Observable<Params>, | ||||
|               public outlet: string, | ||||
|               public component: Type | string, | ||||
|               public snapshot: ActivatedRouteSnapshot | ||||
|   ) {} | ||||
|               futureSnapshot: ActivatedRouteSnapshot | ||||
|   ) { | ||||
|     this._futureSnapshot = futureSnapshot; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @ -123,4 +131,19 @@ export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> { | ||||
|   constructor(root: TreeNode<ActivatedRouteSnapshot>, public queryParams: Params, public fragment: string | null) { | ||||
|     super(root); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * The expectation is that the activate route is created with the right set of parameters. | ||||
|  * So we push new values into the observables only when they are not the initial values. | ||||
|  * And we detect that by checking if the snapshot field is set. | ||||
|  */ | ||||
| export function advanceActivatedRoute(route: ActivatedRoute): void { | ||||
|   if (route.snapshot && !shallowEqual(route.snapshot.params, route._futureSnapshot.params)) { | ||||
|     route.snapshot = route._futureSnapshot; | ||||
|     (<any>route.urlSegments).next(route.snapshot.urlSegments); | ||||
|     (<any>route.params).next(route.snapshot.params); | ||||
|   } else { | ||||
|     route.snapshot = route._futureSnapshot; | ||||
|   } | ||||
| } | ||||
| @ -1,7 +1,8 @@ | ||||
| import {DefaultUrlSerializer} from '../src/url_serializer'; | ||||
| import {UrlTree} from '../src/url_tree'; | ||||
| import {TreeNode} from '../src/utils/tree'; | ||||
| import {Params, PRIMARY_OUTLET} from '../src/shared'; | ||||
| import {ActivatedRoute, ActivatedRouteSnapshot, RouterStateSnapshot, createEmptyState} from '../src/router_state'; | ||||
| import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState, advanceActivatedRoute} from '../src/router_state'; | ||||
| import {createRouterState} from '../src/create_router_state'; | ||||
| import {recognize} from '../src/recognize'; | ||||
| import {RouterConfig} from '../src/config'; | ||||
| @ -32,6 +33,7 @@ describe('create router state', () => { | ||||
|     ]; | ||||
| 
 | ||||
|     const prevState = createRouterState(createState(config, "a(left:b)"), emptyState()); | ||||
|     advanceState(prevState); | ||||
|     const state = createRouterState(createState(config, "a(left:c)"), prevState); | ||||
| 
 | ||||
|     expect(prevState.root).toBe(state.root); | ||||
| @ -44,6 +46,15 @@ describe('create router state', () => { | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| function advanceState(state: RouterState): void { | ||||
|   advanceNode(state._root); | ||||
| } | ||||
| 
 | ||||
| function advanceNode(node: TreeNode<ActivatedRoute>): void { | ||||
|   advanceActivatedRoute(node.value); | ||||
|   node.children.forEach(advanceNode); | ||||
| } | ||||
| 
 | ||||
| function createState(config: RouterConfig, url: string): RouterStateSnapshot { | ||||
|   let res; | ||||
|   recognize(RootComponent, config, tree(url)).forEach(s => res = s); | ||||
|  | ||||
| @ -1,8 +1,9 @@ | ||||
| import {DefaultUrlSerializer} from '../src/url_serializer'; | ||||
| import {UrlTree, UrlSegment} from '../src/url_tree'; | ||||
| import {ActivatedRoute, ActivatedRouteSnapshot} from '../src/router_state'; | ||||
| import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state'; | ||||
| import {PRIMARY_OUTLET, Params} from '../src/shared'; | ||||
| import {createUrlTree} from '../src/create_url_tree'; | ||||
| import {BehaviorSubject} from 'rxjs/BehaviorSubject'; | ||||
| 
 | ||||
| describe('createUrlTree', () => { | ||||
|   const serializer = new DefaultUrlSerializer(); | ||||
| @ -174,7 +175,8 @@ function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryP | ||||
|   if (!start) { | ||||
|     expect(start).toBeDefined(); | ||||
|   } | ||||
|   const s = new ActivatedRouteSnapshot([], <any>null, PRIMARY_OUTLET, "someComponent", null, <any>start); | ||||
|   const a = new ActivatedRoute(<any>null, <any>null, PRIMARY_OUTLET, "someComponent", s); | ||||
|   const s = new ActivatedRouteSnapshot([], <any>{}, PRIMARY_OUTLET, "someComponent", null, <any>start); | ||||
|   const a = new ActivatedRoute(new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, "someComponent", s); | ||||
|   advanceActivatedRoute(a); | ||||
|   return createUrlTree(a, tree, commands, queryParameters, fragment); | ||||
| } | ||||
| @ -2,6 +2,7 @@ import {Component, Injector} from '@angular/core'; | ||||
| import { | ||||
|   describe, | ||||
|   ddescribe, | ||||
|   xdescribe, | ||||
|   it, | ||||
|   iit, | ||||
|   xit, | ||||
| @ -16,7 +17,8 @@ import { | ||||
| import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing'; | ||||
| import { ComponentResolver } from '@angular/core'; | ||||
| import { SpyLocation } from '@angular/common/testing'; | ||||
| import { UrlSerializer, DefaultUrlSerializer, RouterOutletMap, Router, ActivatedRoute, ROUTER_DIRECTIVES, Params } from '../src/index'; | ||||
| import { UrlSerializer, DefaultUrlSerializer, RouterOutletMap, Router, ActivatedRoute, ROUTER_DIRECTIVES, Params, | ||||
|  RouterStateSnapshot, ActivatedRouteSnapshot } from '../src/index'; | ||||
| import { Observable } from 'rxjs/Observable'; | ||||
| import 'rxjs/add/operator/map'; | ||||
| 
 | ||||
| @ -328,7 +330,7 @@ describe("Integration", () => { | ||||
| 
 | ||||
|       describe("should activate a route when CanActivate returns true", () => { | ||||
|         beforeEachProviders(() => [ | ||||
|           {provide: 'alwaysFalse', useValue: (a, b) => true} | ||||
|           {provide: 'alwaysFalse', useValue: (a:ActivatedRouteSnapshot, s:RouterStateSnapshot) => true} | ||||
|         ]); | ||||
| 
 | ||||
|         it('works', | ||||
| @ -347,6 +349,42 @@ describe("Integration", () => { | ||||
|           }))); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     describe("CanDeactivate", () => { | ||||
|       describe("should not deactivate a route when CanDeactivate returns false", () => { | ||||
|         beforeEachProviders(() => [ | ||||
|           {provide: 'CanDeactivate', useValue: (c:TeamCmp, a:ActivatedRouteSnapshot, b:RouterStateSnapshot) => { | ||||
|             return c.route.snapshot.params['id'] === "22"; | ||||
|           }} | ||||
|         ]); | ||||
| 
 | ||||
| 
 | ||||
|         it('works', | ||||
|           fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { | ||||
|             router.resetConfig([ | ||||
|               { path: 'team/:id', component: TeamCmp, canDeactivate: ["CanDeactivate"] } | ||||
|             ]); | ||||
| 
 | ||||
|             const fixture = tcb.createFakeAsync(RootCmp); | ||||
|             advance(fixture); | ||||
| 
 | ||||
|             router.navigateByUrl('/team/22'); | ||||
|             advance(fixture); | ||||
| 
 | ||||
|             expect(location.path()).toEqual('/team/22'); | ||||
| 
 | ||||
|             router.navigateByUrl('/team/33'); | ||||
|             advance(fixture); | ||||
| 
 | ||||
|             expect(location.path()).toEqual('/team/33'); | ||||
| 
 | ||||
|             router.navigateByUrl('/team/44'); | ||||
|             advance(fixture); | ||||
| 
 | ||||
|             expect(location.path()).toEqual('/team/33'); | ||||
|           }))); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| @ -388,7 +426,7 @@ class TeamCmp { | ||||
|   id: Observable<string>; | ||||
|   recordedParams: Params[] = []; | ||||
| 
 | ||||
|   constructor(route: ActivatedRoute) { | ||||
|   constructor(public route: ActivatedRoute) { | ||||
|     this.id = route.params.map(p => p['id']); | ||||
|     route.params.forEach(_ => this.recordedParams.push(_)); | ||||
|   } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user