diff --git a/modules/@angular/router/src/common_router_providers.ts b/modules/@angular/router/src/common_router_providers.ts index 04db250b04..54842fa465 100644 --- a/modules/@angular/router/src/common_router_providers.ts +++ b/modules/@angular/router/src/common_router_providers.ts @@ -3,7 +3,7 @@ import { UrlSerializer, DefaultUrlSerializer } from './url_serializer'; import { ActivatedRoute } from './router_state'; import { Router } from './router'; import { RouterConfig } from './config'; -import { ComponentResolver, ApplicationRef} from '@angular/core'; +import { ComponentResolver, ApplicationRef, Injector } from '@angular/core'; import { LocationStrategy, PathLocationStrategy, Location } from '@angular/common'; /** @@ -32,17 +32,17 @@ export function provideRouter(config: RouterConfig):any[] { { provide: Router, - useFactory: (ref, resolver, urlSerializer, outletMap, location) => { + useFactory: (ref, resolver, urlSerializer, outletMap, location, injector) => { if (ref.componentTypes.length == 0) { throw new Error("Bootstrap at least one component before injecting Router."); } const componentType = ref.componentTypes[0]; - const r = new Router(componentType, resolver, urlSerializer, outletMap, location); + const r = new Router(componentType, resolver, urlSerializer, outletMap, location, injector); r.resetConfig(config); ref.registerDisposeListener(() => r.dispose()); return r; }, - deps: [ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location] + deps: [ApplicationRef, ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector] }, RouterOutletMap, diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index dbc8bd6b68..41b603db0a 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -7,5 +7,6 @@ export interface Route { path?: string; component: Type | string; outlet?: string; + canActivate?: any[], children?: Route[]; } \ No newline at end of file diff --git a/modules/@angular/router/src/create_router_state.ts b/modules/@angular/router/src/create_router_state.ts index 10b686139a..8b8f40f444 100644 --- a/modules/@angular/router/src/create_router_state.ts +++ b/modules/@angular/router/src/create_router_state.ts @@ -2,17 +2,19 @@ import { RouterStateCandidate, ActivatedRouteCandidate, RouterState, ActivatedRo import { TreeNode } from './utils/tree'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -export function createRouterState(curr: RouterStateCandidate, prev: RouterStateCandidate, prevState: RouterState): RouterState { - const root = createNode(curr._root, prev ? prev._root : null, prevState ? prevState._root : null); +export function createRouterState(curr: RouterStateCandidate, prevState: RouterState): RouterState { + const root = createNode(curr._root, prevState ? prevState._root : null); const queryParams = prevState ? prevState.queryParams : new BehaviorSubject(curr.queryParams); const fragment = prevState ? prevState.fragment : new BehaviorSubject(curr.fragment); - return new RouterState(root, queryParams, fragment); + return new RouterState(root, queryParams, fragment, curr); } -function createNode(curr:TreeNode, prev?:TreeNode, prevState?:TreeNode):TreeNode { - if (prev && equalRouteCandidates(prev.value, curr.value)) { +function createNode(curr:TreeNode, prevState?:TreeNode):TreeNode { + if (prevState && equalRouteCandidates(prevState.value.candidate, curr.value)) { const value = prevState.value; - const children = createOrReuseChildren(curr, prev, prevState); + value.candidate = curr.value; + + const children = createOrReuseChildren(curr, prevState); return new TreeNode(value, children); } else { @@ -22,11 +24,11 @@ function createNode(curr:TreeNode, prev?:TreeNode, prev:TreeNode, prevState:TreeNode) { +function createOrReuseChildren(curr:TreeNode, prevState:TreeNode) { return curr.children.map(child => { - const index = prev.children.findIndex(p => equalRouteCandidates(p.value, child.value)); + const index = prevState.children.findIndex(p => equalRouteCandidates(p.value.candidate, child.value)); if (index >= 0) { - return createNode(child, prev.children[index], prevState.children[index]); + return createNode(child, prevState.children[index]); } else { return createNode(child); } @@ -34,7 +36,7 @@ function createOrReuseChildren(curr:TreeNode, prev:Tree } function createActivatedRoute(c:ActivatedRouteCandidate) { - return new ActivatedRoute(new BehaviorSubject(c.urlSegments), new BehaviorSubject(c.params), c.outlet, c.component); + return new ActivatedRoute(new BehaviorSubject(c.urlSegments), new BehaviorSubject(c.params), c.outlet, c.component, c); } function equalRouteCandidates(a: ActivatedRouteCandidate, b: ActivatedRouteCandidate): boolean { diff --git a/modules/@angular/router/src/directives/router_outlet.ts b/modules/@angular/router/src/directives/router_outlet.ts index bdc8044b91..9ee76c1122 100644 --- a/modules/@angular/router/src/directives/router_outlet.ts +++ b/modules/@angular/router/src/directives/router_outlet.ts @@ -1,4 +1,4 @@ -import {Directive, ViewContainerRef, Attribute, ComponentRef, ComponentFactory, ResolvedReflectiveProvider, ReflectiveInjector} from '@angular/core'; +import {Injector, Directive, ViewContainerRef, Attribute, ComponentRef, ComponentFactory, ResolvedReflectiveProvider, ReflectiveInjector} from '@angular/core'; import {RouterOutletMap} from '../router_outlet_map'; import {PRIMARY_OUTLET} from '../shared'; @@ -8,7 +8,7 @@ export class RouterOutlet { public outletMap:RouterOutletMap; constructor(parentOutletMap:RouterOutletMap, private location:ViewContainerRef, - @Attribute('name') name:string) { + @Attribute('name') name:string, public injector: Injector) { parentOutletMap.registerOutlet(name ? name : PRIMARY_OUTLET, this); } diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index ecd04e27b8..cad1346bcc 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -1,4 +1,4 @@ -import { ComponentResolver, ReflectiveInjector, Type } from '@angular/core'; +import { ComponentResolver, ReflectiveInjector, Type, Injector } from '@angular/core'; import { Location } from '@angular/common'; import { UrlSerializer } from './url_serializer'; import { RouterOutletMap } from './router_outlet_map'; @@ -8,15 +8,18 @@ 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, createEmptyStateCandidate, RouterState, RouterStateCandidate, ActivatedRoute, ActivatedRouteCandidate} from './router_state'; +import { createEmptyState, RouterState, RouterStateCandidate, ActivatedRoute, ActivatedRouteCandidate} from './router_state'; import { RouterConfig } from './config'; import { RouterOutlet } from './directives/router_outlet'; import { createUrlTree } from './create_url_tree'; -import { forEach } from './utils/collection'; +import { forEach, and, shallowEqual } from './utils/collection'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/scan'; import 'rxjs/add/operator/mergeMap'; +import {of} from 'rxjs/observable/of'; +import {forkJoin} from 'rxjs/observable/forkJoin'; export interface NavigationExtras { relativeTo?: ActivatedRoute; queryParameters?: Params; fragment?: string; } @@ -26,17 +29,15 @@ export interface NavigationExtras { relativeTo?: ActivatedRoute; queryParameters export class Router { private currentUrlTree: UrlTree; private currentRouterState: RouterState; - private currentRouterStateCandidate: RouterStateCandidate; private config: RouterConfig; private locationSubscription: Subscription; /** * @internal */ - constructor(private rootComponentType:Type, private resolver: ComponentResolver, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private location: Location) { + constructor(private rootComponentType:Type, private resolver: ComponentResolver, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private location: Location, private injector: Injector) { this.currentUrlTree = createEmptyUrlTree(); this.currentRouterState = createEmptyState(rootComponentType); - this.currentRouterStateCandidate = createEmptyStateCandidate(rootComponentType); this.setUpLocationChangeListener(); this.navigateByUrl(this.location.path()); } @@ -159,26 +160,27 @@ export class Router { }); } - private runNavigate(url:UrlTree, pop?:boolean):Observable { - let candidate; + private runNavigate(url:UrlTree, pop?:boolean):Observable { let state; const r = recognize(this.rootComponentType, this.config, url).mergeMap((newRouterStateCandidate) => { return resolve(this.resolver, newRouterStateCandidate); }).map((routerStateCandidate) => { - candidate = routerStateCandidate; - return createRouterState(routerStateCandidate, this.currentRouterStateCandidate, this.currentRouterState); + return createRouterState(routerStateCandidate, this.currentRouterState); }).map((newState:RouterState) => { state = newState; + + }).mergeMap(_ => { + return new GuardChecks(state.candidate, this.currentRouterState.candidate, this.injector).check(this.outletMap); }); - r.subscribe((_) => { - new ActivateRoutes(state, this.currentRouterState, candidate).activate(this.outletMap); + r.subscribe((shouldActivate) => { + if (!shouldActivate) return; + new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap); this.currentUrlTree = url; this.currentRouterState = state; - this.currentRouterStateCandidate = candidate; if (!pop) { this.location.go(this.urlSerializer.serialize(url)); @@ -188,59 +190,104 @@ export class Router { } } +class GuardChecks { + private checks = []; + constructor(private future: RouterStateCandidate, private curr: RouterStateCandidate, private injector: Injector) {} + + check(parentOutletMap: RouterOutletMap): Observable { + const futureRoot = this.future._root; + 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); + } + + private traverseChildRoutes(futureNode: TreeNode, + currNode: TreeNode | null, + outletMap: RouterOutletMap): 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])); + } + + traverseRoutes(futureNode: TreeNode, currNode: TreeNode | null, + parentOutletMap: RouterOutletMap): void { + const future = futureNode.value; + const curr = currNode ? currNode.value : null; + + if (future === curr) { + if (!shallowEqual(future.params, curr.params)) { + this.checks.push(future); + } + this.traverseChildRoutes(futureNode, currNode, null); + } else { + this.deactivateOutletAndItChildren(null); + this.checks.push(future); + this.traverseChildRoutes(futureNode, null, null); + } + } + + private deactivateOutletAndItChildren(outlet: RouterOutlet): void {} + + private runCanActivate(future: ActivatedRouteCandidate): Observable { + const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; + if (!canActivate || canActivate.length === 0) return of(true); + return forkJoin(canActivate.map(c => { + const guard = this.injector.get(c); + return of(guard(future, this.future)); + })).map(and); + } +} + class ActivateRoutes { - constructor(private futureState: RouterState, private currState: RouterState, - private futureStateCandidate: RouterStateCandidate) {} + constructor(private futureState: RouterState, private currState: RouterState) {} activate(parentOutletMap: RouterOutletMap):void { const futureRoot = this.futureState._root; const currRoot = this.currState ? this.currState._root : null; - const futureCandidateRoot = this.futureStateCandidate._root; - this.activateChildRoutes(futureRoot, currRoot, futureCandidateRoot, parentOutletMap); + + pushQueryParamsAndFragment(this.futureState); + this.activateChildRoutes(futureRoot, currRoot, parentOutletMap); } private activateChildRoutes(futureNode: TreeNode, currNode: TreeNode | null, - futureCandidateNode: TreeNode, outletMap: RouterOutletMap): void { const prevChildren = nodeChildrenAsMap(currNode); - for (let i = 0; i < futureNode.children.length; ++i) { - const c = futureNode.children[i]; - const cc = futureCandidateNode.children[i]; - this.activateRoutes(c, prevChildren[c.value.outlet], cc, outletMap); + futureNode.children.forEach(c => { + this.activateRoutes(c, prevChildren[c.value.outlet], outletMap); delete prevChildren[c.value.outlet]; - } + }); forEach(prevChildren, (v, k) => this.deactivateOutletAndItChildren(outletMap._outlets[k])); } - - activateRoutes(futureNode: TreeNode, currNode: TreeNode, - futureCandidateNode: TreeNode, + activateRoutes(futureNode: TreeNode, currNode: TreeNode | null, parentOutletMap: RouterOutletMap): void { const future = futureNode.value; - const futureCandidate = futureCandidateNode.value; const curr = currNode ? currNode.value : null; const outlet = getOutlet(parentOutletMap, futureNode.value); if (future === curr) { - pushValues(future, futureCandidate); - this.activateChildRoutes(futureNode, currNode, futureCandidateNode, outlet.outletMap); + pushValues(future); + this.activateChildRoutes(futureNode, currNode, outlet.outletMap); } else { this.deactivateOutletAndItChildren(outlet); const outletMap = new RouterOutletMap(); - this.activateNewRoutes(outletMap, future, futureCandidate, outlet); - this.activateChildRoutes(futureNode, currNode, futureCandidateNode, outletMap); + this.activateNewRoutes(outletMap, future, outlet); + this.activateChildRoutes(futureNode, null, outletMap); } } - private activateNewRoutes(outletMap: RouterOutletMap, future: ActivatedRoute, - futureCandidate: ActivatedRouteCandidate, outlet: RouterOutlet): void { + private activateNewRoutes(outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void { const resolved = ReflectiveInjector.resolve([ {provide: ActivatedRoute, useValue: future}, {provide: RouterOutletMap, useValue: outletMap} ]); - outlet.activate(futureCandidate._resolvedComponentFactory, resolved, outletMap); - pushValues(future, futureCandidate); + outlet.activate(future.candidate._resolvedComponentFactory, resolved, outletMap); + pushValues(future); } private deactivateOutletAndItChildren(outlet: RouterOutlet): void { @@ -251,12 +298,24 @@ class ActivateRoutes { } } -function pushValues(route: ActivatedRoute, candidate: ActivatedRouteCandidate): void { - (route.urlSegments).next(candidate.urlSegments); - (route.params).next(candidate.params); +function pushValues(route: ActivatedRoute): void { + if (!shallowEqual(route.candidate.params, (route.params).value)) { + (route.urlSegments).next(route.candidate.urlSegments); + (route.params).next(route.candidate.params); + } } -function nodeChildrenAsMap(node: TreeNode|null) { +function pushQueryParamsAndFragment(state: RouterState): void { + if (!shallowEqual(state.candidate.queryParams, (state.queryParams).value)) { + (state.queryParams).next(state.candidate.queryParams); + } + + if (state.candidate.fragment !== (state.fragment).value) { + (state.fragment).next(state.candidate.fragment); + } +} + +function nodeChildrenAsMap(node: TreeNode|null) { return node ? node.children.reduce( (m, c) => { diff --git a/modules/@angular/router/src/router_state.ts b/modules/@angular/router/src/router_state.ts index 1f504ad075..702b3e5efb 100644 --- a/modules/@angular/router/src/router_state.ts +++ b/modules/@angular/router/src/router_state.ts @@ -22,21 +22,22 @@ import { Type, ComponentFactory } from '@angular/core'; * ``` */ export class RouterState extends Tree { - constructor(root: TreeNode, public queryParams: Observable, public fragment: Observable) { + constructor(root: TreeNode, public queryParams: Observable, public fragment: Observable, public candidate: RouterStateCandidate) { super(root); } } export function createEmptyState(rootComponent: Type): RouterState { + const candidate = createEmptyStateCandidate(rootComponent); const emptyUrl = new BehaviorSubject([new UrlSegment("", {}, PRIMARY_OUTLET)]); const emptyParams = new BehaviorSubject({}); const emptyQueryParams = new BehaviorSubject({}); const fragment = new BehaviorSubject(""); - const activated = new ActivatedRoute(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent); - return new RouterState(new TreeNode(activated, []), emptyQueryParams, fragment); + const activated = new ActivatedRoute(emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, candidate.root); + return new RouterState(new TreeNode(activated, []), emptyQueryParams, fragment, candidate); } -export function createEmptyStateCandidate(rootComponent: Type): RouterStateCandidate { +function createEmptyStateCandidate(rootComponent: Type): RouterStateCandidate { const emptyUrl = [new UrlSegment("", {}, PRIMARY_OUTLET)]; const emptyParams = {}; const emptyQueryParams = {}; @@ -62,7 +63,9 @@ export class ActivatedRoute { constructor(public urlSegments: Observable, public params: Observable, public outlet: string, - public component: Type | string) {} + public component: Type | string, + public candidate: ActivatedRouteCandidate + ) {} } export class ActivatedRouteCandidate { diff --git a/modules/@angular/router/src/utils/collection.ts b/modules/@angular/router/src/utils/collection.ts index 5816b02db4..9a96714a79 100644 --- a/modules/@angular/router/src/utils/collection.ts +++ b/modules/@angular/router/src/utils/collection.ts @@ -28,6 +28,10 @@ export function first(a: T[]): T | null { return a.length > 0 ? a[0] : null; } +export function and(bools: boolean[]): boolean { + return bools.reduce((a,b) => a && b, true); +} + export function merge(m1: {[key: string]: V}, m2: {[key: string]: V}): {[key: string]: V} { var m: {[key: string]: V} = {}; diff --git a/modules/@angular/router/test/create_router_state.spec.ts b/modules/@angular/router/test/create_router_state.spec.ts index 556b7877e8..0ba2030598 100644 --- a/modules/@angular/router/test/create_router_state.spec.ts +++ b/modules/@angular/router/test/create_router_state.spec.ts @@ -1,13 +1,12 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; import {UrlTree} from '../src/url_tree'; import {Params, PRIMARY_OUTLET} from '../src/shared'; -import {ActivatedRoute, ActivatedRouteCandidate, RouterStateCandidate, createEmptyStateCandidate, createEmptyState} from '../src/router_state'; +import {ActivatedRoute, ActivatedRouteCandidate, RouterStateCandidate, createEmptyState} from '../src/router_state'; import {createRouterState} from '../src/create_router_state'; import {recognize} from '../src/recognize'; import {RouterConfig} from '../src/config'; describe('create router state', () => { - const emptyCandidate = () => createEmptyStateCandidate(RootComponent); const emptyState = () => createEmptyState(RootComponent); it('should work create new state', () => { @@ -15,7 +14,7 @@ describe('create router state', () => { {path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'}, {path: 'c', component: ComponentC, outlet: 'right'} - ], "a(left:b//right:c)"), emptyCandidate(), emptyState()); + ], "a(left:b//right:c)"), emptyState()); checkActivatedRoute(state.root, RootComponent); @@ -32,10 +31,8 @@ describe('create router state', () => { {path: 'c', component: ComponentC, outlet: 'left'} ]; - const prevCandidate = createState(config, "a(left:b)"); - const prevState = createRouterState(prevCandidate, emptyCandidate(), emptyState()); - - const state = createRouterState(createState(config, "a(left:c)"), prevCandidate, prevState); + const prevState = createRouterState(createState(config, "a(left:b)"), emptyState()); + const state = createRouterState(createState(config, "a(left:c)"), prevState); expect(prevState.root).toBe(state.root); const prevC = prevState.children(prevState.root); diff --git a/modules/@angular/router/test/create_url_tree.spec.ts b/modules/@angular/router/test/create_url_tree.spec.ts index ae9d96b3a8..a680e53c4b 100644 --- a/modules/@angular/router/test/create_url_tree.spec.ts +++ b/modules/@angular/router/test/create_url_tree.spec.ts @@ -175,6 +175,6 @@ function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryP if (!start) { expect(start).toBeDefined(); } - const a = new ActivatedRoute(new BehaviorSubject([start]), null, PRIMARY_OUTLET, "someComponent"); + const a = new ActivatedRoute(new BehaviorSubject([start]), null, PRIMARY_OUTLET, "someComponent", null); return createUrlTree(a, tree, commands, queryParameters, fragment); } \ No newline at end of file diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 5783c36653..76a63c97e4 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {Component, Injector} from '@angular/core'; import { describe, it, @@ -15,7 +15,7 @@ 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 } from '../src/index'; +import { UrlSerializer, DefaultUrlSerializer, RouterOutletMap, Router, ActivatedRoute, ROUTER_DIRECTIVES, Params } from '../src/index'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; @@ -26,13 +26,14 @@ describe("Integration", () => { {provide: Location, useClass: SpyLocation}, { provide: Router, - useFactory: (resolver, urlSerializer, outletMap, location) => - new Router(RootCmp, resolver, urlSerializer, outletMap, location), - deps: [ComponentResolver, UrlSerializer, RouterOutletMap, Location] + useFactory: (resolver, urlSerializer, outletMap, location, injector) => + new Router(RootCmp, resolver, urlSerializer, outletMap, location, injector), + deps: [ComponentResolver, UrlSerializer, RouterOutletMap, Location, Injector] }, {provide: ActivatedRoute, useFactory: (r) => r.routerState.root, deps: [Router]}, ]); - + + it('should update location when navigating', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { router.resetConfig([ @@ -156,6 +157,48 @@ describe("Integration", () => { expect(fixture.debugElement.nativeElement).toHaveText(''); }))); + it('should set query params and fragment', + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + router.resetConfig([ + { path: 'query', component: QueryParamsAndFragmentCmp } + ]); + + const fixture = tcb.createFakeAsync(RootCmp); + + router.navigateByUrl('/query?name=1#fragment1'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('query: 1 fragment: fragment1'); + + router.navigateByUrl('/query?name=2#fragment2'); + advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('query: 2 fragment: fragment2'); + }))); + + it('should push params only when they change', + fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => { + router.resetConfig([ + { path: 'team/:id', component: TeamCmp, children: [ + { path: 'user/:name', component: UserCmp } + ] } + ]); + + const fixture = tcb.createFakeAsync(RootCmp); + + router.navigateByUrl('/team/22/user/victor'); + advance(fixture); + const team = fixture.debugElement.children[1].componentInstance; + const user = fixture.debugElement.children[1].children[1].componentInstance; + + expect(team.recordedParams).toEqual([{id: '22'}]); + expect(user.recordedParams).toEqual([{name: 'victor'}]); + + router.navigateByUrl('/team/22/user/fedor'); + advance(fixture); + + expect(team.recordedParams).toEqual([{id: '22'}]); + expect(user.recordedParams).toEqual([{name: 'victor'}, {name: 'fedor'}]); + }))); + describe("router links", () => { it("should support string router links", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { @@ -238,6 +281,52 @@ describe("Integration", () => { expect(fixture.debugElement.nativeElement).toHaveText('link'); }))); }); + + describe("guards", () => { + describe("CanActivate", () => { + describe("should not activate a route when CanActivate returns false", () => { + beforeEachProviders(() => [ + {provide: 'alwaysFalse', useValue: (a, b) => false} + ]); + + it('works', + fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + router.resetConfig([ + { path: 'team/:id', component: TeamCmp, canActivate: ["alwaysFalse"] } + ]); + + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual(''); + }))); + }); + + describe("should activate a route when CanActivate returns true", () => { + beforeEachProviders(() => [ + {provide: 'alwaysFalse', useValue: (a, b) => true} + ]); + + it('works', + fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + router.resetConfig([ + { path: 'team/:id', component: TeamCmp, canActivate: ["alwaysFalse"] } + ]); + + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + + router.navigateByUrl('/team/22'); + advance(fixture); + + expect(location.path()).toEqual('/team/22'); + }))); + }); + }); + }); }); @Component({ @@ -276,9 +365,11 @@ class SimpleCmp { }) class TeamCmp { id: Observable; + recordedParams: Params[] = []; constructor(route: ActivatedRoute) { this.id = route.params.map(p => p['id']); + route.params.forEach(_ => this.recordedParams.push(_)); } } @@ -289,8 +380,26 @@ class TeamCmp { }) class UserCmp { name: Observable; + recordedParams: Params[] = []; + constructor(route: ActivatedRoute) { this.name = route.params.map(p => p['name']); + route.params.forEach(_ => this.recordedParams.push(_)); + } +} + +@Component({ + selector: 'query-cmp', + template: `query: {{name | async}} fragment: {{fragment | async}}`, + directives: [ROUTER_DIRECTIVES] +}) +class QueryParamsAndFragmentCmp { + name: Observable; + fragment: Observable; + + constructor(router: Router) { + this.name = router.routerState.queryParams.map(p => p['name']); + this.fragment = router.routerState.fragment; } }