fix(router): allow router-link to link to redirects

Closes #3335

Closes #3624
This commit is contained in:
Brian Ford 2015-08-13 11:09:22 -07:00
parent b5c4d8ba79
commit 72e0b8f7dc
4 changed files with 113 additions and 30 deletions

View File

@ -216,17 +216,17 @@ export class PathRecognizer {
var captured = []; var captured = [];
for (var i = 0; i < this._segments.length; i += 1) { for (var i = 0; i < this._segments.length; i += 1) {
if (isBlank(nextSegment)) {
return null;
}
currentSegment = nextSegment;
var segment = this._segments[i]; var segment = this._segments[i];
currentSegment = nextSegment;
if (segment instanceof ContinuationSegment) { if (segment instanceof ContinuationSegment) {
break; break;
} }
if (isBlank(currentSegment)) {
return null;
}
captured.push(currentSegment.path); captured.push(currentSegment.path);
// the star segment consumes all of the remaining URL, including matrix params // the star segment consumes all of the remaining URL, including matrix params
@ -251,18 +251,26 @@ export class PathRecognizer {
var urlPath = captured.join('/'); var urlPath = captured.join('/');
var auxiliary;
var instruction: ComponentInstruction;
if (isPresent(currentSegment)) {
// If this is the root component, read query params. Otherwise, read matrix params. // If this is the root component, read query params. Otherwise, read matrix params.
var paramsSegment = beginningSegment instanceof RootUrl ? beginningSegment : currentSegment; var paramsSegment = beginningSegment instanceof RootUrl ? beginningSegment : currentSegment;
var allParams = isPresent(paramsSegment.params) ? var allParams = isPresent(paramsSegment.params) ?
StringMapWrapper.merge(paramsSegment.params, positionalParams) : StringMapWrapper.merge(paramsSegment.params, positionalParams) :
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);
} }

View File

@ -33,7 +33,7 @@ import {
import {reflector} from 'angular2/src/reflection/reflection'; import {reflector} from 'angular2/src/reflection/reflection';
import {Injectable} from 'angular2/di'; import {Injectable} from 'angular2/di';
import {normalizeRouteConfig} from './route_config_nomalizer'; import {normalizeRouteConfig} from './route_config_nomalizer';
import {parser, Url} from './url_parser'; import {parser, Url, pathSegmentsToUrl} from './url_parser';
var _resolveToNull = PromiseWrapper.resolve(null); var _resolveToNull = PromiseWrapper.resolve(null);
@ -236,7 +236,8 @@ export class RouteRegistry {
componentCursor = response.componentType; componentCursor = response.componentType;
} }
var instruction = null; var instruction: Instruction = this._generateRedirects(componentCursor);
while (segments.length > 0) { while (segments.length > 0) {
instruction = new Instruction(segments.pop(), instruction, {}); instruction = new Instruction(segments.pop(), instruction, {});
@ -244,6 +245,38 @@ export class RouteRegistry {
return instruction; 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 * Given a list of instructions, returns the most specific instruction
*/ */
function mostSpecific(instructions: List<PrimaryInstruction>): PrimaryInstruction { function mostSpecific(instructions: List<PrimaryInstruction>): PrimaryInstruction {
if (instructions.length == 0) { return ListWrapper.maximum(
return null; instructions, (instruction: PrimaryInstruction) => instruction.component.specificity);
}
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;
} }
function assertTerminalComponent(component, path) { function assertTerminalComponent(component, path) {

View File

@ -57,6 +57,14 @@ export class RootUrl extends Url {
} }
} }
export function pathSegmentsToUrl(pathSegments: List<string>): 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('^[^\\/\\(\\)\\?;=&]+'); var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+');
function matchUrlSegment(str: string): string { function matchUrlSegment(str: string): string {
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str); var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);

View File

@ -14,7 +14,13 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Type} from 'angular2/src/facade/lang'; import {Type} from 'angular2/src/facade/lang';
import {RouteRegistry} from 'angular2/src/router/route_registry'; 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 {stringifyInstruction} from 'angular2/src/router/instruction';
import {IS_DART} from '../platform'; import {IS_DART} from '../platform';
@ -45,6 +51,24 @@ export function main() {
.toEqual('second'); .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', () => { it('should generate URLs with params', () => {
registry.config( registry.config(
RootHostCmp, RootHostCmp,
@ -255,6 +279,28 @@ class DummyAsyncCmp {
class DummyCmpA {} class DummyCmpA {}
class DummyCmpB {} 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'})]) @RouteConfig([new Route({path: '/second', component: DummyCmpB, as: 'secondCmp'})])
class DummyParentCmp { class DummyParentCmp {
} }