From ef6163e652aef79f804a43c90c8b73561ed588b7 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Fri, 22 Apr 2016 12:04:56 -0700 Subject: [PATCH] feat(router): implement recognizer --- modules/angular2/src/alt_router/recognize.ts | 84 +++++++++++++++++ .../test/alt_router/recognize_spec.ts | 89 +++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 modules/angular2/src/alt_router/recognize.ts create mode 100644 modules/angular2/test/alt_router/recognize_spec.ts diff --git a/modules/angular2/src/alt_router/recognize.ts b/modules/angular2/src/alt_router/recognize.ts new file mode 100644 index 0000000000..7c1b79b070 --- /dev/null +++ b/modules/angular2/src/alt_router/recognize.ts @@ -0,0 +1,84 @@ +import {RouteSegment, UrlSegment, Tree} from './segments'; +import {RoutesMetadata, RouteMetadata} from './metadata/metadata'; +import {Type, isPresent, stringify} from 'angular2/src/facade/lang'; +import {PromiseWrapper} from 'angular2/src/facade/promise'; +import {BaseException} from 'angular2/src/facade/exceptions'; +import {ComponentResolver} from 'angular2/core'; +import {reflector} from 'angular2/src/core/reflection/reflection'; + +export function recognize(componentResolver: ComponentResolver, type: Type, + url: Tree): Promise> { + return _recognize(componentResolver, type, url, url.root) + .then(nodes => new Tree(nodes)); +} + +function _recognize(componentResolver: ComponentResolver, type: Type, url: Tree, + current: UrlSegment): Promise { + let metadata = _readMetadata(type); // should read from the factory instead + + let matched; + try { + matched = _match(metadata, url, current); + } catch (e) { + return PromiseWrapper.reject(e, null); + } + + return componentResolver.resolveComponent(matched.route.component) + .then(factory => { + let segment = new RouteSegment(matched.consumedUrlSegments, matched.parameters, "", + matched.route.component, factory); + + if (isPresent(matched.leftOver)) { + return _recognize(componentResolver, matched.route.component, url, matched.leftOver) + .then(children => [segment].concat(children)); + } else { + return [segment]; + } + }); +} + +function _match(metadata: RoutesMetadata, url: Tree, + current: UrlSegment): _MatchingResult { + for (let r of metadata.routes) { + let matchingResult = _matchWithParts(r, url, current); + if (isPresent(matchingResult)) { + return matchingResult; + } + } + throw new BaseException("Cannot match any routes"); +} + +function _matchWithParts(route: RouteMetadata, url: Tree, + current: UrlSegment): _MatchingResult { + let parts = route.path.split("/"); + let parameters = {}; + let consumedUrlSegments = []; + + let u = current; + for (let i = 0; i < parts.length; ++i) { + consumedUrlSegments.push(u); + let p = parts[i]; + if (p.startsWith(":")) { + let segment = u.segment; + parameters[p.substring(1)] = segment; + } else if (p != u.segment) { + return null; + } + u = url.firstChild(u); + } + return new _MatchingResult(route, consumedUrlSegments, parameters, u); +} + +class _MatchingResult { + constructor(public route: RouteMetadata, public consumedUrlSegments: UrlSegment[], + public parameters: {[key: string]: string}, public leftOver: UrlSegment) {} +} + +function _readMetadata(componentType: Type) { + let metadata = reflector.annotations(componentType).filter(f => f instanceof RoutesMetadata); + if (metadata.length === 0) { + throw new BaseException( + `Component '${stringify(componentType)}' does not have route configuration`); + } + return metadata[0]; +} \ No newline at end of file diff --git a/modules/angular2/test/alt_router/recognize_spec.ts b/modules/angular2/test/alt_router/recognize_spec.ts new file mode 100644 index 0000000000..905b85b790 --- /dev/null +++ b/modules/angular2/test/alt_router/recognize_spec.ts @@ -0,0 +1,89 @@ +import { + ComponentFixture, + AsyncTestCompleter, + TestComponentBuilder, + beforeEach, + ddescribe, + xdescribe, + describe, + el, + expect, + iit, + inject, + beforeEachProviders, + it, + xit +} from 'angular2/testing_internal'; + +import {recognize} from 'angular2/src/alt_router/recognize'; +import {Routes, Route} from 'angular2/alt_router'; +import {provide, Component, ComponentResolver} from 'angular2/core'; +import {UrlSegment, Tree} from 'angular2/src/alt_router/segments'; + +export function main() { + describe('recognize', () => { + it('should handle position args', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree(["b", "paramB", "c", "paramC"])) + .then(r => { + let b = r.root; + expect(stringifyUrl(b.urlSegments)).toEqual(["b", "paramB"]); + expect(b.type).toBe(ComponentB); + + let c = r.firstChild(r.root); + expect(stringifyUrl(c.urlSegments)).toEqual(["c", "paramC"]); + expect(c.type).toBe(ComponentC); + + async.done(); + }); + })); + + it('should error when no matching routes', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree(["invalid"])) + .catch(e => { + expect(e.message).toEqual("Cannot match any routes"); + async.done(); + }); + })); + + it("should error when a component doesn't have @Routes", + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree(["d", "invalid"])) + .catch(e => { + expect(e.message) + .toEqual("Component 'ComponentD' does not have route configuration"); + async.done(); + }); + })); + }); +} + +function tree(nodes: string[]) { + return new Tree(nodes.map(v => new UrlSegment(v, {}, ""))); +} + +function stringifyUrl(segments: UrlSegment[]): string[] { + return segments.map(s => s.segment); +} + +@Component({selector: 'c', template: 't'}) +class ComponentC { +} + +@Component({selector: 'd', template: 't'}) +class ComponentD { +} + +@Component({selector: 'b', template: 't'}) +@Routes([new Route({path: "c/:c", component: ComponentC})]) +class ComponentB { +} + +@Component({selector: 'a', template: 't'}) +@Routes([ + new Route({path: "b/:b", component: ComponentB}), + new Route({path: "d", component: ComponentD}) +]) +class ComponentA { +} \ No newline at end of file