import {ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {Promise, PromiseWrapper} from 'angular2/src/facade/async'; import { isPresent, isArray, isBlank, isType, isString, isStringMap, Type, getTypeNameForDebugging, CONST_EXPR } from 'angular2/src/facade/lang'; import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; import {reflector} from 'angular2/src/core/reflection/reflection'; import {Injectable, Inject, OpaqueToken} from 'angular2/core'; import { RouteConfig, AsyncRoute, Route, AuxRoute, Redirect, RouteDefinition } from './route_config_impl'; import {PathMatch, RedirectMatch, RouteMatch} from './route_recognizer'; import {ComponentRecognizer} from './component_recognizer'; import { Instruction, ResolvedInstruction, RedirectInstruction, UnresolvedInstruction, DefaultInstruction } from './instruction'; import {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer'; import {parser, Url, pathSegmentsToUrl} from './url_parser'; var _resolveToNull = PromiseWrapper.resolve(null); /** * Token used to bind the component with the top-level {@link RouteConfig}s for the * application. * * ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm)) * * ``` * import {Component} from 'angular2/angular2'; * import { * ROUTER_DIRECTIVES, * ROUTER_PROVIDERS, * RouteConfig * } from 'angular2/router'; * * @Component({directives: [ROUTER_DIRECTIVES]}) * @RouteConfig([ * {...}, * ]) * class AppCmp { * // ... * } * * bootstrap(AppCmp, [ROUTER_PROVIDERS]); * ``` */ export const ROUTER_PRIMARY_COMPONENT: OpaqueToken = CONST_EXPR(new OpaqueToken('RouterPrimaryComponent')); /** * The RouteRegistry holds route configurations for each component in an Angular app. * It is responsible for creating Instructions from URLs, and generating URLs based on route and * parameters. */ @Injectable() export class RouteRegistry { private _rules = new Map(); constructor(@Inject(ROUTER_PRIMARY_COMPONENT) private _rootComponent: Type) {} /** * Given a component and a configuration object, add the route to this registry */ config(parentComponent: any, config: RouteDefinition): void { config = normalizeRouteConfig(config, this); // this is here because Dart type guard reasons if (config instanceof Route) { assertComponentExists(config.component, config.path); } else if (config instanceof AuxRoute) { assertComponentExists(config.component, config.path); } var recognizer: ComponentRecognizer = this._rules.get(parentComponent); if (isBlank(recognizer)) { recognizer = new ComponentRecognizer(); this._rules.set(parentComponent, recognizer); } var terminal = recognizer.config(config); if (config instanceof Route) { if (terminal) { assertTerminalComponent(config.component, config.path); } else { this.configFromComponent(config.component); } } } /** * Reads the annotations of a component and configures the registry based on them */ configFromComponent(component: any): void { if (!isType(component)) { return; } // Don't read the annotations from a type more than once – // this prevents an infinite loop if a component routes recursively. if (this._rules.has(component)) { return; } var annotations = reflector.annotations(component); if (isPresent(annotations)) { for (var i = 0; i < annotations.length; i++) { var annotation = annotations[i]; if (annotation instanceof RouteConfig) { let routeCfgs: RouteDefinition[] = annotation.configs; routeCfgs.forEach(config => this.config(component, config)); } } } } /** * Given a URL and a parent component, return the most specific instruction for navigating * the application into the state specified by the url */ recognize(url: string, ancestorInstructions: Instruction[]): Promise { var parsedUrl = parser.parse(url); return this._recognize(parsedUrl, ancestorInstructions); } /** * Recognizes all parent-child routes, but creates unresolved auxiliary routes */ private _recognize(parsedUrl: Url, ancestorInstructions: Instruction[], _aux = false): Promise { var parentComponent = ancestorInstructions.length > 0 ? ancestorInstructions[ancestorInstructions.length - 1].component.componentType : this._rootComponent; var componentRecognizer = this._rules.get(parentComponent); if (isBlank(componentRecognizer)) { return _resolveToNull; } // Matches some beginning part of the given URL var possibleMatches: Promise[] = _aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) : componentRecognizer.recognize(parsedUrl); var matchPromises: Promise[] = possibleMatches.map( (candidate: Promise) => candidate.then((candidate: RouteMatch) => { if (candidate instanceof PathMatch) { var auxParentInstructions = ancestorInstructions.length > 0 ? [ancestorInstructions[ancestorInstructions.length - 1]] : []; var auxInstructions = this._auxRoutesToUnresolved(candidate.remainingAux, auxParentInstructions); var instruction = new ResolvedInstruction(candidate.instruction, null, auxInstructions); if (candidate.instruction.terminal) { return instruction; } var newAncestorComponents = ancestorInstructions.concat([instruction]); return this._recognize(candidate.remaining, newAncestorComponents) .then((childInstruction) => { if (isBlank(childInstruction)) { return null; } // redirect instructions are already absolute if (childInstruction instanceof RedirectInstruction) { return childInstruction; } instruction.child = childInstruction; return instruction; }); } if (candidate instanceof RedirectMatch) { var instruction = this.generate(candidate.redirectTo, ancestorInstructions); return new RedirectInstruction(instruction.component, instruction.child, instruction.auxInstruction); } })); if ((isBlank(parsedUrl) || parsedUrl.path == '') && possibleMatches.length == 0) { return PromiseWrapper.resolve(this.generateDefault(parentComponent)); } return PromiseWrapper.all(matchPromises).then(mostSpecific); } private _auxRoutesToUnresolved(auxRoutes: Url[], parentInstructions: Instruction[]): {[key: string]: Instruction} { var unresolvedAuxInstructions: {[key: string]: Instruction} = {}; auxRoutes.forEach((auxUrl: Url) => { unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction( () => { return this._recognize(auxUrl, parentInstructions, true); }); }); return unresolvedAuxInstructions; } /** * Given a normalized list with component names and params like: `['user', {id: 3 }]` * generates a url with a leading slash relative to the provided `parentComponent`. * * If the optional param `_aux` is `true`, then we generate starting at an auxiliary * route boundary. */ generate(linkParams: any[], ancestorInstructions: Instruction[], _aux = false): Instruction { let normalizedLinkParams = splitAndFlattenLinkParams(linkParams); var first = ListWrapper.first(normalizedLinkParams); var rest = ListWrapper.slice(normalizedLinkParams, 1); // The first segment should be either '.' (generate from parent) or '' (generate from root). // When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''. if (first == '') { ancestorInstructions = []; } else if (first == '..') { // we already captured the first instance of "..", so we need to pop off an ancestor ancestorInstructions.pop(); while (ListWrapper.first(rest) == '..') { rest = ListWrapper.slice(rest, 1); ancestorInstructions.pop(); if (ancestorInstructions.length <= 0) { throw new BaseException( `Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`); } } } else if (first != '.') { let parentComponent = this._rootComponent; let grandparentComponent = null; if (ancestorInstructions.length > 1) { parentComponent = ancestorInstructions[ancestorInstructions.length - 1].component.componentType; grandparentComponent = ancestorInstructions[ancestorInstructions.length - 2].component.componentType; } else if (ancestorInstructions.length == 1) { parentComponent = ancestorInstructions[0].component.componentType; grandparentComponent = this._rootComponent; } // For a link with no leading `./`, `/`, or `../`, we look for a sibling and child. // If both exist, we throw. Otherwise, we prefer whichever exists. var childRouteExists = this.hasRoute(first, parentComponent); var parentRouteExists = isPresent(grandparentComponent) && this.hasRoute(first, grandparentComponent); if (parentRouteExists && childRouteExists) { let msg = `Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`; throw new BaseException(msg); } if (parentRouteExists) { ancestorInstructions.pop(); } rest = linkParams; } if (rest[rest.length - 1] == '') { rest.pop(); } if (rest.length < 1) { let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`; throw new BaseException(msg); } var generatedInstruction = this._generate(rest, ancestorInstructions, _aux); for (var i = ancestorInstructions.length - 1; i >= 0; i--) { let ancestorInstruction = ancestorInstructions[i]; generatedInstruction = ancestorInstruction.replaceChild(generatedInstruction); } return generatedInstruction; } /* * Internal helper that does not make any assertions about the beginning of the link DSL */ private _generate(linkParams: any[], ancestorInstructions: Instruction[], _aux = false): Instruction { let parentComponent = ancestorInstructions.length > 0 ? ancestorInstructions[ancestorInstructions.length - 1].component.componentType : this._rootComponent; if (linkParams.length == 0) { return this.generateDefault(parentComponent); } let linkIndex = 0; let routeName = linkParams[linkIndex]; if (!isString(routeName)) { throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`); } else if (routeName == '' || routeName == '.' || routeName == '..') { throw new BaseException(`"${routeName}/" is only allowed at the beginning of a link DSL.`); } let params = {}; if (linkIndex + 1 < linkParams.length) { let nextSegment = linkParams[linkIndex + 1]; if (isStringMap(nextSegment) && !isArray(nextSegment)) { params = nextSegment; linkIndex += 1; } } let auxInstructions: {[key: string]: Instruction} = {}; var nextSegment; while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) { let auxParentInstruction = ancestorInstructions.length > 0 ? [ancestorInstructions[ancestorInstructions.length - 1]] : []; let auxInstruction = this._generate(nextSegment, auxParentInstruction, true); // TODO: this will not work for aux routes with parameters or multiple segments auxInstructions[auxInstruction.component.urlPath] = auxInstruction; linkIndex += 1; } var componentRecognizer = this._rules.get(parentComponent); if (isBlank(componentRecognizer)) { throw new BaseException( `Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`); } var routeRecognizer = (_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName); if (!isPresent(routeRecognizer)) { throw new BaseException( `Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`); } if (!isPresent(routeRecognizer.handler.componentType)) { var compInstruction = routeRecognizer.generateComponentPathValues(params); return new UnresolvedInstruction(() => { return routeRecognizer.handler.resolveComponentType().then( (_) => { return this._generate(linkParams, ancestorInstructions, _aux); }); }, compInstruction['urlPath'], compInstruction['urlParams']); } var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) : componentRecognizer.generate(routeName, params); var remaining = linkParams.slice(linkIndex + 1); var instruction = new ResolvedInstruction(componentInstruction, null, auxInstructions); // the component is sync if (isPresent(componentInstruction.componentType)) { let childInstruction: Instruction = null; if (linkIndex + 1 < linkParams.length) { let childAncestorComponents = ancestorInstructions.concat([instruction]); childInstruction = this._generate(remaining, childAncestorComponents); } else if (!componentInstruction.terminal) { // ... look for defaults childInstruction = this.generateDefault(componentInstruction.componentType); if (isBlank(childInstruction)) { throw new BaseException( `Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal instruction.`); } } instruction.child = childInstruction; } return instruction; } public hasRoute(name: string, parentComponent: any): boolean { var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent); if (isBlank(componentRecognizer)) { return false; } return componentRecognizer.hasRoute(name); } public generateDefault(componentCursor: Type): Instruction { if (isBlank(componentCursor)) { return null; } var componentRecognizer = this._rules.get(componentCursor); if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) { return null; } var defaultChild = null; if (isPresent(componentRecognizer.defaultRoute.handler.componentType)) { var componentInstruction = componentRecognizer.defaultRoute.generate({}); if (!componentRecognizer.defaultRoute.terminal) { defaultChild = this.generateDefault(componentRecognizer.defaultRoute.handler.componentType); } return new DefaultInstruction(componentInstruction, defaultChild); } return new UnresolvedInstruction(() => { return componentRecognizer.defaultRoute.handler.resolveComponentType().then( (_) => this.generateDefault(componentCursor)); }); } } /* * Given: ['/a/b', {c: 2}] * Returns: ['', 'a', 'b', {c: 2}] */ function splitAndFlattenLinkParams(linkParams: any[]): any[] { return linkParams.reduce((accumulation: any[], item) => { if (isString(item)) { let strItem: string = item; return accumulation.concat(strItem.split('/')); } accumulation.push(item); return accumulation; }, []); } /* * Given a list of instructions, returns the most specific instruction */ function mostSpecific(instructions: Instruction[]): Instruction { return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity); } function assertTerminalComponent(component, path) { if (!isType(component)) { return; } var annotations = reflector.annotations(component); if (isPresent(annotations)) { for (var i = 0; i < annotations.length; i++) { var annotation = annotations[i]; if (annotation instanceof RouteConfig) { throw new BaseException( `Child routes are not allowed for "${path}". Use "..." on the parent's route path.`); } } } }