feat(router): add support for canActivateChild

This commit is contained in:
vsavkin 2016-07-13 15:15:20 -07:00
parent 961c9d48ae
commit 9e3d13f61f
5 changed files with 157 additions and 32 deletions

View File

@ -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';

View File

@ -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;

View File

@ -56,6 +56,66 @@ export interface CanActivate {
Observable<boolean>|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<boolean> {
* 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>|boolean;
}
/**
* An interface a class can implement to be a guard deciding if a route can be deactivated.
*

View File

@ -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<CanActivate|CanDeactivate> = [];
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<boolean> {
@ -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<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
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<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
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<boolean> {
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<boolean> {
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<boolean> {
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<any>>): Observable<boolean> {
return observables.mergeAll().every(result => result === true);
}
function pushQueryParamsAndFragment(state: RouterState): void {
if (!shallowEqual(state.snapshot.queryParams, (<any>state.queryParams).value)) {
(<any>state.queryParams).next(state.snapshot.queryParams);

View File

@ -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([{