From 5a897cf29989fae9cf2a720655b193076ca3a95f Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 22 Apr 2016 12:05:38 -0700 Subject: [PATCH] feat(router): add Router and RouterOutlet Closes #8173 --- modules/angular2/alt_router.ts | 17 +++ .../alt_router/directives/router_outlet.ts | 34 ++++++ modules/angular2/src/alt_router/interfaces.ts | 6 + .../src/alt_router/lifecycle_reflector.dart | 5 + .../src/alt_router/lifecycle_reflector.ts | 7 ++ modules/angular2/src/alt_router/router.ts | 55 +++++++++ .../test/alt_router/integration_spec.ts | 106 ++++++++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 modules/angular2/alt_router.ts create mode 100644 modules/angular2/src/alt_router/directives/router_outlet.ts create mode 100644 modules/angular2/src/alt_router/interfaces.ts create mode 100644 modules/angular2/src/alt_router/lifecycle_reflector.dart create mode 100644 modules/angular2/src/alt_router/lifecycle_reflector.ts create mode 100644 modules/angular2/src/alt_router/router.ts create mode 100644 modules/angular2/test/alt_router/integration_spec.ts diff --git a/modules/angular2/alt_router.ts b/modules/angular2/alt_router.ts new file mode 100644 index 0000000000..f31900532b --- /dev/null +++ b/modules/angular2/alt_router.ts @@ -0,0 +1,17 @@ +/** + * @module + * @description + * Alternative implementation of the router. Experimental. + */ + +export {Router, RouterOutletMap} from './src/alt_router/router'; +export {RouteSegment} from './src/alt_router/segments'; +export {Routes} from './src/alt_router/metadata/decorators'; +export {Route} from './src/alt_router/metadata/metadata'; +export {RouterUrlParser, DefaultRouterUrlParser} from './src/alt_router/router_url_parser'; +export {OnActivate} from './src/alt_router/interfaces'; + +import {RouterOutlet} from './src/alt_router/directives/router_outlet'; +import {CONST_EXPR} from './src/facade/lang'; + +export const ROUTER_DIRECTIVES: any[] = CONST_EXPR([RouterOutlet]); diff --git a/modules/angular2/src/alt_router/directives/router_outlet.ts b/modules/angular2/src/alt_router/directives/router_outlet.ts new file mode 100644 index 0000000000..7dcb149a0b --- /dev/null +++ b/modules/angular2/src/alt_router/directives/router_outlet.ts @@ -0,0 +1,34 @@ +import { + ResolvedReflectiveProvider, + Directive, + DynamicComponentLoader, + ViewContainerRef, + Input, + ComponentRef, + ComponentFactory, + ReflectiveInjector +} from 'angular2/core'; +import {RouterOutletMap} from '../router'; +import {isPresent} from 'angular2/src/facade/lang'; + +@Directive({selector: 'router-outlet'}) +export class RouterOutlet { + private _loaded: ComponentRef; + public outletMap: RouterOutletMap; + @Input() name: string = ""; + + constructor(parentOutletMap: RouterOutletMap, private _location: ViewContainerRef) { + parentOutletMap.registerOutlet("", this); + } + + load(factory: ComponentFactory, providers: ResolvedReflectiveProvider[], + outletMap: RouterOutletMap): ComponentRef { + if (isPresent(this._loaded)) { + this._loaded.destroy(); + } + this.outletMap = outletMap; + let inj = ReflectiveInjector.fromResolvedProviders(providers, this._location.parentInjector); + this._loaded = this._location.createComponent(factory, this._location.length, inj, []); + return this._loaded; + } +} \ No newline at end of file diff --git a/modules/angular2/src/alt_router/interfaces.ts b/modules/angular2/src/alt_router/interfaces.ts new file mode 100644 index 0000000000..136e8a0034 --- /dev/null +++ b/modules/angular2/src/alt_router/interfaces.ts @@ -0,0 +1,6 @@ +import {RouteSegment, Tree} from './segments'; + +export interface OnActivate { + routerOnActivate(curr: RouteSegment, prev?: RouteSegment, currTree?: Tree, + prevTree?: Tree): void; +} \ No newline at end of file diff --git a/modules/angular2/src/alt_router/lifecycle_reflector.dart b/modules/angular2/src/alt_router/lifecycle_reflector.dart new file mode 100644 index 0000000000..2a5546def5 --- /dev/null +++ b/modules/angular2/src/alt_router/lifecycle_reflector.dart @@ -0,0 +1,5 @@ +import './interfaces.dart'; +bool hasLifecycleHook(String name, Object obj) { + if (name == "routerOnActivate") return obj is OnActivate; + return false; +} diff --git a/modules/angular2/src/alt_router/lifecycle_reflector.ts b/modules/angular2/src/alt_router/lifecycle_reflector.ts new file mode 100644 index 0000000000..7d50894692 --- /dev/null +++ b/modules/angular2/src/alt_router/lifecycle_reflector.ts @@ -0,0 +1,7 @@ +import {Type} from 'angular2/src/facade/lang'; + +export function hasLifecycleHook(name: string, obj: Object): boolean { + let type = obj.constructor; + if (!(type instanceof Type)) return false; + return name in(type).prototype; +} diff --git a/modules/angular2/src/alt_router/router.ts b/modules/angular2/src/alt_router/router.ts new file mode 100644 index 0000000000..bd9bf184ac --- /dev/null +++ b/modules/angular2/src/alt_router/router.ts @@ -0,0 +1,55 @@ +import {provide, ReflectiveInjector, ComponentResolver} from 'angular2/core'; +import {RouterOutlet} from './directives/router_outlet'; +import {Type, isBlank, isPresent} from 'angular2/src/facade/lang'; +import {RouterUrlParser} from './router_url_parser'; +import {recognize} from './recognize'; +import {equalSegments, routeSegmentComponentFactory, RouteSegment, Tree} from './segments'; +import {hasLifecycleHook} from './lifecycle_reflector'; + +export class RouterOutletMap { + /** @internal */ + _outlets: {[name: string]: RouterOutlet} = {}; + registerOutlet(name: string, outlet: RouterOutlet): void { this._outlets[name] = outlet; } +} + +export class Router { + private prevTree: Tree; + constructor(private _componentType: Type, private _componentResolver: ComponentResolver, + private _urlParser: RouterUrlParser, private _routerOutletMap: RouterOutletMap) {} + + navigateByUrl(url: string): Promise { + let urlSegmentTree = this._urlParser.parse(url.substring(1)); + return recognize(this._componentResolver, this._componentType, urlSegmentTree) + .then(currTree => { + let prevRoot = isPresent(this.prevTree) ? this.prevTree.root : null; + _loadSegments(currTree, currTree.root, this.prevTree, prevRoot, this, + this._routerOutletMap); + this.prevTree = currTree; + }); + } +} + +function _loadSegments(currTree: Tree, curr: RouteSegment, + prevTree: Tree, prev: RouteSegment, router: Router, + parentOutletMap: RouterOutletMap): void { + let outlet = parentOutletMap._outlets[curr.outlet]; + + let outletMap; + if (equalSegments(curr, prev)) { + outletMap = outlet.outletMap; + } else { + outletMap = new RouterOutletMap(); + let resolved = ReflectiveInjector.resolve( + [provide(RouterOutletMap, {useValue: outletMap}), provide(RouteSegment, {useValue: curr})]); + let ref = outlet.load(routeSegmentComponentFactory(curr), resolved, outletMap); + if (hasLifecycleHook("routerOnActivate", ref.instance)) { + ref.instance.routerOnActivate(curr, prev, currTree, prevTree); + } + } + + if (isPresent(currTree.firstChild(curr))) { + let cc = currTree.firstChild(curr); + let pc = isBlank(prevTree) ? null : prevTree.firstChild(prev); + _loadSegments(currTree, cc, prevTree, pc, router, outletMap); + } +} \ No newline at end of file diff --git a/modules/angular2/test/alt_router/integration_spec.ts b/modules/angular2/test/alt_router/integration_spec.ts new file mode 100644 index 0000000000..41ae480905 --- /dev/null +++ b/modules/angular2/test/alt_router/integration_spec.ts @@ -0,0 +1,106 @@ +import { + ComponentFixture, + AsyncTestCompleter, + TestComponentBuilder, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + expect, + iit, + inject, + beforeEachProviders, + it, + xit +} from 'angular2/testing_internal'; +import {provide, Component, ComponentResolver} from 'angular2/core'; + +import { + Router, + RouterOutletMap, + RouteSegment, + Route, + ROUTER_DIRECTIVES, + Routes, + RouterUrlParser, + DefaultRouterUrlParser, + OnActivate +} from 'angular2/alt_router'; + +export function main() { + describe('navigation', () => { + beforeEachProviders(() => [ + provide(RouterUrlParser, {useClass: DefaultRouterUrlParser}), + RouterOutletMap, + provide(Router, + { + useFactory: (resolver, urlParser, outletMap) => + new Router(RootCmp, resolver, urlParser, outletMap), + deps: [ComponentResolver, RouterUrlParser, RouterOutletMap] + }) + ]); + + it('should support nested routes', + inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { + let fixture; + compileRoot(tcb) + .then((rtc) => {fixture = rtc}) + .then((_) => router.navigateByUrl('/team/22/user/victor')) + .then((_) => { + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello victor }'); + async.done(); + }); + })); + + it('should update nested routes when url changes', + inject([AsyncTestCompleter, Router, TestComponentBuilder], (async, router, tcb) => { + let fixture; + let team1; + let team2; + compileRoot(tcb) + .then((rtc) => {fixture = rtc}) + .then((_) => router.navigateByUrl('/team/22/user/victor')) + .then((_) => { team1 = fixture.debugElement.children[1].componentInstance; }) + .then((_) => router.navigateByUrl('/team/22/user/fedor')) + .then((_) => { team2 = fixture.debugElement.children[1].componentInstance; }) + .then((_) => { + fixture.detectChanges(); + expect(team1).toBe(team2); + expect(fixture.debugElement.nativeElement).toHaveText('team 22 { hello fedor }'); + async.done(); + }); + })); + }); +} + +function compileRoot(tcb: TestComponentBuilder): Promise { + return tcb.createAsync(RootCmp); +} + +@Component({selector: 'user-cmp', template: `hello {{user}}`}) +class UserCmp implements OnActivate { + user: string; + routerOnActivate(s: RouteSegment, a?, b?, c?) { this.user = s.getParam('name'); } +} + +@Component({ + selector: 'team-cmp', + template: `team {{id}} { }`, + directives: [ROUTER_DIRECTIVES] +}) +@Routes([new Route({path: 'user/:name', component: UserCmp})]) +class TeamCmp implements OnActivate { + id: string; + routerOnActivate(s: RouteSegment, a?, b?, c?) { this.id = s.getParam('id'); } +} + +@Component({ + selector: 'root-cmp', + template: ``, + directives: [ROUTER_DIRECTIVES] +}) +@Routes([new Route({path: 'team/:id', component: TeamCmp})]) +class RootCmp { +}