Skip to content

Commit

Permalink
feat(router): auxiliary routes
Browse files Browse the repository at this point in the history
  • Loading branch information
btford committed Aug 3, 2015
1 parent 1beaf81 commit 12dfc75
Show file tree
Hide file tree
Showing 22 changed files with 1,404 additions and 954 deletions.
19 changes: 0 additions & 19 deletions modules/angular2/src/router/helpers.ts

This file was deleted.

83 changes: 58 additions & 25 deletions modules/angular2/src/router/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {isPresent, isBlank, normalizeBlank} from 'angular2/src/facade/lang';

import {PathRecognizer} from './path_recognizer';
import {ParsedUrl} from './parsed_url';

export class RouteParams {
constructor(public params: StringMap<string, string>) {}
Expand All @@ -18,34 +19,66 @@ export class RouteParams {


/**
* An `Instruction` represents the component hierarchy of the application based on a given route
* Represents a completed instruction, including auxiliary routes.
*/
export class Instruction {
// "capturedUrl" is the part of the URL captured by this instruction
// "accumulatedUrl" is the part of the URL captured by this instruction and all children
accumulatedUrl: string;
reuse: boolean = false;
specificity: number;

constructor(public component: any, public capturedUrl: string,
private _recognizer: PathRecognizer, public child: Instruction = null,
private _params: StringMap<string, any> = null) {
this.accumulatedUrl = capturedUrl;
this.specificity = _recognizer.specificity;
if (isPresent(child)) {
this.child = child;
this.specificity += child.specificity;
var childUrl = child.accumulatedUrl;
if (isPresent(childUrl)) {
this.accumulatedUrl += childUrl;
}
}
constructor(public component: ComponentInstruction, public child: Instruction,
public auxInstruction: StringMap<string, Instruction>) {}

replaceChild(child: Instruction): Instruction {
return new Instruction(this.component, child, this.auxInstruction);
}
}

params(): StringMap<string, string> {
if (isBlank(this._params)) {
this._params = this._recognizer.parseParams(this.capturedUrl);
}
return this._params;
/**
* Represents a partially completed instruction during recognition that only has the
* primary (non-aux) route instructions matched.
*/
export class PrimaryInstruction {
constructor(public component: ComponentInstruction, public child: PrimaryInstruction,
public auxUrls: List<ParsedUrl>) {}
}

export function stringifyInstruction(instruction: Instruction): string {
var params = instruction.component.urlParams.length > 0 ?
('?' + instruction.component.urlParams.join('&')) :
'';

return instruction.component.urlPath + stringifyAux(instruction) +
stringifyPrimary(instruction.child) + params;
}

function stringifyPrimary(instruction: Instruction): string {
if (isBlank(instruction)) {
return '';
}
var params = instruction.component.urlParams.length > 0 ?
(';' + instruction.component.urlParams.join(';')) :
'';
return '/' + instruction.component.urlPath + params + stringifyAux(instruction) +
stringifyPrimary(instruction.child);
}

function stringifyAux(instruction: Instruction): string {
var routes = [];
StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => {
routes.push(stringifyPrimary(auxInstruction));
});
if (routes.length > 0) {
return '(' + routes.join('//') + ')';
}
return '';
}


/**
* A `ComponentInstruction` represents the route state for a single component.
*/
export class ComponentInstruction {
reuse: boolean = false;

constructor(public urlPath: string, public urlParams: List<string>,
public recognizer: PathRecognizer, public params: StringMap<string, any> = null) {}

get componentType() { return this.recognizer.handler.componentType; }
}
12 changes: 6 additions & 6 deletions modules/angular2/src/router/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Instruction} from './instruction';
import {ComponentInstruction} from './instruction';
import {global} from 'angular2/src/facade/lang';

// This is here only so that after TS transpilation the file is not empty.
Expand All @@ -11,33 +11,33 @@ var __ignore_me = global;
* Defines route lifecycle method [onActivate]
*/
export interface OnActivate {
onActivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
onActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
}

/**
* Defines route lifecycle method [onReuse]
*/
export interface OnReuse {
onReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
onReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
}

/**
* Defines route lifecycle method [onDeactivate]
*/
export interface OnDeactivate {
onDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
onDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
}

/**
* Defines route lifecycle method [canReuse]
*/
export interface CanReuse {
canReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
canReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
}

/**
* Defines route lifecycle method [canDeactivate]
*/
export interface CanDeactivate {
canDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
canDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
}
210 changes: 210 additions & 0 deletions modules/angular2/src/router/parsed_url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
import {
isPresent,
isBlank,
BaseException,
RegExpWrapper,
CONST_EXPR
} from 'angular2/src/facade/lang';


export class ParsedUrl {
constructor(public path: string, public child: ParsedUrl = null,
public auxiliary: List<ParsedUrl> = CONST_EXPR([]),
public params: StringMap<string, any> = null) {}

toString(): string {
return this.path + this._matrixParamsToString() + this._auxToString() + this._childString();
}

segmentToString(): string { return this.path + this._matrixParamsToString(); }

_auxToString(): string {
return this.auxiliary.length > 0 ?
('(' + this.auxiliary.map(sibling => sibling.toString()).join('//') + ')') :
'';
}

private _matrixParamsToString(): string {
if (isBlank(this.params)) {
return '';
}

return ';' + serializeParams(this.params).join(';');
}

_childString(): string { return isPresent(this.child) ? ('/' + this.child.toString()) : ''; }
}

export class RootParsedUrl extends ParsedUrl {
constructor(path: string, child: ParsedUrl = null, auxiliary: List<ParsedUrl> = CONST_EXPR([]),
params: StringMap<string, any> = null) {
super(path, child, auxiliary, params);
}

toString(): string {
return this.path + this._auxToString() + this._childString() + this._queryParamsToString();
}

segmentToString(): string { return this.path + this._queryParamsToString(); }

private _queryParamsToString(): string {
if (isBlank(this.params)) {
return '';
}

return '?' + serializeParams(this.params).join('&');
}
}

var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+');
function matchUrlSegment(str: string): string {
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);
return isPresent(match) ? match[0] : null;
}

export class UrlParser {
private remaining: string;

peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }

capture(str: string): void {
if (!this.remaining.startsWith(str)) {
throw new BaseException(`Expected "${str}".`);
}
this.remaining = this.remaining.substring(str.length);
}

parse(url: string): ParsedUrl {
this.remaining = url;
if (url == '' || url == '/') {
return new ParsedUrl('');
}
return this.parseRoot();
}

// segment + (aux segments) + (query params)
parseRoot(): ParsedUrl {
if (this.peekStartsWith('/')) {
this.capture('/');
}
var path = matchUrlSegment(this.remaining);
this.capture(path);

var aux = [];
if (this.peekStartsWith('(')) {
aux = this.parseAuxiliaryRoutes();
}
if (this.peekStartsWith(';')) {
// TODO: should these params just be dropped?
this.parseMatrixParams();
}
var child = null;
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
this.capture('/');
child = this.parseSegment();
}
var queryParams = null;
if (this.peekStartsWith('?')) {
queryParams = this.parseQueryParams();
}
return new RootParsedUrl(path, child, aux, queryParams);
}

// segment + (matrix params) + (aux segments)
parseSegment(): ParsedUrl {
if (this.remaining.length == 0) {
return null;
}
if (this.peekStartsWith('/')) {
this.capture('/');
}
var path = matchUrlSegment(this.remaining);
this.capture(path);

var matrixParams = null;
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
var aux = [];
if (this.peekStartsWith('(')) {
aux = this.parseAuxiliaryRoutes();
}
var child = null;
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
this.capture('/');
child = this.parseSegment();
}
return new ParsedUrl(path, child, aux, matrixParams);
}

parseQueryParams(): StringMap<string, any> {
var params = {};
this.capture('?');
this.parseParam(params);
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
this.capture('&');
this.parseParam(params);
}
return params;
}

parseMatrixParams(): StringMap<string, any> {
var params = {};
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
this.capture(';');
this.parseParam(params);
}
return params;
}

parseParam(params: StringMap<string, any>): void {
var key = matchUrlSegment(this.remaining);
if (isBlank(key)) {
return;
}
this.capture(key);
var value: any = true;
if (this.peekStartsWith('=')) {
this.capture('=');
var valueMatch = matchUrlSegment(this.remaining);
if (isPresent(valueMatch)) {
value = valueMatch;
this.capture(value);
}
}

params[key] = value;
}

parseAuxiliaryRoutes(): List<ParsedUrl> {
var routes = [];
this.capture('(');

while (!this.peekStartsWith(')') && this.remaining.length > 0) {
routes.push(this.parseSegment());
if (this.peekStartsWith('//')) {
this.capture('//');
}
}
this.capture(')');

return routes;
}
}

export var parser = new UrlParser();

export function serializeParams(paramMap: StringMap<string, any>): List<string> {
var params = [];
if (isPresent(paramMap)) {
StringMapWrapper.forEach(paramMap, (value, key) => {
if (value == true) {
params.push(key);
} else {
params.push(key + '=' + value);
}
});
}
return params;
}
Loading

0 comments on commit 12dfc75

Please sign in to comment.