From 72e0b8f7dc3ed6295836f2f112f3cfcab6049780 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Thu, 13 Aug 2015 11:09:22 -0700 Subject: [PATCH] fix(router): allow router-link to link to redirects Closes #3335 Closes #3624 --- .../angular2/src/router/path_recognizer.ts | 34 +++++++----- modules/angular2/src/router/route_registry.ts | 53 +++++++++++++------ modules/angular2/src/router/url_parser.ts | 8 +++ .../test/router/route_registry_spec.ts | 48 ++++++++++++++++- 4 files changed, 113 insertions(+), 30 deletions(-) diff --git a/modules/angular2/src/router/path_recognizer.ts b/modules/angular2/src/router/path_recognizer.ts index d44380caa9..269dece17c 100644 --- a/modules/angular2/src/router/path_recognizer.ts +++ b/modules/angular2/src/router/path_recognizer.ts @@ -216,17 +216,17 @@ export class PathRecognizer { var captured = []; for (var i = 0; i < this._segments.length; i += 1) { - if (isBlank(nextSegment)) { - return null; - } - currentSegment = nextSegment; - var segment = this._segments[i]; + currentSegment = nextSegment; if (segment instanceof ContinuationSegment) { break; } + if (isBlank(currentSegment)) { + return null; + } + captured.push(currentSegment.path); // the star segment consumes all of the remaining URL, including matrix params @@ -251,18 +251,26 @@ export class PathRecognizer { var urlPath = captured.join('/'); - // If this is the root component, read query params. Otherwise, read matrix params. - var paramsSegment = beginningSegment instanceof RootUrl ? beginningSegment : currentSegment; + var auxiliary; + var instruction: ComponentInstruction; + if (isPresent(currentSegment)) { + // If this is the root component, read query params. Otherwise, read matrix params. + var paramsSegment = beginningSegment instanceof RootUrl ? beginningSegment : currentSegment; + var allParams = isPresent(paramsSegment.params) ? + StringMapWrapper.merge(paramsSegment.params, positionalParams) : + positionalParams; - var allParams = isPresent(paramsSegment.params) ? - StringMapWrapper.merge(paramsSegment.params, positionalParams) : - positionalParams; - var urlParams = serializeParams(paramsSegment.params); + var urlParams = serializeParams(paramsSegment.params); - var instruction = new ComponentInstruction(urlPath, urlParams, this, allParams); + instruction = new ComponentInstruction(urlPath, urlParams, this, allParams); - return new PathMatch(instruction, nextSegment, currentSegment.auxiliary); + auxiliary = currentSegment.auxiliary; + } else { + instruction = new ComponentInstruction(urlPath, [], this, positionalParams); + auxiliary = []; + } + return new PathMatch(instruction, nextSegment, auxiliary); } diff --git a/modules/angular2/src/router/route_registry.ts b/modules/angular2/src/router/route_registry.ts index fa73497918..bc7e3a7f7d 100644 --- a/modules/angular2/src/router/route_registry.ts +++ b/modules/angular2/src/router/route_registry.ts @@ -33,7 +33,7 @@ import { import {reflector} from 'angular2/src/reflection/reflection'; import {Injectable} from 'angular2/di'; import {normalizeRouteConfig} from './route_config_nomalizer'; -import {parser, Url} from './url_parser'; +import {parser, Url, pathSegmentsToUrl} from './url_parser'; var _resolveToNull = PromiseWrapper.resolve(null); @@ -236,7 +236,8 @@ export class RouteRegistry { componentCursor = response.componentType; } - var instruction = null; + var instruction: Instruction = this._generateRedirects(componentCursor); + while (segments.length > 0) { instruction = new Instruction(segments.pop(), instruction, {}); @@ -244,6 +245,38 @@ export class RouteRegistry { return instruction; } + + // if the child includes a redirect like : "/" -> "/something", + // we want to honor that redirection when creating the link + private _generateRedirects(componentCursor: Type): Instruction { + if (isBlank(componentCursor)) { + return null; + } + var componentRecognizer = this._rules.get(componentCursor); + if (isBlank(componentRecognizer)) { + return null; + } + + for (let i = 0; i < componentRecognizer.redirects.length; i += 1) { + let redirect = componentRecognizer.redirects[i]; + + // we only handle redirecting from an empty segment + if (redirect.segments.length == 1 && redirect.segments[0] == '') { + var toSegments = pathSegmentsToUrl(redirect.toSegments); + var matches = componentRecognizer.recognize(toSegments); + var primaryInstruction = + ListWrapper.maximum(matches, (match: PathMatch) => match.instruction.specificity); + + if (isPresent(primaryInstruction)) { + var child = this._generateRedirects(primaryInstruction.instruction.componentType); + return new Instruction(primaryInstruction.instruction, child, {}); + } + return null; + } + } + + return null; + } } @@ -251,20 +284,8 @@ export class RouteRegistry { * Given a list of instructions, returns the most specific instruction */ function mostSpecific(instructions: List): PrimaryInstruction { - if (instructions.length == 0) { - return null; - } - var mostSpecificSolution = instructions[0]; - for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) { - var solution: PrimaryInstruction = instructions[solutionIndex]; - if (isBlank(solution)) { - continue; - } - if (solution.component.specificity > mostSpecificSolution.component.specificity) { - mostSpecificSolution = solution; - } - } - return mostSpecificSolution; + return ListWrapper.maximum( + instructions, (instruction: PrimaryInstruction) => instruction.component.specificity); } function assertTerminalComponent(component, path) { diff --git a/modules/angular2/src/router/url_parser.ts b/modules/angular2/src/router/url_parser.ts index 1e9e15c58d..647d72a8da 100644 --- a/modules/angular2/src/router/url_parser.ts +++ b/modules/angular2/src/router/url_parser.ts @@ -57,6 +57,14 @@ export class RootUrl extends Url { } } +export function pathSegmentsToUrl(pathSegments: List): Url { + var url = new Url(pathSegments[pathSegments.length - 1]); + for (var i = pathSegments.length - 2; i >= 0; i -= 1) { + url = new Url(pathSegments[i], url); + } + return url; +} + var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+'); function matchUrlSegment(str: string): string { var match = RegExpWrapper.firstMatch(SEGMENT_RE, str); diff --git a/modules/angular2/test/router/route_registry_spec.ts b/modules/angular2/test/router/route_registry_spec.ts index bdf9f8303e..27f7b1ef4e 100644 --- a/modules/angular2/test/router/route_registry_spec.ts +++ b/modules/angular2/test/router/route_registry_spec.ts @@ -14,7 +14,13 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import {Type} from 'angular2/src/facade/lang'; import {RouteRegistry} from 'angular2/src/router/route_registry'; -import {RouteConfig, Route, AuxRoute, AsyncRoute} from 'angular2/src/router/route_config_decorator'; +import { + RouteConfig, + Route, + Redirect, + AuxRoute, + AsyncRoute +} from 'angular2/src/router/route_config_decorator'; import {stringifyInstruction} from 'angular2/src/router/instruction'; import {IS_DART} from '../platform'; @@ -45,6 +51,24 @@ export function main() { .toEqual('second'); }); + it('should generate URLs that account for redirects', () => { + registry.config( + RootHostCmp, + new Route({path: '/first/...', component: DummyParentRedirectCmp, as: 'firstCmp'})); + + expect(stringifyInstruction(registry.generate(['firstCmp'], RootHostCmp))) + .toEqual('first/second'); + }); + + it('should generate URLs in a hierarchy of redirects', () => { + registry.config( + RootHostCmp, + new Route({path: '/first/...', component: DummyMultipleRedirectCmp, as: 'firstCmp'})); + + expect(stringifyInstruction(registry.generate(['firstCmp'], RootHostCmp))) + .toEqual('first/second/third'); + }); + it('should generate URLs with params', () => { registry.config( RootHostCmp, @@ -255,6 +279,28 @@ class DummyAsyncCmp { class DummyCmpA {} class DummyCmpB {} +@RouteConfig([ + new Redirect({path: '/', redirectTo: '/third'}), + new Route({path: '/third', component: DummyCmpB, as: 'thirdCmp'}) +]) +class DummyRedirectCmp { +} + + +@RouteConfig([ + new Redirect({path: '/', redirectTo: '/second'}), + new Route({path: '/second/...', component: DummyRedirectCmp, as: 'secondCmp'}) +]) +class DummyMultipleRedirectCmp { +} + +@RouteConfig([ + new Redirect({path: '/', redirectTo: '/second'}), + new Route({path: '/second', component: DummyCmpB, as: 'secondCmp'}) +]) +class DummyParentRedirectCmp { +} + @RouteConfig([new Route({path: '/second', component: DummyCmpB, as: 'secondCmp'})]) class DummyParentCmp { }