feat: implement a simple version of the router service
This commit is contained in:
		
							parent
							
								
									0f79e504c9
								
							
						
					
					
						commit
						4f6ec01932
					
				
							
								
								
									
										7
									
								
								modules/@angular/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								modules/@angular/router/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| export { Router } from './src/router'; | ||||
| export { UrlSerializer, DefaultUrlSerializer } from './src/url_serializer'; | ||||
| export { RouterState, ActivatedRoute } from './src/router_state'; | ||||
| export { RouterOutletMap } from './src/router_outlet_map'; | ||||
| 
 | ||||
| import { RouterOutlet } from './src/directives/router_outlet'; | ||||
| export const ROUTER_DIRECTIVES = [RouterOutlet]; | ||||
| @ -1 +1,124 @@ | ||||
| export const X = 100; | ||||
| import { ComponentResolver, ReflectiveInjector } from '@angular/core'; | ||||
| import { Location } from '@angular/common'; | ||||
| import { UrlSerializer } from './url_serializer'; | ||||
| import { RouterOutletMap } from './router_outlet_map'; | ||||
| import { recognize } from './recognize'; | ||||
| import { rootNode, TreeNode } from './tree'; | ||||
| import { UrlTree } from './url_tree'; | ||||
| import { createEmptyState, RouterState, ActivatedRoute, PRIMARY_OUTLET } from './router_state'; | ||||
| import { RouterConfig } from './config'; | ||||
| import { RouterOutlet } from './directives/router_outlet'; | ||||
| import { forEach } from './util'; | ||||
| import { Subscription } from 'rxjs/Subscription'; | ||||
| 
 | ||||
| export class Router { | ||||
|   private currentState: RouterState; | ||||
|   private config: RouterConfig; | ||||
|   private locationSubscription: Subscription; | ||||
| 
 | ||||
|   constructor(private rootComponent:Object, private resolver: ComponentResolver, private urlSerializer: UrlSerializer, private outletMap: RouterOutletMap, private location: Location) { | ||||
|     this.currentState = createEmptyState(<any>rootComponent.constructor); | ||||
|     this.setUpLocationChangeListener(); | ||||
|     this.navigateByUrl(this.location.path()); | ||||
|   } | ||||
| 
 | ||||
|   navigateByUrl(url: string): void { | ||||
|     const urlTree = this.urlSerializer.parse(url); | ||||
|     this.navigate(urlTree, false); | ||||
|   } | ||||
| 
 | ||||
|   resetConfig(config: RouterConfig): void { | ||||
|     this.config = config; | ||||
|   } | ||||
| 
 | ||||
|   private setUpLocationChangeListener(): void { | ||||
|     this.locationSubscription = <any>this.location.subscribe((change) => { | ||||
|       this.navigate(this.urlSerializer.parse(change['url']), change['pop']) | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private navigate(url: UrlTree, pop?: boolean): Promise<void> { | ||||
|     return recognize(this.resolver, this.config, url, this.currentState).then(newState => { | ||||
|       new ActivateRoutes(newState, this.currentState).activate(this.outletMap); | ||||
|       this.currentState = newState; | ||||
|       if (!pop) { | ||||
|         this.location.go(this.urlSerializer.serialize(url)); | ||||
|       } | ||||
|     }).catch(e => console.log("error", e.message)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ActivateRoutes { | ||||
|   constructor(private futureState: RouterState, private currState: RouterState) {} | ||||
| 
 | ||||
|   activate(parentOutletMap: RouterOutletMap): void { | ||||
|     const currRoot = this.currState ? rootNode(this.currState) : null; | ||||
|     const futureRoot = rootNode(this.futureState); | ||||
|     this.activateChildRoutes(futureRoot, currRoot, parentOutletMap); | ||||
|   } | ||||
| 
 | ||||
|   private activateChildRoutes(futureNode: TreeNode<ActivatedRoute>, | ||||
|                               currNode: TreeNode<ActivatedRoute> | null, | ||||
|                               outletMap: RouterOutletMap): void { | ||||
|     const prevChildren = nodeChildrenAsMap(currNode); | ||||
|     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<ActivatedRoute>, currNode: TreeNode<ActivatedRoute>, | ||||
|                  parentOutletMap: RouterOutletMap): void { | ||||
|     const future = futureNode.value; | ||||
|     const curr = currNode ? currNode.value : null; | ||||
|     const outlet = getOutlet(parentOutletMap, futureNode.value); | ||||
| 
 | ||||
|     if (future === curr) { | ||||
|       this.activateChildRoutes(futureNode, currNode, outlet.outletMap); | ||||
|     } else { | ||||
|       this.deactivateOutletAndItChildren(outlet); | ||||
|       const outletMap = new RouterOutletMap(); | ||||
|       this.activateNewRoutes(outletMap, future, outlet); | ||||
|       this.activateChildRoutes(futureNode, currNode, outletMap); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private activateNewRoutes(outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void { | ||||
|     const resolved = ReflectiveInjector.resolve([ | ||||
|       {provide: ActivatedRoute, useValue: future}, | ||||
|       {provide: RouterOutletMap, useValue: outletMap} | ||||
|     ]); | ||||
|     outlet.activate(future.factory, resolved, outletMap); | ||||
|   } | ||||
| 
 | ||||
|   private deactivateOutletAndItChildren(outlet: RouterOutlet): void { | ||||
|     if (outlet && outlet.isActivated) { | ||||
|       forEach(outlet.outletMap._outlets, (v, k) => this.deactivateOutletAndItChildren(v)); | ||||
|       outlet.deactivate(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function nodeChildrenAsMap(node: TreeNode<ActivatedRoute>|null) { | ||||
|   return node ? | ||||
|     node.children.reduce( | ||||
|       (m, c) => { | ||||
|         m[c.value.outlet] = c; | ||||
|         return m; | ||||
|       }, | ||||
|       {}) : | ||||
|   {}; | ||||
| } | ||||
| 
 | ||||
| function getOutlet(outletMap: RouterOutletMap, route: ActivatedRoute): RouterOutlet { | ||||
|   let outlet = outletMap._outlets[route.outlet]; | ||||
|   if (!outlet) { | ||||
|     if (route.outlet === PRIMARY_OUTLET) { | ||||
|       throw new Error(`Cannot find primary outlet`); | ||||
|     } else { | ||||
|       throw new Error(`Cannot find the outlet ${route.outlet}`); | ||||
|     } | ||||
|   } | ||||
|   return outlet; | ||||
| } | ||||
|  | ||||
| @ -44,4 +44,12 @@ export function merge<V>(m1: {[key: string]: V}, m2: {[key: string]: V}): {[key: | ||||
|   } | ||||
| 
 | ||||
|   return m; | ||||
| } | ||||
| 
 | ||||
| export function forEach<K, V>(map: {[key: string]: V}, callback: /*(V, K) => void*/ Function): void { | ||||
|   for (var prop in map) { | ||||
|     if (map.hasOwnProperty(prop)) { | ||||
|       callback(map[prop], prop); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,4 +1,201 @@ | ||||
| import {Component} from '@angular/core'; | ||||
| import { | ||||
|   describe, | ||||
|   it, | ||||
|   iit, | ||||
|   xit, | ||||
|   expect, | ||||
|   beforeEach, | ||||
|   beforeEachProviders, | ||||
|   inject, | ||||
|   fakeAsync, | ||||
|   tick | ||||
| } from '@angular/core/testing'; | ||||
| 
 | ||||
| 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 '../index'; | ||||
| import { Observable } from 'rxjs/Observable'; | ||||
| import 'rxjs/add/operator/map'; | ||||
| 
 | ||||
| describe("Integration", () => { | ||||
|   it("test", () => { | ||||
|   }); | ||||
|   beforeEachProviders(() => [ | ||||
|     RouterOutletMap, | ||||
|     {provide: UrlSerializer, useClass: DefaultUrlSerializer}, | ||||
|     {provide: Location, useClass: SpyLocation}, | ||||
|     { | ||||
|       provide: Router, | ||||
|       useFactory: (resolver, urlSerializer, outletMap, location) => | ||||
|         new Router(new RootCmp(), resolver, urlSerializer, outletMap, location), | ||||
|       deps: [ComponentResolver, UrlSerializer, RouterOutletMap, Location] | ||||
|     } | ||||
|   ]); | ||||
|    | ||||
|   it('should update location when navigating', | ||||
|     fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { | ||||
|       router.resetConfig([ | ||||
|         { name: 'team', path: 'team/:id', component: TeamCmp } | ||||
|       ]); | ||||
|        | ||||
|       const fixture = tcb.createFakeAsync(RootCmp); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22'); | ||||
|       advance(fixture); | ||||
|       expect(location.path()).toEqual('/team/22'); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/33'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       expect(location.path()).toEqual('/team/33'); | ||||
|     }))); | ||||
| 
 | ||||
|   xit('should navigate back and forward', | ||||
|     fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { | ||||
|       router.resetConfig([ | ||||
|         { name: 'team', path: 'team/:id', component: TeamCmp, children: [ | ||||
|           { name: 'simple', path: 'simple', component: SimpleCmp }, | ||||
|           { name: 'user', path: 'user/:name', component: UserCmp } | ||||
|         ] } | ||||
|       ]); | ||||
| 
 | ||||
|       const fixture = tcb.createFakeAsync(RootCmp); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/33/simple'); | ||||
|       advance(fixture); | ||||
|       expect(location.path()).toEqual('/team/33/simple'); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22/user/victor'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       location.back(); | ||||
|       advance(fixture); | ||||
|       expect(location.path()).toEqual('/team/33/simple'); | ||||
| 
 | ||||
|       location.forward(); | ||||
|       advance(fixture); | ||||
|       expect(location.path()).toEqual('/team/22/user/victor'); | ||||
|     }))); | ||||
| 
 | ||||
|   it('should navigate when locations changes', | ||||
|     fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { | ||||
|       router.resetConfig([ | ||||
|         { name: 'team', path: 'team/:id', component: TeamCmp, children: [ | ||||
|           { name: 'user', path: 'user/:name', component: UserCmp } | ||||
|         ] } | ||||
|       ]); | ||||
| 
 | ||||
|       const fixture = tcb.createFakeAsync(RootCmp); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22/user/victor'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       location.simulateHashChange("/team/22/user/fedor"); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       expect(fixture.debugElement.nativeElement).toHaveText('team 22 { user fedor, right:  }'); | ||||
|     }))); | ||||
| 
 | ||||
|   it('should support secondary routes', | ||||
|     fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { | ||||
|       router.resetConfig([ | ||||
|         { name: 'team', path: 'team/:id', component: TeamCmp, children: [ | ||||
|           { name: 'user', path: 'user/:name', component: UserCmp }, | ||||
|           { name: 'simple', path: 'simple', component: SimpleCmp, outlet: 'right' } | ||||
|         ] } | ||||
|       ]); | ||||
| 
 | ||||
|       const fixture = tcb.createFakeAsync(RootCmp); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22/user/victor(simple)'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       expect(fixture.debugElement.nativeElement) | ||||
|         .toHaveText('team 22 { user victor, right: simple }'); | ||||
|     }))); | ||||
| 
 | ||||
|   it('should deactivate outlets', | ||||
|     fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { | ||||
|       router.resetConfig([ | ||||
|         { name: 'team', path: 'team/:id', component: TeamCmp, children: [ | ||||
|           { name: 'user', path: 'user/:name', component: UserCmp }, | ||||
|           { name: 'simple', path: 'simple', component: SimpleCmp, outlet: 'right' } | ||||
|         ] } | ||||
|       ]); | ||||
| 
 | ||||
|       const fixture = tcb.createFakeAsync(RootCmp); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22/user/victor(simple)'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22/user/victor'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       expect(fixture.debugElement.nativeElement).toHaveText('team 22 { user victor, right:  }'); | ||||
|     }))); | ||||
| 
 | ||||
|   it('should deactivate nested outlets', | ||||
|     fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { | ||||
|       router.resetConfig([ | ||||
|         { name: 'team', path: 'team/:id', component: TeamCmp, children: [ | ||||
|           { name: 'user', path: 'user/:name', component: UserCmp }, | ||||
|           { name: 'simple', path: 'simple', component: SimpleCmp, outlet: 'right' } | ||||
|         ] } | ||||
|       ]); | ||||
| 
 | ||||
|       const fixture = tcb.createFakeAsync(RootCmp); | ||||
| 
 | ||||
|       router.navigateByUrl('/team/22/user/victor(simple)'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       router.navigateByUrl('/'); | ||||
|       advance(fixture); | ||||
| 
 | ||||
|       expect(fixture.debugElement.nativeElement).toHaveText(''); | ||||
|     }))); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'simple-cmp', | ||||
|   template: `simple`, | ||||
|   directives: [ROUTER_DIRECTIVES] | ||||
| }) | ||||
| class SimpleCmp {} | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'team-cmp', | ||||
|   template: `team {{id | async}} { <router-outlet></router-outlet>, right: <router-outlet name="right"></router-outlet> }`, | ||||
|   directives: [ROUTER_DIRECTIVES] | ||||
| }) | ||||
| class TeamCmp { | ||||
|   id: Observable<string>; | ||||
| 
 | ||||
|   constructor(route: ActivatedRoute) { | ||||
|     this.id = route.params.map(p => p['id']); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'user-cmp', | ||||
|   template: `user {{name | async}}`, | ||||
|   directives: [ROUTER_DIRECTIVES] | ||||
| }) | ||||
| class UserCmp { | ||||
|   name: Observable<string>; | ||||
|   constructor(route: ActivatedRoute) { | ||||
|     this.name = route.params.map(p => p['name']); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'root-cmp', | ||||
|   template: `<router-outlet></router-outlet>`, | ||||
|   directives: [ROUTER_DIRECTIVES] | ||||
| }) | ||||
| class RootCmp {} | ||||
| 
 | ||||
| function advance(fixture: ComponentFixture<any>): void { | ||||
|   tick(); | ||||
|   fixture.detectChanges(); | ||||
| } | ||||
|  | ||||
| @ -21,3 +21,5 @@ | ||||
|     "typings/index.d.ts" | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user