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 {RouterLink, RouterLinkWithHref} from './src/directives/router_link';
|
||||||
export {RouterLinkActive} from './src/directives/router_link_active';
|
export {RouterLinkActive} from './src/directives/router_link_active';
|
||||||
export {RouterOutlet} from './src/directives/router_outlet';
|
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 {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router, RoutesRecognized} from './src/router';
|
||||||
export {ROUTER_DIRECTIVES, RouterModule} from './src/router_module';
|
export {ROUTER_DIRECTIVES, RouterModule} from './src/router_module';
|
||||||
export {RouterOutletMap} from './src/router_outlet_map';
|
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.
|
* - `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` 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` 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`.
|
* - `data` is additional data provided to the component via `ActivatedRoute`.
|
||||||
|
@ -487,6 +489,7 @@ export interface Route {
|
||||||
redirectTo?: string;
|
redirectTo?: string;
|
||||||
outlet?: string;
|
outlet?: string;
|
||||||
canActivate?: any[];
|
canActivate?: any[];
|
||||||
|
canActivateChild?: any[];
|
||||||
canDeactivate?: any[];
|
canDeactivate?: any[];
|
||||||
data?: Data;
|
data?: Data;
|
||||||
resolve?: ResolveData;
|
resolve?: ResolveData;
|
||||||
|
|
|
@ -56,6 +56,66 @@ export interface CanActivate {
|
||||||
Observable<boolean>|boolean;
|
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.
|
* 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 {
|
class CanActivate {
|
||||||
constructor(public route: ActivatedRouteSnapshot) {}
|
constructor(public path: ActivatedRouteSnapshot[]) {}
|
||||||
|
|
||||||
|
get route(): ActivatedRouteSnapshot { return this.path[this.path.length - 1]; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
class CanDeactivate {
|
class CanDeactivate {
|
||||||
constructor(public component: Object, public route: ActivatedRouteSnapshot) {}
|
constructor(public component: Object, public route: ActivatedRouteSnapshot) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PreActivation {
|
class PreActivation {
|
||||||
private checks: Array<CanActivate|CanDeactivate> = [];
|
private checks: Array<CanActivate|CanDeactivate> = [];
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -410,7 +408,7 @@ class PreActivation {
|
||||||
traverse(parentOutletMap: RouterOutletMap): void {
|
traverse(parentOutletMap: RouterOutletMap): void {
|
||||||
const futureRoot = this.future._root;
|
const futureRoot = this.future._root;
|
||||||
const currRoot = this.curr ? this.curr._root : null;
|
const currRoot = this.curr ? this.curr._root : null;
|
||||||
this.traverseChildRoutes(futureRoot, currRoot, parentOutletMap);
|
this.traverseChildRoutes(futureRoot, currRoot, parentOutletMap, [futureRoot.value]);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkGuards(): Observable<boolean> {
|
checkGuards(): Observable<boolean> {
|
||||||
|
@ -418,7 +416,8 @@ class PreActivation {
|
||||||
return Observable.from(this.checks)
|
return Observable.from(this.checks)
|
||||||
.map(s => {
|
.map(s => {
|
||||||
if (s instanceof CanActivate) {
|
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) {
|
} else if (s instanceof CanDeactivate) {
|
||||||
// workaround https://github.com/Microsoft/TypeScript/issues/7271
|
// workaround https://github.com/Microsoft/TypeScript/issues/7271
|
||||||
const s2 = s as CanDeactivate;
|
const s2 = s as CanDeactivate;
|
||||||
|
@ -446,10 +445,10 @@ class PreActivation {
|
||||||
|
|
||||||
private traverseChildRoutes(
|
private traverseChildRoutes(
|
||||||
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
|
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
|
||||||
outletMap: RouterOutletMap): void {
|
outletMap: RouterOutletMap, futurePath: ActivatedRouteSnapshot[]): void {
|
||||||
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(currNode);
|
const prevChildren: {[key: string]: any} = nodeChildrenAsMap(currNode);
|
||||||
futureNode.children.forEach(c => {
|
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];
|
delete prevChildren[c.value.outlet];
|
||||||
});
|
});
|
||||||
forEach(
|
forEach(
|
||||||
|
@ -459,7 +458,7 @@ class PreActivation {
|
||||||
|
|
||||||
traverseRoutes(
|
traverseRoutes(
|
||||||
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
|
futureNode: TreeNode<ActivatedRouteSnapshot>, currNode: TreeNode<ActivatedRouteSnapshot>,
|
||||||
parentOutletMap: RouterOutletMap): void {
|
parentOutletMap: RouterOutletMap, futurePath: ActivatedRouteSnapshot[]): void {
|
||||||
const future = futureNode.value;
|
const future = futureNode.value;
|
||||||
const curr = currNode ? currNode.value : null;
|
const curr = currNode ? currNode.value : null;
|
||||||
const outlet = parentOutletMap ? parentOutletMap._outlets[futureNode.value.outlet] : null;
|
const outlet = parentOutletMap ? parentOutletMap._outlets[futureNode.value.outlet] : null;
|
||||||
|
@ -467,16 +466,17 @@ class PreActivation {
|
||||||
// reusing the node
|
// reusing the node
|
||||||
if (curr && future._routeConfig === curr._routeConfig) {
|
if (curr && future._routeConfig === curr._routeConfig) {
|
||||||
if (!shallowEqual(future.params, curr.params)) {
|
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 we have a component, we need to go through an outlet.
|
||||||
if (future.component) {
|
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.
|
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||||
} else {
|
} else {
|
||||||
this.traverseChildRoutes(futureNode, currNode, parentOutletMap);
|
this.traverseChildRoutes(futureNode, currNode, parentOutletMap, futurePath);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (curr) {
|
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 we have a component, we need to go through an outlet.
|
||||||
if (future.component) {
|
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.
|
// if we have a componentless route, we recurse but keep the same outlet map.
|
||||||
} else {
|
} else {
|
||||||
this.traverseChildRoutes(futureNode, null, parentOutletMap);
|
this.traverseChildRoutes(futureNode, null, parentOutletMap, futurePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -520,19 +520,44 @@ class PreActivation {
|
||||||
private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> {
|
private runCanActivate(future: ActivatedRouteSnapshot): Observable<boolean> {
|
||||||
const canActivate = future._routeConfig ? future._routeConfig.canActivate : null;
|
const canActivate = future._routeConfig ? future._routeConfig.canActivate : null;
|
||||||
if (!canActivate || canActivate.length === 0) return Observable.of(true);
|
if (!canActivate || canActivate.length === 0) return Observable.of(true);
|
||||||
return Observable.from(canActivate)
|
const obs = Observable.from(canActivate).map(c => {
|
||||||
.map(c => {
|
|
||||||
const guard = this.injector.get(c);
|
const guard = this.injector.get(c);
|
||||||
if (guard.canActivate) {
|
if (guard.canActivate) {
|
||||||
return wrapIntoObservable(guard.canActivate(future, this.future));
|
return wrapIntoObservable(guard.canActivate(future, this.future));
|
||||||
} else {
|
} else {
|
||||||
return wrapIntoObservable(guard(future, this.future));
|
return wrapIntoObservable(guard(future, this.future));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.mergeAll()
|
return andObservables(obs);
|
||||||
.every(result => result === true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
private runCanDeactivate(component: Object, curr: ActivatedRouteSnapshot): Observable<boolean> {
|
||||||
const canDeactivate = curr && curr._routeConfig ? curr._routeConfig.canDeactivate : null;
|
const canDeactivate = curr && curr._routeConfig ? curr._routeConfig.canDeactivate : null;
|
||||||
if (!canDeactivate || canDeactivate.length === 0) return Observable.of(true);
|
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 {
|
function pushQueryParamsAndFragment(state: RouterState): void {
|
||||||
if (!shallowEqual(state.snapshot.queryParams, (<any>state.queryParams).value)) {
|
if (!shallowEqual(state.snapshot.queryParams, (<any>state.queryParams).value)) {
|
||||||
(<any>state.queryParams).next(state.snapshot.queryParams);
|
(<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', () => {
|
describe('should work when returns an observable', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
addProviders([{
|
addProviders([{
|
||||||
|
|
Loading…
Reference in New Issue