feat(router): add support for canActivateChild
This commit is contained in:
parent
961c9d48ae
commit
9e3d13f61f
|
@ -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';
|
||||
|
|
|
@ -242,6 +242,8 @@ export type RouterConfig = Route[];
|
|||
* - `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.
|
||||
* - `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.
|
||||
* - `data` is additional data provided to the component via `ActivatedRoute`.
|
||||
|
@ -487,6 +489,7 @@ export interface Route {
|
|||
redirectTo?: string;
|
||||
outlet?: string;
|
||||
canActivate?: any[];
|
||||
canActivateChild?: any[];
|
||||
canDeactivate?: any[];
|
||||
data?: Data;
|
||||
resolve?: ResolveData;
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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 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));
|
||||
}
|
||||
})
|
||||
.mergeAll()
|
||||
.every(result => result === true);
|
||||
});
|
||||
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);
|
||||
|
|
|
@ -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([{
|
||||
|
|
Loading…
Reference in New Issue