From 9e3d13f61f5b04e03c1234cdf05e1834ec41c8cd Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 13 Jul 2016 15:15:20 -0700 Subject: [PATCH] feat(router): add support for canActivateChild --- modules/@angular/router/index.ts | 2 +- modules/@angular/router/src/config.ts | 7 +- modules/@angular/router/src/interfaces.ts | 60 ++++++++++++++ modules/@angular/router/src/router.ts | 87 ++++++++++++++------- modules/@angular/router/test/router.spec.ts | 33 ++++++++ 5 files changed, 157 insertions(+), 32 deletions(-) diff --git a/modules/@angular/router/index.ts b/modules/@angular/router/index.ts index 6ad67a89f5..6e21a68b73 100644 --- a/modules/@angular/router/index.ts +++ b/modules/@angular/router/index.ts @@ -12,7 +12,7 @@ export {Data, ResolveData, Route, RouterConfig, Routes} from './src/config'; export {RouterLink, RouterLinkWithHref} from './src/directives/router_link'; export {RouterLinkActive} from './src/directives/router_link_active'; export {RouterOutlet} from './src/directives/router_outlet'; -export {CanActivate, CanDeactivate, Resolve} from './src/interfaces'; +export {CanActivate, CanActivateChild, CanDeactivate, Resolve} from './src/interfaces'; export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RoutesRecognized} from './src/router'; export {ROUTER_DIRECTIVES, RouterModule} from './src/router_module'; export {RouterOutletMap} from './src/router_outlet_map'; diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index 36eb6a4bae..665c754bbc 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -241,9 +241,11 @@ export type RouterConfig = Route[]; * - `redirectTo` is the url fragment which will replace the current matched segment. * - `outlet` is the name of the outlet the component should be placed into. * - `canActivate` is an array of DI tokens used to look up CanActivate handlers. See {@link - * CanActivate} for more info. + * CanActivate} for more info. + * - `canActivateChild` is an array of DI tokens used to look up CanActivateChild handlers. See {@link + * CanActivateChild} for more info. * - `canDeactivate` is an array of DI tokens used to look up CanDeactivate handlers. See {@link - * CanDeactivate} for more info. + * CanDeactivate} for more info. * - `data` is additional data provided to the component via `ActivatedRoute`. * - `resolve` is a map of DI tokens used to look up data resolvers. See {@link Resolve} for more * info. @@ -487,6 +489,7 @@ export interface Route { redirectTo?: string; outlet?: string; canActivate?: any[]; + canActivateChild?: any[]; canDeactivate?: any[]; data?: Data; resolve?: ResolveData; diff --git a/modules/@angular/router/src/interfaces.ts b/modules/@angular/router/src/interfaces.ts index b8267e536d..05868f4d54 100644 --- a/modules/@angular/router/src/interfaces.ts +++ b/modules/@angular/router/src/interfaces.ts @@ -56,6 +56,66 @@ export interface CanActivate { Observable|boolean; } +/** + * An interface a class can implement to be a guard deciding if a child route can be activated. + * + * ### Example + * + * ``` + * @Injectable() + * class CanActivateTeam implements CanActivate { + * constructor(private permissions: Permissions, private currentUser: UserToken) {} + * + * canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):Observable { + * return this.permissions.canActivate(this.currentUser, this.route.params.id); + * } + * } + * + * bootstrap(AppComponent, [ + * CanActivateTeam, + * + * provideRouter([ + * { + * path: 'root', + * canActivateChild: [CanActivateTeam], + * children: [ + * { + * path: 'team/:id', + * component: Team + * } + * ] + * } + * ]); + * ``` + * + * You can also provide a function with the same signature instead of the class: + * + * ``` + * bootstrap(AppComponent, [ + * {provide: 'canActivateTeam', useValue: (route: ActivatedRouteSnapshot, state: + * RouterStateSnapshot) => true}, + * provideRouter([ + * { + * path: 'root', + * canActivateChild: [CanActivateTeam], + * children: [ + * { + * path: 'team/:id', + * component: Team + * } + * ] + * } + * ]); + * ``` + * + * @stable + */ + +export interface CanActivateChild { + canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): + Observable|boolean; +} + /** * An interface a class can implement to be a guard deciding if a route can be deactivated. * diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 63cbaa4320..705938302b 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -387,20 +387,18 @@ export class Router { } } -/** - * @experimental - */ + class CanActivate { - constructor(public route: ActivatedRouteSnapshot) {} + constructor(public path: ActivatedRouteSnapshot[]) {} + + get route(): ActivatedRouteSnapshot { return this.path[this.path.length - 1]; } } -/** - * @experimental - */ class CanDeactivate { constructor(public component: Object, public route: ActivatedRouteSnapshot) {} } + class PreActivation { private checks: Array = []; constructor( @@ -410,7 +408,7 @@ class PreActivation { traverse(parentOutletMap: RouterOutletMap): void { const futureRoot = this.future._root; const currRoot = this.curr ? this.curr._root : null; - this.traverseChildRoutes(futureRoot, currRoot, parentOutletMap); + this.traverseChildRoutes(futureRoot, currRoot, parentOutletMap, [futureRoot.value]); } checkGuards(): Observable { @@ -418,7 +416,8 @@ class PreActivation { return Observable.from(this.checks) .map(s => { if (s instanceof CanActivate) { - return this.runCanActivate(s.route); + return andObservables( + Observable.from([this.runCanActivate(s.route), this.runCanActivateChild(s.path)])); } else if (s instanceof CanDeactivate) { // workaround https://github.com/Microsoft/TypeScript/issues/7271 const s2 = s as CanDeactivate; @@ -446,10 +445,10 @@ class PreActivation { private traverseChildRoutes( futureNode: TreeNode, currNode: TreeNode, - outletMap: RouterOutletMap): void { + outletMap: RouterOutletMap, futurePath: ActivatedRouteSnapshot[]): void { const prevChildren: {[key: string]: any} = nodeChildrenAsMap(currNode); futureNode.children.forEach(c => { - this.traverseRoutes(c, prevChildren[c.value.outlet], outletMap); + this.traverseRoutes(c, prevChildren[c.value.outlet], outletMap, futurePath.concat([c.value])); delete prevChildren[c.value.outlet]; }); forEach( @@ -459,7 +458,7 @@ class PreActivation { traverseRoutes( futureNode: TreeNode, currNode: TreeNode, - parentOutletMap: RouterOutletMap): void { + parentOutletMap: RouterOutletMap, futurePath: ActivatedRouteSnapshot[]): void { const future = futureNode.value; const curr = currNode ? currNode.value : null; const outlet = parentOutletMap ? parentOutletMap._outlets[futureNode.value.outlet] : null; @@ -467,16 +466,17 @@ class PreActivation { // reusing the node if (curr && future._routeConfig === curr._routeConfig) { if (!shallowEqual(future.params, curr.params)) { - this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(future)); + this.checks.push(new CanDeactivate(outlet.component, curr), new CanActivate(futurePath)); } // If we have a component, we need to go through an outlet. if (future.component) { - this.traverseChildRoutes(futureNode, currNode, outlet ? outlet.outletMap : null); + this.traverseChildRoutes( + futureNode, currNode, outlet ? outlet.outletMap : null, futurePath); // if we have a componentless route, we recurse but keep the same outlet map. } else { - this.traverseChildRoutes(futureNode, currNode, parentOutletMap); + this.traverseChildRoutes(futureNode, currNode, parentOutletMap, futurePath); } } else { if (curr) { @@ -490,14 +490,14 @@ class PreActivation { } } - this.checks.push(new CanActivate(future)); + this.checks.push(new CanActivate(futurePath)); // If we have a component, we need to go through an outlet. if (future.component) { - this.traverseChildRoutes(futureNode, null, outlet ? outlet.outletMap : null); + this.traverseChildRoutes(futureNode, null, outlet ? outlet.outletMap : null, futurePath); // if we have a componentless route, we recurse but keep the same outlet map. } else { - this.traverseChildRoutes(futureNode, null, parentOutletMap); + this.traverseChildRoutes(futureNode, null, parentOutletMap, futurePath); } } } @@ -520,19 +520,44 @@ class PreActivation { private runCanActivate(future: ActivatedRouteSnapshot): Observable { const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return Observable.of(true); - return Observable.from(canActivate) - .map(c => { - const guard = this.injector.get(c); - if (guard.canActivate) { - return wrapIntoObservable(guard.canActivate(future, this.future)); - } else { - return wrapIntoObservable(guard(future, this.future)); - } - }) - .mergeAll() - .every(result => result === true); + const obs = Observable.from(canActivate).map(c => { + const guard = this.injector.get(c); + if (guard.canActivate) { + return wrapIntoObservable(guard.canActivate(future, this.future)); + } else { + return wrapIntoObservable(guard(future, this.future)); + } + }); + return andObservables(obs); } + private runCanActivateChild(path: ActivatedRouteSnapshot[]): Observable { + const future = path[path.length - 1]; + + const canActivateChildGuards = + path.slice(0, path.length - 1) + .reverse() + .map(p => { + const canActivateChild = p._routeConfig ? p._routeConfig.canActivateChild : null; + if (!canActivateChild || canActivateChild.length === 0) return null; + return {snapshot: future, node: p, guards: canActivateChild}; + }) + .filter(_ => _ !== null); + + return andObservables(Observable.from(canActivateChildGuards).map(d => { + const obs = Observable.from(d.guards).map(c => { + const guard = this.injector.get(c); + if (guard.canActivateChild) { + return wrapIntoObservable(guard.canActivateChild(d.snapshot, this.future)); + } else { + return wrapIntoObservable(guard(d.snapshot, this.future)); + } + }); + return andObservables(obs); + })); + } + + private runCanDeactivate(component: Object, curr: ActivatedRouteSnapshot): Observable { const canDeactivate = curr && curr._routeConfig ? curr._routeConfig.canDeactivate : null; if (!canDeactivate || canDeactivate.length === 0) return Observable.of(true); @@ -682,6 +707,10 @@ class ActivateRoutes { } } +function andObservables(observables: Observable>): Observable { + return observables.mergeAll().every(result => result === true); +} + function pushQueryParamsAndFragment(state: RouterState): void { if (!shallowEqual(state.snapshot.queryParams, (state.queryParams).value)) { (state.queryParams).next(state.snapshot.queryParams); diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index dfbe2200d2..963a2233eb 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -1037,6 +1037,39 @@ describe('Integration', () => { }); }); + describe('CanActivateChild', () => { + describe('should be invoked when activating a child', () => { + beforeEach(() => { + addProviders([{ + provide: 'alwaysFalse', + useValue: (a: any, b: any) => { return a.params.id === '22'; } + }]); + }); + + it('works', fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{ + path: '', + canActivateChild: ['alwaysFalse'], + children: [{path: 'team/:id', component: TeamCmp}] + }]); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + + router.navigateByUrl('/team/33').catch(() => {}); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + }))); + }); + }); + describe('should work when returns an observable', () => { beforeEach(() => { addProviders([{