diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index 6d0a47488e..54541dff11 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -3,158 +3,202 @@ import {of } from 'rxjs/observable/of'; import {Route, RouterConfig} from './config'; import {PRIMARY_OUTLET} from './shared'; -import {UrlSegment, UrlTree} from './url_tree'; -import {first} from './utils/collection'; -import {TreeNode} from './utils/tree'; +import {UrlPathWithParams, UrlSegment, UrlTree, mapChildren} from './url_tree'; -class NoMatch {} +class NoMatch { + constructor(public segment: UrlSegment = null) {} +} class GlobalRedirect { - constructor(public segments: UrlSegment[]) {} + constructor(public paths: UrlPathWithParams[]) {} } export function applyRedirects(urlTree: UrlTree, config: RouterConfig): Observable { try { - const transformedChildren = urlTree._root.children.map(c => applyNode(config, c)); - return createUrlTree(urlTree, transformedChildren); + return createUrlTree(urlTree, expandSegment(config, urlTree.root, PRIMARY_OUTLET)); } catch (e) { if (e instanceof GlobalRedirect) { - return createUrlTree(urlTree, [constructNodes(e.segments, [], [])]); + return createUrlTree( + urlTree, new UrlSegment([], {[PRIMARY_OUTLET]: new UrlSegment(e.paths, {})})); } else if (e instanceof NoMatch) { - return new Observable(obs => obs.error(new Error('Cannot match any routes'))); + return new Observable( + obs => obs.error(new Error(`Cannot match any routes: '${e.segment}'`))); } else { return new Observable(obs => obs.error(e)); } } } -function createUrlTree(urlTree: UrlTree, children: TreeNode[]): Observable { - const transformedRoot = new TreeNode(urlTree.root, children); - return of (new UrlTree(transformedRoot, urlTree.queryParams, urlTree.fragment)); +function createUrlTree(urlTree: UrlTree, root: UrlSegment): Observable { + return of (new UrlTree(root, urlTree.queryParams, urlTree.fragment)); } -function applyNode(config: Route[], url: TreeNode): TreeNode { - for (let r of config) { +function expandSegment(routes: Route[], segment: UrlSegment, outlet: string): UrlSegment { + if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) { + return new UrlSegment([], expandSegmentChildren(routes, segment)); + } else { + return expandPathsWithParams(segment, routes, segment.pathsWithParams, outlet, true); + } +} + +function expandSegmentChildren(routes: Route[], segment: UrlSegment): {[name: string]: UrlSegment} { + return mapChildren(segment, (child, childOutlet) => expandSegment(routes, child, childOutlet)); +} + +function expandPathsWithParams( + segment: UrlSegment, routes: Route[], paths: UrlPathWithParams[], outlet: string, + allowRedirects: boolean): UrlSegment { + for (let r of routes) { try { - return matchNode(config, r, url); + return expandPathsWithParamsAgainstRoute(segment, routes, r, paths, outlet, allowRedirects); } catch (e) { if (!(e instanceof NoMatch)) throw e; } } - throw new NoMatch(); + throw new NoMatch(segment); } -function matchNode(config: Route[], route: Route, url: TreeNode): TreeNode { - if (!route.path) throw new NoMatch(); - if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== url.value.outlet) { - throw new NoMatch(); - } +function expandPathsWithParamsAgainstRoute( + segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], outlet: string, + allowRedirects: boolean): UrlSegment { + if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch(); + if (route.redirectTo && !allowRedirects) throw new NoMatch(); + if (route.redirectTo) { + return expandPathsWithParamsAgainstRouteUsingRedirect(segment, routes, route, paths, outlet); + } else { + return matchPathsWithParamsAgainstRoute(segment, route, paths); + } +} + +function expandPathsWithParamsAgainstRouteUsingRedirect( + segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], + outlet: string): UrlSegment { if (route.path === '**') { - const newSegments = applyRedirectCommands([], route.redirectTo, {}); - return constructNodes(newSegments, [], []); + return expandWildCardWithParamsAgainstRouteUsingRedirect(route); + } else { + return expandRegularPathWithParamsAgainstRouteUsingRedirect( + segment, routes, route, paths, outlet); } - - const m = match(route, url); - if (!m) throw new NoMatch(); - const {consumedUrlSegments, lastSegment, lastParent, positionalParamSegments} = m; - - const newSegments = - applyRedirectCommands(consumedUrlSegments, route.redirectTo, positionalParamSegments); - - const childConfig = route.children ? route.children : []; - const transformedChildren = lastSegment.children.map(c => applyNode(childConfig, c)); - - const secondarySubtrees = lastParent ? lastParent.children.slice(1) : []; - const transformedSecondarySubtrees = secondarySubtrees.map(c => applyNode(config, c)); - - return constructNodes(newSegments, transformedChildren, transformedSecondarySubtrees); } -export function match(route: Route, url: TreeNode) { +function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): UrlSegment { + const newPaths = applyRedirectCommands([], route.redirectTo, {}); + if (route.redirectTo.startsWith('/')) { + throw new GlobalRedirect(newPaths); + } else { + return new UrlSegment(newPaths, {}); + } +} +function expandRegularPathWithParamsAgainstRouteUsingRedirect( + segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], + outlet: string): UrlSegment { + const {consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths); + const newPaths = applyRedirectCommands(consumedPaths, route.redirectTo, positionalParamSegments); + if (route.redirectTo.startsWith('/')) { + throw new GlobalRedirect(newPaths); + } else { + return expandPathsWithParams( + segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false); + } +} + +function matchPathsWithParamsAgainstRoute( + segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): UrlSegment { + if (route.path === '**') { + return new UrlSegment(paths, {}); + } else { + const {consumedPaths, lastChild} = match(segment, route, paths); + const childConfig = route.children ? route.children : []; + const slicedPath = paths.slice(lastChild); + + if (childConfig.length === 0 && slicedPath.length === 0) { + return new UrlSegment(consumedPaths, {}); + + // TODO: check that the right segment is present + } else if (slicedPath.length === 0 && Object.keys(segment.children).length > 0) { + const children = expandSegmentChildren(childConfig, segment); + return new UrlSegment(consumedPaths, children); + + } else { + const cs = expandPathsWithParams(segment, childConfig, slicedPath, PRIMARY_OUTLET, true); + return new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children); + } + } +} + +function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { + if (route.index || route.path === '' || route.path === '/') { + if (route.terminal && (Object.keys(segment.children).length > 0 || paths.length > 0)) { + throw new NoMatch(); + } else { + return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}}; + } + } + const path = route.path.startsWith('/') ? route.path.substring(1) : route.path; const parts = path.split('/'); const positionalParamSegments = {}; - const consumedUrlSegments = []; + const consumedPaths = []; - let lastParent: TreeNode|null = null; - let lastSegment: TreeNode|null = null; + let currentIndex = 0; - let current: TreeNode|null = url; for (let i = 0; i < parts.length; ++i) { - if (!current) return null; + if (currentIndex >= paths.length) throw new NoMatch(); + const current = paths[currentIndex]; const p = parts[i]; - const isLastSegment = i === parts.length - 1; - const isLastParent = i === parts.length - 2; const isPosParam = p.startsWith(':'); - if (!isPosParam && p != current.value.path) return null; - if (isLastSegment) { - lastSegment = current; - } - if (isLastParent) { - lastParent = current; - } + if (!isPosParam && p !== current.path) throw new NoMatch(); if (isPosParam) { - positionalParamSegments[p.substring(1)] = current.value; + positionalParamSegments[p.substring(1)] = current; } - consumedUrlSegments.push(current.value); - current = first(current.children); + consumedPaths.push(current); + currentIndex++; } - if (!lastSegment) throw 'Cannot be reached'; - return {consumedUrlSegments, lastSegment, lastParent, positionalParamSegments}; -} -function constructNodes( - segments: UrlSegment[], children: TreeNode[], - secondary: TreeNode[]): TreeNode { - let prevChildren = children; - for (let i = segments.length - 1; i >= 0; --i) { - if (i === segments.length - 2) { - prevChildren = [new TreeNode(segments[i], prevChildren.concat(secondary))]; - } else { - prevChildren = [new TreeNode(segments[i], prevChildren)]; - } + if (route.terminal && (Object.keys(segment.children).length > 0 || currentIndex < paths.length)) { + throw new NoMatch(); } - return prevChildren[0]; + + return {consumedPaths, lastChild: currentIndex, positionalParamSegments}; } function applyRedirectCommands( - segments: UrlSegment[], redirectTo: string, - posParams: {[k: string]: UrlSegment}): UrlSegment[] { - if (!redirectTo) return segments; - + paths: UrlPathWithParams[], redirectTo: string, + posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] { if (redirectTo.startsWith('/')) { const parts = redirectTo.substring(1).split('/'); - throw new GlobalRedirect(createSegments(redirectTo, parts, segments, posParams)); + throw new GlobalRedirect(createPaths(redirectTo, parts, paths, posParams)); } else { - return createSegments(redirectTo, redirectTo.split('/'), segments, posParams); + return createPaths(redirectTo, redirectTo.split('/'), paths, posParams); } } -function createSegments( - redirectTo: string, parts: string[], segments: UrlSegment[], - posParams: {[k: string]: UrlSegment}): UrlSegment[] { +function createPaths( + redirectTo: string, parts: string[], segments: UrlPathWithParams[], + posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] { return parts.map( - p => p.startsWith(':') ? findPosParamSegment(p, posParams, redirectTo) : - findOrCreateSegment(p, segments)); + p => p.startsWith(':') ? findPosParam(p, posParams, redirectTo) : + findOrCreatePath(p, segments)); } -function findPosParamSegment( - part: string, posParams: {[k: string]: UrlSegment}, redirectTo: string): UrlSegment { +function findPosParam( + part: string, posParams: {[k: string]: UrlPathWithParams}, + redirectTo: string): UrlPathWithParams { const paramName = part.substring(1); const pos = posParams[paramName]; if (!pos) throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${part}'.`); return pos; } -function findOrCreateSegment(part: string, segments: UrlSegment[]): UrlSegment { - const matchingIndex = segments.findIndex(s => s.path === part); +function findOrCreatePath(part: string, paths: UrlPathWithParams[]): UrlPathWithParams { + const matchingIndex = paths.findIndex(s => s.path === part); if (matchingIndex > -1) { - const r = segments[matchingIndex]; - segments.splice(matchingIndex); + const r = paths[matchingIndex]; + paths.splice(matchingIndex); return r; } else { - return new UrlSegment(part, {}, PRIMARY_OUTLET); + return new UrlPathWithParams(part, {}); } } diff --git a/modules/@angular/router/src/common_router_providers.ts b/modules/@angular/router/src/common_router_providers.ts index 8ebc5ee46a..70a534848a 100644 --- a/modules/@angular/router/src/common_router_providers.ts +++ b/modules/@angular/router/src/common_router_providers.ts @@ -72,9 +72,7 @@ export function provideRouter(config: RouterConfig, opts: ExtraOptions): any[] { setTimeout(_ => { const appRef = injector.get(ApplicationRef); if (appRef.componentTypes.length == 0) { - appRef.registerBootstrapListener((_) => { - injector.get(Router).initialNavigation(); - }); + appRef.registerBootstrapListener((_) => { injector.get(Router).initialNavigation(); }); } else { injector.get(Router).initialNavigation(); } diff --git a/modules/@angular/router/src/config.ts b/modules/@angular/router/src/config.ts index 754a22a24f..b33cca44d9 100644 --- a/modules/@angular/router/src/config.ts +++ b/modules/@angular/router/src/config.ts @@ -5,6 +5,7 @@ export type RouterConfig = Route[]; export interface Route { index?: boolean; path?: string; + terminal?: boolean; component?: Type|string; outlet?: string; canActivate?: any[]; diff --git a/modules/@angular/router/src/create_router_state.ts b/modules/@angular/router/src/create_router_state.ts index 3037ab2b7c..9b6c37aa2c 100644 --- a/modules/@angular/router/src/create_router_state.ts +++ b/modules/@angular/router/src/create_router_state.ts @@ -41,7 +41,7 @@ function createOrReuseChildren( function createActivatedRoute(c: ActivatedRouteSnapshot) { return new ActivatedRoute( - new BehaviorSubject(c.urlSegments), new BehaviorSubject(c.params), c.outlet, c.component, c); + new BehaviorSubject(c.url), new BehaviorSubject(c.params), c.outlet, c.component, c); } function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean { diff --git a/modules/@angular/router/src/create_url_tree.ts b/modules/@angular/router/src/create_url_tree.ts index 5825cacbce..32556d21be 100644 --- a/modules/@angular/router/src/create_url_tree.ts +++ b/modules/@angular/router/src/create_url_tree.ts @@ -1,36 +1,52 @@ import {ActivatedRoute} from './router_state'; import {PRIMARY_OUTLET, Params} from './shared'; -import {UrlSegment, UrlTree} from './url_tree'; +import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree'; import {forEach, shallowEqual} from './utils/collection'; -import {TreeNode} from './utils/tree'; export function createUrlTree( route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params | undefined, fragment: string | undefined): UrlTree { if (commands.length === 0) { - return tree(urlTree._root, urlTree, queryParams, fragment); + return tree(urlTree.root, urlTree.root, urlTree, queryParams, fragment); } const normalizedCommands = normalizeCommands(commands); if (navigateToRoot(normalizedCommands)) { - return tree(new TreeNode(urlTree.root, []), urlTree, queryParams, fragment); + return tree(urlTree.root, new UrlSegment([], {}), urlTree, queryParams, fragment); } - const startingNode = findStartingNode(normalizedCommands, urlTree, route); - const updated = normalizedCommands.commands.length > 0 ? - updateMany(startingNode.children.slice(0), normalizedCommands.commands) : - []; - const newRoot = constructNewTree(urlTree._root, startingNode, updated); - - return tree(newRoot, urlTree, queryParams, fragment); + const startingPosition = findStartingPosition(normalizedCommands, urlTree, route); + const segment = startingPosition.processChildren ? + updateSegmentChildren( + startingPosition.segment, startingPosition.index, normalizedCommands.commands) : + updateSegment(startingPosition.segment, startingPosition.index, normalizedCommands.commands); + return tree(startingPosition.segment, segment, urlTree, queryParams, fragment); } function tree( - root: TreeNode, urlTree: UrlTree, queryParams: Params | undefined, - fragment: string | undefined): UrlTree { + oldSegment: UrlSegment, newSegment: UrlSegment, urlTree: UrlTree, + queryParams: Params | undefined, fragment: string | undefined): UrlTree { const q = queryParams ? stringify(queryParams) : urlTree.queryParams; const f = fragment ? fragment : urlTree.fragment; - return new UrlTree(root, q, f); + + if (urlTree.root === oldSegment) { + return new UrlTree(newSegment, q, f); + } else { + return new UrlTree(replaceSegment(urlTree.root, oldSegment, newSegment), q, f); + } +} + +function replaceSegment( + current: UrlSegment, oldSegment: UrlSegment, newSegment: UrlSegment): UrlSegment { + const children = {}; + forEach(current.children, (c, k) => { + if (c === oldSegment) { + children[k] = newSegment; + } else { + children[k] = replaceSegment(c, oldSegment, newSegment); + } + }); + return new UrlSegment(current.pathsWithParams, children); } function navigateToRoot(normalizedChange: NormalizedNavigationCommands): boolean { @@ -87,63 +103,30 @@ function normalizeCommands(commands: any[]): NormalizedNavigationCommands { return new NormalizedNavigationCommands(isAbsolute, numberOfDoubleDots, res); } -function findStartingNode( - normalizedChange: NormalizedNavigationCommands, urlTree: UrlTree, - route: ActivatedRoute): TreeNode { - if (normalizedChange.isAbsolute) { - return urlTree._root; - } else { - const urlSegment = findUrlSegment(route, urlTree, normalizedChange.numberOfDoubleDots); - return findMatchingNode(urlSegment, urlTree._root); - } +class Position { + constructor(public segment: UrlSegment, public processChildren: boolean, public index: number) {} } -function findUrlSegment( - route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment { - const urlSegment = route.snapshot._lastUrlSegment; - const path = urlTree.pathFromRoot(urlSegment); - if (path.length <= numberOfDoubleDots) { +function findStartingPosition( + normalizedChange: NormalizedNavigationCommands, urlTree: UrlTree, + route: ActivatedRoute): Position { + if (normalizedChange.isAbsolute) { + return new Position(urlTree.root, true, 0); + } else if (route.snapshot._lastPathIndex === -1) { + return new Position(route.snapshot._urlSegment, true, 0); + } else if (route.snapshot._lastPathIndex + 1 - normalizedChange.numberOfDoubleDots >= 0) { + return new Position( + route.snapshot._urlSegment, false, + route.snapshot._lastPathIndex + 1 - normalizedChange.numberOfDoubleDots); + } else { throw new Error('Invalid number of \'../\''); } - return path[path.length - 1 - numberOfDoubleDots]; } -function findMatchingNode(segment: UrlSegment, node: TreeNode): TreeNode { - if (node.value === segment) return node; - for (let c of node.children) { - const r = findMatchingNode(segment, c); - if (r) return r; - } - throw new Error(`Cannot find url segment '${segment}'`); -} - -function constructNewTree( - node: TreeNode, original: TreeNode, - updated: TreeNode[]): TreeNode { - if (node === original) { - return new TreeNode(node.value, updated); - } else { - return new TreeNode( - node.value, node.children.map(c => constructNewTree(c, original, updated))); - } -} - -function updateMany(nodes: TreeNode[], commands: any[]): TreeNode[] { - const outlet = getOutlet(commands); - const nodesInRightOutlet = nodes.filter(c => c.value.outlet === outlet); - if (nodesInRightOutlet.length > 0) { - const nodeRightOutlet = nodesInRightOutlet[0]; // there can be only one - nodes[nodes.indexOf(nodeRightOutlet)] = update(nodeRightOutlet, commands); - } else { - nodes.push(update(null, commands)); - } - return nodes; -} - -function getPath(commands: any[]): any { - if (!(typeof commands[0] === 'string')) return commands[0]; - const parts = commands[0].toString().split(':'); - return parts.length > 1 ? parts[1] : commands[0]; +function getPath(command: any): any { + if (!(typeof command === 'string')) return command; + const parts = command.toString().split(':'); + return parts.length > 1 ? parts[1] : command; } function getOutlet(commands: any[]): string { @@ -152,49 +135,91 @@ function getOutlet(commands: any[]): string { return parts.length > 1 ? parts[0] : PRIMARY_OUTLET; } -function update(node: TreeNode| null, commands: any[]): TreeNode { - const rest = commands.slice(1); - const next = rest.length === 0 ? null : rest[0]; - const outlet = getOutlet(commands); - const path = getPath(commands); - - // reach the end of the tree => create new tree nodes. - if (!node && !(typeof next === 'object')) { - const urlSegment = new UrlSegment(path, {}, outlet); - const children = rest.length === 0 ? [] : [update(null, rest)]; - return new TreeNode(urlSegment, children); - - } else if (!node && typeof next === 'object') { - const urlSegment = new UrlSegment(path, stringify(next), outlet); - return recurse(urlSegment, node, rest.slice(1)); - - // different outlet => preserve the subtree - } else if (node && outlet !== node.value.outlet) { - return node; - - // params command - } else if (node && typeof path === 'object') { - const newSegment = new UrlSegment(node.value.path, stringify(path), node.value.outlet); - return recurse(newSegment, node, rest); - - // next one is a params command && can reuse the node - } else if (node && typeof next === 'object' && compare(path, stringify(next), node.value)) { - return recurse(node.value, node, rest.slice(1)); - - // next one is a params command && cannot reuse the node - } else if (node && typeof next === 'object') { - const urlSegment = new UrlSegment(path, stringify(next), outlet); - return recurse(urlSegment, node, rest.slice(1)); - - // next one is not a params command && can reuse the node - } else if (node && compare(path, {}, node.value)) { - return recurse(node.value, node, rest); - - // next one is not a params command && cannot reuse the node - } else { - const urlSegment = new UrlSegment(path, {}, outlet); - return recurse(urlSegment, node, rest); +function updateSegment(segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment { + if (!segment) { + segment = new UrlSegment([], {}); } + if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) { + return updateSegmentChildren(segment, startIndex, commands); + } + const m = prefixedWith(segment, startIndex, commands); + const slicedCommands = commands.slice(m.lastIndex); + + if (m.match && slicedCommands.length === 0) { + return new UrlSegment(segment.pathsWithParams, {}); + } else if (m.match && Object.keys(segment.children).length === 0) { + return createNewSegment(segment, startIndex, commands); + } else if (m.match) { + return updateSegmentChildren(segment, 0, slicedCommands); + } else { + return createNewSegment(segment, startIndex, commands); + } +} + +function updateSegmentChildren( + segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment { + if (commands.length === 0) { + return new UrlSegment(segment.pathsWithParams, {}); + } else { + const outlet = getOutlet(commands); + const children = {}; + children[outlet] = updateSegment(segment.children[outlet], startIndex, commands); + forEach(segment.children, (child, childOutlet) => { + if (childOutlet !== outlet) { + children[childOutlet] = child; + } + }); + return new UrlSegment(segment.pathsWithParams, children); + } +} + +function prefixedWith(segment: UrlSegment, startIndex: number, commands: any[]) { + let currentCommandIndex = 0; + let currentPathIndex = startIndex; + + const noMatch = {match: false, lastIndex: 0}; + while (currentPathIndex < segment.pathsWithParams.length) { + if (currentCommandIndex >= commands.length) return noMatch; + const path = segment.pathsWithParams[currentPathIndex]; + const curr = getPath(commands[currentCommandIndex]); + const next = + currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null; + + if (curr && next && (typeof next === 'object')) { + if (!compare(curr, next, path)) return noMatch; + currentCommandIndex += 2; + } else { + if (!compare(curr, {}, path)) return noMatch; + currentCommandIndex++; + } + currentPathIndex++; + } + + return { match: true, lastIndex: currentCommandIndex }; +} + +function createNewSegment(segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment { + const paths = segment.pathsWithParams.slice(0, startIndex); + let i = 0; + while (i < commands.length) { + if (i === 0 && (typeof commands[0] === 'object')) { + const p = segment.pathsWithParams[startIndex]; + paths.push(new UrlPathWithParams(p.path, commands[0])); + i++; + continue; + } + + const curr = getPath(commands[i]); + const next = (i < commands.length - 1) ? commands[i + 1] : null; + if (curr && next && (typeof next === 'object')) { + paths.push(new UrlPathWithParams(curr, stringify(next))); + i += 2; + } else { + paths.push(new UrlPathWithParams(curr, {})); + i++; + } + } + return new UrlSegment(paths, {}); } function stringify(params: {[key: string]: any}): {[key: string]: string} { @@ -203,15 +228,7 @@ function stringify(params: {[key: string]: any}): {[key: string]: string} { return res; } -function compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean { - return path == segment.path && shallowEqual(params, segment.parameters); -} - -function recurse( - urlSegment: UrlSegment, node: TreeNode| null, rest: any[]): TreeNode { - if (rest.length === 0) { - return new TreeNode(urlSegment, []); - } - const children = node ? node.children.slice(0) : []; - return new TreeNode(urlSegment, updateMany(children, rest)); +function compare( + path: string, params: {[key: string]: any}, pathWithParams: UrlPathWithParams): boolean { + return path == pathWithParams.path && shallowEqual(params, pathWithParams.parameters); } \ No newline at end of file diff --git a/modules/@angular/router/src/index.ts b/modules/@angular/router/src/index.ts index 2c7ddf0d58..96ed94237e 100644 --- a/modules/@angular/router/src/index.ts +++ b/modules/@angular/router/src/index.ts @@ -10,6 +10,6 @@ export {provideRouter} from './router_providers'; export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state'; export {PRIMARY_OUTLET, Params} from './shared'; export {DefaultUrlSerializer, UrlSerializer} from './url_serializer'; -export {UrlSegment, UrlTree} from './url_tree'; +export {UrlPathWithParams, UrlTree} from './url_tree'; export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink]; \ No newline at end of file diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index b2cdd80511..cfed27e04b 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -2,171 +2,159 @@ import {Type} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {of } from 'rxjs/observable/of'; -import {match} from './apply_redirects'; import {Route, RouterConfig} from './config'; import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; import {PRIMARY_OUTLET} from './shared'; -import {UrlSegment, UrlTree} from './url_tree'; -import {first, flatten, forEach, merge} from './utils/collection'; +import {UrlPathWithParams, UrlSegment, UrlTree, mapChildrenIntoArray} from './url_tree'; +import {last, merge} from './utils/collection'; import {TreeNode} from './utils/tree'; -class CannotRecognize {} +class NoMatch { + constructor(public segment: UrlSegment = null) {} +} export function recognize( - rootComponentType: Type, config: RouterConfig, url: UrlTree): Observable { + rootComponentType: Type, config: RouterConfig, urlTree: UrlTree, + url: string): Observable { try { - const match = new MatchResult( - rootComponentType, config, [url.root], {}, url._root.children, [], PRIMARY_OUTLET, null, - url.root); - const roots = constructActivatedRoute(match); - return of (new RouterStateSnapshot(roots[0], url.queryParams, url.fragment)); + const children = processSegment(config, urlTree.root, PRIMARY_OUTLET); + const root = new ActivatedRouteSnapshot( + [], {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1); + const rootNode = new TreeNode(root, children); + return of (new RouterStateSnapshot(url, rootNode, urlTree.queryParams, urlTree.fragment)); } catch (e) { - if (e instanceof CannotRecognize) { + if (e instanceof NoMatch) { return new Observable( - obs => obs.error(new Error('Cannot match any routes'))); + obs => obs.error(new Error(`Cannot match any routes: '${e.segment}'`))); } else { return new Observable(obs => obs.error(e)); } } } -function constructActivatedRoute(match: MatchResult): TreeNode[] { - const activatedRoute = createActivatedRouteSnapshot(match); - const children = match.leftOverUrl.length > 0 ? - recognizeMany(match.children, match.leftOverUrl) : - recognizeLeftOvers(match.children, match.lastUrlSegment); +function processSegment( + config: Route[], segment: UrlSegment, outlet: string): TreeNode[] { + if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) { + return processSegmentChildren(config, segment); + } else { + return [processPathsWithParams(config, segment, 0, segment.pathsWithParams, outlet)]; + } +} + +function processSegmentChildren( + config: Route[], segment: UrlSegment): TreeNode[] { + const children = mapChildrenIntoArray( + segment, (child, childOutlet) => processSegment(config, child, childOutlet)); checkOutletNameUniqueness(children); - children.sort((a, b) => { + sortActivatedRouteSnapshots(children); + return children; +} + +function sortActivatedRouteSnapshots(nodes: TreeNode[]): void { + nodes.sort((a, b) => { if (a.value.outlet === PRIMARY_OUTLET) return -1; if (b.value.outlet === PRIMARY_OUTLET) return 1; return a.value.outlet.localeCompare(b.value.outlet); }); - return [new TreeNode(activatedRoute, children)]; } -function recognizeLeftOvers( - config: Route[], lastUrlSegment: UrlSegment): TreeNode[] { - if (!config) return []; - const mIndex = matchIndex(config, [], lastUrlSegment); - return mIndex ? constructActivatedRoute(mIndex) : []; -} - -function recognizeMany( - config: Route[], urls: TreeNode[]): TreeNode[] { - return flatten(urls.map(url => recognizeOne(config, url))); -} - -function createActivatedRouteSnapshot(match: MatchResult): ActivatedRouteSnapshot { - return new ActivatedRouteSnapshot( - match.consumedUrlSegments, match.parameters, match.outlet, match.component, match.route, - match.lastUrlSegment); -} - -function recognizeOne( - config: Route[], url: TreeNode): TreeNode[] { - const matches = matchNode(config, url); - for (let match of matches) { +function processPathsWithParams( + config: Route[], segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], + outlet: string): TreeNode { + for (let r of config) { try { - const primary = constructActivatedRoute(match); - const secondary = recognizeMany(config, match.secondary); - const res = primary.concat(secondary); - checkOutletNameUniqueness(res); - return res; + return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, outlet); } catch (e) { - if (!(e instanceof CannotRecognize)) { - throw e; - } + if (!(e instanceof NoMatch)) throw e; } } - throw new CannotRecognize(); + throw new NoMatch(segment); } -function checkOutletNameUniqueness(nodes: TreeNode[]): - TreeNode[] { - let names = {}; +function processPathsWithParamsAgainstRoute( + route: Route, segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[], + outlet: string): TreeNode { + if (route.redirectTo) throw new NoMatch(); + if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch(); + + if (route.path === '**') { + const params = paths.length > 0 ? last(paths).parameters : {}; + const snapshot = + new ActivatedRouteSnapshot(paths, params, outlet, route.component, route, segment, -1); + return new TreeNode(snapshot, []); + } + + const {consumedPaths, parameters, lastChild} = match(segment, route, paths); + + const snapshot = new ActivatedRouteSnapshot( + consumedPaths, parameters, outlet, route.component, route, segment, + pathIndex + lastChild - 1); + const slicedPath = paths.slice(lastChild); + const childConfig = route.children ? route.children : []; + + if (childConfig.length === 0 && slicedPath.length === 0) { + return new TreeNode(snapshot, []); + + // TODO: check that the right segment is present + } else if (slicedPath.length === 0 && Object.keys(segment.children).length > 0) { + const children = processSegmentChildren(childConfig, segment); + return new TreeNode(snapshot, children); + + } else { + const child = processPathsWithParams( + childConfig, segment, pathIndex + lastChild, slicedPath, PRIMARY_OUTLET); + return new TreeNode(snapshot, [child]); + } +} + +function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) { + if (route.index || route.path === '' || route.path === '/') { + if (route.terminal && (Object.keys(segment.children).length > 0 || paths.length > 0)) { + throw new NoMatch(); + } else { + return {consumedPaths: [], lastChild: 0, parameters: {}}; + } + } + + const path = route.path.startsWith('/') ? route.path.substring(1) : route.path; + const parts = path.split('/'); + const posParameters = {}; + const consumedPaths = []; + + let currentIndex = 0; + + for (let i = 0; i < parts.length; ++i) { + if (currentIndex >= paths.length) throw new NoMatch(); + const current = paths[currentIndex]; + + const p = parts[i]; + const isPosParam = p.startsWith(':'); + + if (!isPosParam && p !== current.path) throw new NoMatch(); + if (isPosParam) { + posParameters[p.substring(1)] = current.path; + } + consumedPaths.push(current); + currentIndex++; + } + + if (route.terminal && (Object.keys(segment.children).length > 0 || currentIndex < paths.length)) { + throw new NoMatch(); + } + + const parameters = merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters); + return {consumedPaths, lastChild: currentIndex, parameters}; +} + +function checkOutletNameUniqueness(nodes: TreeNode[]): void { + const names = {}; nodes.forEach(n => { let routeWithSameOutletName = names[n.value.outlet]; if (routeWithSameOutletName) { const p = routeWithSameOutletName.urlSegments.map(s => s.toString()).join('/'); - const c = n.value.urlSegments.map(s => s.toString()).join('/'); + const c = n.value.url.map(s => s.toString()).join('/'); throw new Error(`Two segments cannot have the same outlet name: '${p}' and '${c}'.`); } names[n.value.outlet] = n.value; }); - return nodes; -} - -function matchNode(config: Route[], url: TreeNode): MatchResult[] { - const res = []; - for (let r of config) { - const m = matchWithParts(r, url); - if (m) { - res.push(m); - } else if (r.index) { - res.push(createIndexMatch(r, [url], url.value)); - } - } - return res; -} - -function createIndexMatch( - r: Route, leftOverUrls: TreeNode[], lastUrlSegment: UrlSegment): MatchResult { - const outlet = r.outlet ? r.outlet : PRIMARY_OUTLET; - const children = r.children ? r.children : []; - return new MatchResult( - r.component, children, [], lastUrlSegment.parameters, leftOverUrls, [], outlet, r, - lastUrlSegment); -} - -function matchIndex( - config: Route[], leftOverUrls: TreeNode[], lastUrlSegment: UrlSegment): MatchResult| - null { - for (let r of config) { - if (r.index) { - return createIndexMatch(r, leftOverUrls, lastUrlSegment); - } - } - return null; -} - -function matchWithParts(route: Route, url: TreeNode): MatchResult|null { - if (!route.path) return null; - if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== url.value.outlet) return null; - - const path = route.path.startsWith('/') ? route.path.substring(1) : route.path; - if (path === '**') { - const consumedUrl = []; - let u: TreeNode|null = url; - while (u) { - consumedUrl.push(u.value); - u = first(u.children); - } - const last = consumedUrl[consumedUrl.length - 1]; - return new MatchResult( - route.component, [], consumedUrl, last.parameters, [], [], PRIMARY_OUTLET, route, last); - } - - const m = match(route, url); - if (!m) return null; - const {consumedUrlSegments, lastSegment, lastParent, positionalParamSegments} = m; - - const p = lastSegment.value.parameters; - const posParams = {}; - forEach(positionalParamSegments, (v, k) => { posParams[k] = v.path; }); - const parameters = <{[key: string]: string}>merge(p, posParams); - const secondarySubtrees = lastParent ? lastParent.children.slice(1) : []; - const children = route.children ? route.children : []; - const outlet = route.outlet ? route.outlet : PRIMARY_OUTLET; - - return new MatchResult( - route.component, children, consumedUrlSegments, parameters, lastSegment.children, - secondarySubtrees, outlet, route, lastSegment.value); -} - -class MatchResult { - constructor( - public component: Type|string, public children: Route[], - public consumedUrlSegments: UrlSegment[], public parameters: {[key: string]: string}, - public leftOverUrl: TreeNode[], public secondary: TreeNode[], - public outlet: string, public route: Route|null, public lastUrlSegment: UrlSegment) {} } \ No newline at end of file diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 845905ffe1..402c4e0e56 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -39,7 +39,7 @@ export interface NavigationExtras { * An event triggered when a navigation starts */ export class NavigationStart { - constructor(public id: number, public url: UrlTree) {} + constructor(public id: number, public url: string) {} toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; } } @@ -48,16 +48,18 @@ export class NavigationStart { * An event triggered when a navigation ends successfully */ export class NavigationEnd { - constructor(public id: number, public url: UrlTree) {} + constructor(public id: number, public url: string, public urlAfterRedirects: string) {} - toString(): string { return `NavigationEnd(id: ${this.id}, url: '${this.url}')`; } + toString(): string { + return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`; + } } /** * An event triggered when a navigation is canceled */ export class NavigationCancel { - constructor(public id: number, public url: UrlTree) {} + constructor(public id: number, public url: string) {} toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; } } @@ -66,7 +68,7 @@ export class NavigationCancel { * An event triggered when a navigation fails due to unexpected error */ export class NavigationError { - constructor(public id: number, public url: UrlTree, public error: any) {} + constructor(public id: number, public url: string, public error: any) {} toString(): string { return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`; @@ -78,7 +80,7 @@ export class NavigationError { */ export class RoutesRecognized { constructor( - public id: number, public url: UrlTree, public urlAfterRedirects: UrlTree, + public id: number, public url: string, public urlAfterRedirects: string, public state: RouterStateSnapshot) {} toString(): string { @@ -107,7 +109,7 @@ export class Router { private location: Location, private injector: Injector, private config: RouterConfig) { this.routerEvents = new Subject(); this.currentUrlTree = createEmptyUrlTree(); - this.currentRouterState = createEmptyState(this.rootComponentType); + this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType); } /** @@ -124,9 +126,9 @@ export class Router { get routerState(): RouterState { return this.currentRouterState; } /** - * Returns the current url tree. + * Returns the current url. */ - get urlTree(): UrlTree { return this.currentUrlTree; } + get url(): string { return this.serializeUrl(this.currentUrlTree); } /** * Returns an observable of route events @@ -210,7 +212,6 @@ export class Router { return createUrlTree(a, this.currentUrlTree, commands, queryParams, fragment); } - /** * Navigate based on the provided array of commands and a starting point. * If no starting route is provided, the navigation is absolute. @@ -242,7 +243,7 @@ export class Router { private scheduleNavigation(url: UrlTree, pop: boolean): Promise { const id = ++this.navigationId; - this.routerEvents.next(new NavigationStart(id, url)); + this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url))); return Promise.resolve().then((_) => this.runNavigate(url, false, id)); } @@ -255,7 +256,7 @@ export class Router { private runNavigate(url: UrlTree, pop: boolean, id: number): Promise { if (id !== this.navigationId) { this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); - this.routerEvents.next(new NavigationCancel(id, url)); + this.routerEvents.next(new NavigationCancel(id, this.serializeUrl(url))); return Promise.resolve(false); } @@ -265,12 +266,13 @@ export class Router { applyRedirects(url, this.config) .mergeMap(u => { updatedUrl = u; - return recognize(this.rootComponentType, this.config, updatedUrl); + return recognize( + this.rootComponentType, this.config, updatedUrl, this.serializeUrl(updatedUrl)); }) .mergeMap((newRouterStateSnapshot) => { - this.routerEvents.next( - new RoutesRecognized(id, url, updatedUrl, newRouterStateSnapshot)); + this.routerEvents.next(new RoutesRecognized( + id, this.serializeUrl(url), this.serializeUrl(updatedUrl), newRouterStateSnapshot)); return resolve(this.resolver, newRouterStateSnapshot); }) @@ -290,13 +292,13 @@ export class Router { .forEach((shouldActivate) => { if (!shouldActivate || id !== this.navigationId) { this.location.go(this.urlSerializer.serialize(this.currentUrlTree)); - this.routerEvents.next(new NavigationCancel(id, url)); + this.routerEvents.next(new NavigationCancel(id, this.serializeUrl(url))); return Promise.resolve(false); } new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap); - this.currentUrlTree = url; + this.currentUrlTree = updatedUrl; this.currentRouterState = state; if (!pop) { this.location.go(this.urlSerializer.serialize(updatedUrl)); @@ -304,12 +306,13 @@ export class Router { }) .then( () => { - this.routerEvents.next(new NavigationEnd(id, url)); + this.routerEvents.next( + new NavigationEnd(id, this.serializeUrl(url), this.serializeUrl(updatedUrl))); resolvePromise(true); }, e => { - this.routerEvents.next(new NavigationError(id, url, e)); + this.routerEvents.next(new NavigationError(id, this.serializeUrl(url), e)); rejectPromise(e); }); }); @@ -380,9 +383,11 @@ class GuardChecks { private deactivateOutletAndItChildren(route: ActivatedRouteSnapshot, outlet: RouterOutlet): void { if (outlet && outlet.isActivated) { - forEach( - outlet.outletMap._outlets, - (v, k) => this.deactivateOutletAndItChildren(v.activatedRoute.snapshot, v)); + forEach(outlet.outletMap._outlets, (v, k) => { + if (v.isActivated) { + this.deactivateOutletAndItChildren(v.activatedRoute.snapshot, v); + } + }); this.checks.push(new CanDeactivate(outlet.component, route)); } } @@ -455,6 +460,7 @@ class ActivateRoutes { parentOutletMap: RouterOutletMap): void { const future = futureNode.value; const curr = currNode ? currNode.value : null; + const outlet = getOutlet(parentOutletMap, futureNode.value); if (future === curr) { @@ -506,10 +512,11 @@ function nodeChildrenAsMap(node: TreeNode| null) { function getOutlet(outletMap: RouterOutletMap, route: ActivatedRoute): RouterOutlet { let outlet = outletMap._outlets[route.outlet]; if (!outlet) { + const componentName = (route.component).name; if (route.outlet === PRIMARY_OUTLET) { - throw new Error(`Cannot find primary outlet`); + throw new Error(`Cannot find primary outlet to load '${componentName}'`); } else { - throw new Error(`Cannot find the outlet ${route.outlet}`); + throw new Error(`Cannot find the outlet ${route.outlet} to load '${componentName}'`); } } return outlet; diff --git a/modules/@angular/router/src/router_state.ts b/modules/@angular/router/src/router_state.ts index 0331602b1f..2c9a6a0b4f 100644 --- a/modules/@angular/router/src/router_state.ts +++ b/modules/@angular/router/src/router_state.ts @@ -4,7 +4,7 @@ import {Observable} from 'rxjs/Observable'; import {Route} from './config'; import {PRIMARY_OUTLET, Params} from './shared'; -import {UrlSegment} from './url_tree'; +import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree'; import {shallowEqual} from './utils/collection'; import {Tree, TreeNode} from './utils/tree'; @@ -37,9 +37,9 @@ export class RouterState extends Tree { toString(): string { return this.snapshot.toString(); } } -export function createEmptyState(rootComponent: Type): RouterState { - const snapshot = createEmptyStateSnapshot(rootComponent); - const emptyUrl = new BehaviorSubject([new UrlSegment('', {}, PRIMARY_OUTLET)]); +export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterState { + const snapshot = createEmptyStateSnapshot(urlTree, rootComponent); + const emptyUrl = new BehaviorSubject([new UrlPathWithParams('', {})]); const emptyParams = new BehaviorSubject({}); const emptyQueryParams = new BehaviorSubject({}); const fragment = new BehaviorSubject(''); @@ -50,16 +50,14 @@ export function createEmptyState(rootComponent: Type): RouterState { new TreeNode(activated, []), emptyQueryParams, fragment, snapshot); } -function createEmptyStateSnapshot(rootComponent: Type): RouterStateSnapshot { - const rootUrlSegment = new UrlSegment('', {}, PRIMARY_OUTLET); - const emptyUrl = [rootUrlSegment]; +function createEmptyStateSnapshot(urlTree: UrlTree, rootComponent: Type): RouterStateSnapshot { const emptyParams = {}; const emptyQueryParams = {}; const fragment = ''; const activated = new ActivatedRouteSnapshot( - emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, null, rootUrlSegment); + [], emptyParams, PRIMARY_OUTLET, rootComponent, null, urlTree.root, -1); return new RouterStateSnapshot( - new TreeNode(activated, []), emptyQueryParams, fragment); + '', new TreeNode(activated, []), emptyQueryParams, fragment); } /** @@ -86,7 +84,7 @@ export class ActivatedRoute { * @internal */ constructor( - public urlSegments: Observable, public params: Observable, + public url: Observable, public params: Observable, public outlet: string, public component: Type|string, futureSnapshot: ActivatedRouteSnapshot) { this._futureSnapshot = futureSnapshot; @@ -120,20 +118,24 @@ export class ActivatedRouteSnapshot { _routeConfig: Route|null; /** @internal **/ - _lastUrlSegment: UrlSegment; + _urlSegment: UrlSegment; + + _lastPathIndex: number; /** * @internal */ constructor( - public urlSegments: UrlSegment[], public params: Params, public outlet: string, - public component: Type|string, routeConfig: Route|null, lastUrlSegment: UrlSegment) { + public url: UrlPathWithParams[], public params: Params, public outlet: string, + public component: Type|string, routeConfig: Route|null, urlSegment: UrlSegment, + lastPathIndex: number) { this._routeConfig = routeConfig; - this._lastUrlSegment = lastUrlSegment; + this._urlSegment = urlSegment; + this._lastPathIndex = lastPathIndex; } toString(): string { - const url = this.urlSegments.map(s => s.toString()).join('/'); + const url = this.url.map(s => s.toString()).join('/'); const matched = this._routeConfig ? this._routeConfig.path : ''; return `Route(url:'${url}', path:'${matched}')`; } @@ -157,7 +159,7 @@ export class RouterStateSnapshot extends Tree { * @internal */ constructor( - root: TreeNode, public queryParams: Params, + public url: string, root: TreeNode, public queryParams: Params, public fragment: string|null) { super(root); } @@ -179,7 +181,7 @@ function serializeNode(node: TreeNode): string { export function advanceActivatedRoute(route: ActivatedRoute): void { if (route.snapshot && !shallowEqual(route.snapshot.params, route._futureSnapshot.params)) { route.snapshot = route._futureSnapshot; - (route.urlSegments).next(route.snapshot.urlSegments); + (route.url).next(route.snapshot.url); (route.params).next(route.snapshot.params); } else { route.snapshot = route._futureSnapshot; diff --git a/modules/@angular/router/src/url_serializer.ts b/modules/@angular/router/src/url_serializer.ts index b5b36faa67..311485a180 100644 --- a/modules/@angular/router/src/url_serializer.ts +++ b/modules/@angular/router/src/url_serializer.ts @@ -1,6 +1,7 @@ import {PRIMARY_OUTLET} from './shared'; -import {UrlSegment, UrlTree} from './url_tree'; -import {TreeNode} from './utils/tree'; +import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree'; +import {forEach} from './utils/collection'; + /** @@ -28,37 +29,65 @@ export class DefaultUrlSerializer implements UrlSerializer { } serialize(tree: UrlTree): string { - const node = serializeUrlTreeNode(tree._root); + const segment = `/${serializeSegment(tree.root, true)}`; const query = serializeQueryParams(tree.queryParams); const fragment = tree.fragment !== null ? `#${tree.fragment}` : ''; - return `${node}${query}${fragment}`; + return `${segment}${query}${fragment}`; } } -function serializeUrlTreeNode(node: TreeNode): string { - return `${serializeSegment(node.value)}${serializeChildren(node)}`; +export function serializePaths(segment: UrlSegment): string { + return segment.pathsWithParams.map(p => serializePath(p)).join('/'); } -function serializeUrlTreeNodes(nodes: TreeNode[]): string { - const primary = serializeSegment(nodes[0].value); - const secondaryNodes = nodes.slice(1); - const secondary = - secondaryNodes.length > 0 ? `(${secondaryNodes.map(serializeUrlTreeNode).join("//")})` : ''; - const children = serializeChildren(nodes[0]); - return `${primary}${secondary}${children}`; +function serializeSegment(segment: UrlSegment, root: boolean): string { + if (segment.children[PRIMARY_OUTLET] && root) { + const primary = serializeSegment(segment.children[PRIMARY_OUTLET], false); + const children = []; + forEach(segment.children, (v, k) => { + if (k !== PRIMARY_OUTLET) { + children.push(`${k}:${serializeSegment(v, false)}`); + } + }); + if (children.length > 0) { + return `${primary}(${children.join('//')})`; + } else { + return `${primary}`; + } + } else if (segment.children[PRIMARY_OUTLET] && !root) { + const children = [serializeSegment(segment.children[PRIMARY_OUTLET], false)]; + forEach(segment.children, (v, k) => { + if (k !== PRIMARY_OUTLET) { + children.push(`${k}:${serializeSegment(v, false)}`); + } + }); + return `${serializePaths(segment)}/(${children.join('//')})`; + } else { + return serializePaths(segment); + } } -function serializeChildren(node: TreeNode): string { - if (node.children.length > 0) { - return `/${serializeUrlTreeNodes(node.children)}`; +function serializeChildren(segment: UrlSegment) { + if (segment.children[PRIMARY_OUTLET]) { + const primary = serializePaths(segment.children[PRIMARY_OUTLET]); + + const secondary = []; + forEach(segment.children, (v, k) => { + if (k !== PRIMARY_OUTLET) { + secondary.push(`${k}:${serializePaths(v)}${serializeChildren(v)}`); + } + }); + const secondaryStr = secondary.length > 0 ? `(${secondary.join('//')})` : ''; + const primaryChildren = serializeChildren(segment.children[PRIMARY_OUTLET]); + const primaryChildrenStr = primaryChildren ? `/${primaryChildren}` : ''; + return `${primary}${secondaryStr}${primaryChildrenStr}`; } else { return ''; } } -export function serializeSegment(segment: UrlSegment): string { - const outlet = segment.outlet === PRIMARY_OUTLET ? '' : `${segment.outlet}:`; - return `${outlet}${segment.path}${serializeParams(segment.parameters)}`; +export function serializePath(path: UrlPathWithParams): string { + return `${path.path}${serializeParams(path.parameters)}`; } function serializeParams(params: {[key: string]: string}): string { @@ -84,7 +113,7 @@ function pairs(obj: {[key: string]: T}): Pair[] { } const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/; -function matchUrlSegment(str: string): string { +function matchPathWithParams(str: string): string { SEGMENT_RE.lastIndex = 0; var match = SEGMENT_RE.exec(str); return match ? match[0] : ''; @@ -109,61 +138,53 @@ class UrlParser { this.remaining = this.remaining.substring(str.length); } - parseRootSegment(): TreeNode { - if (this.remaining == '' || this.remaining == '/') { - return new TreeNode(new UrlSegment('', {}, PRIMARY_OUTLET), []); + parseRootSegment(): UrlSegment { + if (this.remaining === '' || this.remaining === '/') { + return new UrlSegment([], {}); } else { - const segments = this.parseSegments(false); - return new TreeNode(new UrlSegment('', {}, PRIMARY_OUTLET), segments); + return new UrlSegment([], this.parseSegmentChildren()); } } - parseSegments(hasOutletName: boolean): TreeNode[] { + parseSegmentChildren(): {[key: string]: UrlSegment} { if (this.remaining.length == 0) { - return []; + return {}; } + if (this.peekStartsWith('/')) { this.capture('/'); } - let path = matchUrlSegment(this.remaining); - this.capture(path); - let outletName; - if (hasOutletName) { - if (path.indexOf(':') === -1) { - throw new Error('Not outlet name is provided'); - } - if (path.indexOf(':') > -1 && hasOutletName) { - let parts = path.split(':'); - outletName = parts[0]; - path = parts[1]; - } - } else { - if (path.indexOf(':') > -1) { - throw new Error('Not outlet name is allowed'); - } - outletName = PRIMARY_OUTLET; + const paths = [this.parsePathWithParams()]; + + while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) { + this.capture('/'); + paths.push(this.parsePathWithParams()); } + let children = {}; + if (this.peekStartsWith('/(')) { + this.capture('/'); + children = this.parseParens(true); + } + + let res: {[key: string]: UrlSegment} = {}; + if (this.peekStartsWith('(')) { + res = this.parseParens(false); + } + + res[PRIMARY_OUTLET] = new UrlSegment(paths, children); + return res; + } + + parsePathWithParams(): UrlPathWithParams { + let path = matchPathWithParams(this.remaining); + this.capture(path); let matrixParams: {[key: string]: any} = {}; if (this.peekStartsWith(';')) { matrixParams = this.parseMatrixParams(); } - - let secondary = []; - if (this.peekStartsWith('(')) { - secondary = this.parseSecondarySegments(); - } - - let children: TreeNode[] = []; - if (this.peekStartsWith('/') && !this.peekStartsWith('//')) { - this.capture('/'); - children = this.parseSegments(false); - } - - const segment = new UrlSegment(path, matrixParams, outletName); - const node = new TreeNode(segment, children); - return [node].concat(secondary); + return new UrlPathWithParams(path, matrixParams); } parseQueryParams(): {[key: string]: any} { @@ -197,7 +218,7 @@ class UrlParser { } parseParam(params: {[key: string]: any}): void { - var key = matchUrlSegment(this.remaining); + var key = matchPathWithParams(this.remaining); if (!key) { return; } @@ -205,7 +226,7 @@ class UrlParser { var value: any = 'true'; if (this.peekStartsWith('=')) { this.capture('='); - var valueMatch = matchUrlSegment(this.remaining); + var valueMatch = matchPathWithParams(this.remaining); if (valueMatch) { value = valueMatch; this.capture(value); @@ -216,7 +237,7 @@ class UrlParser { } parseQueryParam(params: {[key: string]: any}): void { - var key = matchUrlSegment(this.remaining); + var key = matchPathWithParams(this.remaining); if (!key) { return; } @@ -233,12 +254,25 @@ class UrlParser { params[key] = value; } - parseSecondarySegments(): TreeNode[] { - var segments = []; + parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} { + const segments = {}; this.capture('('); while (!this.peekStartsWith(')') && this.remaining.length > 0) { - segments = segments.concat(this.parseSegments(true)); + let path = matchPathWithParams(this.remaining); + let outletName; + if (path.indexOf(':') > -1) { + outletName = path.substr(0, path.indexOf(':')); + this.capture(outletName); + this.capture(':'); + } else if (allowPrimary) { + outletName = PRIMARY_OUTLET; + } + + const children = this.parseSegmentChildren(); + segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] : + new UrlSegment([], children); + if (this.peekStartsWith('//')) { this.capture('//'); } diff --git a/modules/@angular/router/src/url_tree.ts b/modules/@angular/router/src/url_tree.ts index 5c765b9222..cc981f6fab 100644 --- a/modules/@angular/router/src/url_tree.ts +++ b/modules/@angular/router/src/url_tree.ts @@ -1,40 +1,41 @@ import {PRIMARY_OUTLET} from './shared'; -import {DefaultUrlSerializer, serializeSegment} from './url_serializer'; -import {shallowEqual} from './utils/collection'; -import {Tree, TreeNode} from './utils/tree'; +import {DefaultUrlSerializer, serializePath, serializePaths} from './url_serializer'; +import {forEach, shallowEqual} from './utils/collection'; export function createEmptyUrlTree() { - return new UrlTree( - new TreeNode(new UrlSegment('', {}, PRIMARY_OUTLET), []), {}, null); + return new UrlTree(new UrlSegment([], {}), {}, null); } /** * A URL in the tree form. */ -export class UrlTree extends Tree { +export class UrlTree { /** * @internal */ constructor( - root: TreeNode, public queryParams: {[key: string]: string}, - public fragment: string|null) { - super(root); - } + public root: UrlSegment, public queryParams: {[key: string]: string}, + public fragment: string|null) {} toString(): string { return new DefaultUrlSerializer().serialize(this); } } export class UrlSegment { - /** - * @internal - */ + public parent: UrlSegment|null = null; constructor( - public path: string, public parameters: {[key: string]: string}, public outlet: string) {} + public pathsWithParams: UrlPathWithParams[], public children: {[key: string]: UrlSegment}) { + forEach(children, (v, k) => v.parent = this); + } - toString(): string { return serializeSegment(this); } + toString(): string { return serializePaths(this); } } -export function equalUrlSegments(a: UrlSegment[], b: UrlSegment[]): boolean { +export class UrlPathWithParams { + constructor(public path: string, public parameters: {[key: string]: string}) {} + toString(): string { return serializePath(this); } +} + +export function equalPathsWithParams(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; ++i) { if (a[i].path !== b[i].path) return false; @@ -42,3 +43,35 @@ export function equalUrlSegments(a: UrlSegment[], b: UrlSegment[]): boolean { } return true; } + +export function mapChildren(segment: UrlSegment, fn: (v: UrlSegment, k: string) => UrlSegment): + {[name: string]: UrlSegment} { + const newChildren = {}; + forEach(segment.children, (child, childOutlet) => { + if (childOutlet === PRIMARY_OUTLET) { + newChildren[childOutlet] = fn(child, childOutlet); + } + }); + forEach(segment.children, (child, childOutlet) => { + if (childOutlet !== PRIMARY_OUTLET) { + newChildren[childOutlet] = fn(child, childOutlet); + } + }); + return newChildren; +} + +export function mapChildrenIntoArray( + segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] { + let res = []; + forEach(segment.children, (child, childOutlet) => { + if (childOutlet === PRIMARY_OUTLET) { + res = res.concat(fn(child, childOutlet)); + } + }); + forEach(segment.children, (child, childOutlet) => { + if (childOutlet !== PRIMARY_OUTLET) { + res = res.concat(fn(child, childOutlet)); + } + }); + return res; +} diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index ba22116ef6..aea85b2a23 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -1,30 +1,32 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; import {TreeNode} from '../src/utils/tree'; -import {UrlTree, UrlSegment, equalUrlSegments} from '../src/url_tree'; -import {Params, PRIMARY_OUTLET} from '../src/shared'; +import {UrlTree, UrlSegment, equalPathsWithParams} from '../src/url_tree'; +import {RouterConfig} from '../src/config'; import {applyRedirects} from '../src/apply_redirects'; describe('applyRedirects', () => { it("should return the same url tree when no redirects", () => { - applyRedirects(tree("/a/b"), [ + checkRedirect([ {path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]} - ]).forEach(t => { + ], "/a/b", t => { compareTrees(t, tree('/a/b')); }); }); it("should add new segments when needed", () => { - applyRedirects(tree("/a/b"), [ - {path: 'a/b', redirectTo: 'a/b/c'} - ]).forEach(t => { + checkRedirect([ + {path: 'a/b', redirectTo: 'a/b/c'}, + {path: '**', component: ComponentC} + ], "/a/b", t => { compareTrees(t, tree('/a/b/c')); }); }); it("should handle positional parameters", () => { - applyRedirects(tree("/a/1/b/2"), [ - {path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'} - ]).forEach(t => { + checkRedirect([ + {path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'}, + {path: '**', component: ComponentC} + ], "/a/1/b/2", t => { compareTrees(t, tree('/newa/1/newb/2')); }); }); @@ -38,50 +40,122 @@ describe('applyRedirects', () => { }); it("should pass matrix parameters", () => { - applyRedirects(tree("/a;p1=1/1;p2=2"), [ - {path: 'a/:id', redirectTo: 'd/a/:id/e'} - ]).forEach(t => { + checkRedirect([ + {path: 'a/:id', redirectTo: 'd/a/:id/e'}, + {path: '**', component: ComponentC} + ], "/a;p1=1/1;p2=2", t => { compareTrees(t, tree('/d/a;p1=1/1;p2=2/e')); }); }); it("should handle preserve secondary routes", () => { - applyRedirects(tree("/a/1(aux:c/d)"), [ + checkRedirect([ {path: 'a/:id', redirectTo: 'd/a/:id/e'}, - {path: 'c/d', component: ComponentA, outlet: 'aux'} - ]).forEach(t => { + {path: 'c/d', component: ComponentA, outlet: 'aux'}, + {path: '**', component: ComponentC} + ], "/a/1(aux:c/d)", t => { compareTrees(t, tree('/d/a/1/e(aux:c/d)')); }); }); it("should redirect secondary routes", () => { - applyRedirects(tree("/a/1(aux:c/d)"), [ + checkRedirect([ {path: 'a/:id', component: ComponentA}, - {path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'} - ]).forEach(t => { + {path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'}, + {path: '**', component: ComponentC, outlet: 'aux'} + ], "/a/1(aux:c/d)", t => { compareTrees(t, tree('/a/1(aux:f/c/d/e)')); }); }); + it("should use the configuration of the route redirected to", () => { + checkRedirect([ + {path: 'a', component: ComponentA, children: [ + {path: 'b', component: ComponentB}, + ]}, + {path: 'c', redirectTo: 'a'} + ], "c/b", t => { + compareTrees(t, tree('a/b')); + }); + }); + + it("should redirect empty path", () => { + checkRedirect([ + {path: 'a', component: ComponentA, children: [ + {path: 'b', component: ComponentB}, + ]}, + {path: '', redirectTo: 'a'} + ], "b", t => { + compareTrees(t, tree('a/b')); + }); + }); + + xit("should support nested redirects", () => { + checkRedirect([ + {path: 'a', component: ComponentA, children: [ + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'} + ]}, + {path: '', redirectTo: 'a'} + ], "", t => { + compareTrees(t, tree('a/b')); + }); + }); + + xit("should support nested redirects (when redirected to an empty path)", () => { + checkRedirect([ + {path: '', component: ComponentA, children: [ + {path: 'b', component: ComponentB}, + {path: '', redirectTo: 'b'} + ]}, + {path: 'a', redirectTo: ''} + ], "a", t => { + compareTrees(t, tree('b')); + }); + }); + + it("should redirect empty path route only when terminal", () => { + const config = [ + {path: 'a', component: ComponentA, children: [ + {path: 'b', component: ComponentB}, + ]}, + {path: '', redirectTo: 'a', terminal: true} + ]; + + applyRedirects(tree("b"), config).subscribe((_) => { + throw "Should not be reached"; + }, e => { + expect(e.message).toEqual("Cannot match any routes: 'b'"); + }); + }); + it("should redirect wild cards", () => { - applyRedirects(tree("/a/1(aux:c/d)"), [ + checkRedirect([ + {path: '404', component: ComponentA}, {path: '**', redirectTo: '/404'}, - ]).forEach(t => { + ], "/a/1(aux:c/d)", t => { compareTrees(t, tree('/404')); }); }); it("should support global redirects", () => { - applyRedirects(tree("/a/b/1"), [ + checkRedirect([ {path: 'a', component: ComponentA, children: [ {path: 'b/:id', redirectTo: '/global/:id'} ]}, - ]).forEach(t => { + {path: '**', component: ComponentC} + ], "/a/b/1", t => { compareTrees(t, tree('/global/1')); }); }); }); +function checkRedirect(config: RouterConfig, url: string, callback: any): void { + applyRedirects(tree(url), config).subscribe(callback, e => { + throw e; + }); +} + function tree(url: string): UrlTree { return new DefaultUrlSerializer().parse(url); } @@ -89,19 +163,18 @@ function tree(url: string): UrlTree { function compareTrees(actual: UrlTree, expected: UrlTree): void{ const serializer = new DefaultUrlSerializer(); const error = `"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`; - compareNode(actual._root, expected._root, error); + compareSegments(actual.root, expected.root, error); } -function compareNode(actual: TreeNode, expected: TreeNode, error: string): void{ - expect(equalUrlSegments([actual.value], [expected.value])).toEqual(true, error); +function compareSegments(actual: UrlSegment, expected: UrlSegment, error: string): void{ + expect(actual).toBeDefined(error); + expect(equalPathsWithParams(actual.pathsWithParams, expected.pathsWithParams)).toEqual(true, error); - expect(actual.children.length).toEqual(expected.children.length, error); + expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, error); - if (actual.children.length === expected.children.length) { - for (let i = 0; i < actual.children.length; ++i) { - compareNode(actual.children[i], expected.children[i], error); - } - } + Object.keys(expected.children).forEach(key => { + compareSegments(actual.children[key], expected.children[key], error); + }); } class ComponentA {} diff --git a/modules/@angular/router/test/create_router_state.spec.ts b/modules/@angular/router/test/create_router_state.spec.ts index 1536f33a59..c2d1a16ec1 100644 --- a/modules/@angular/router/test/create_router_state.spec.ts +++ b/modules/@angular/router/test/create_router_state.spec.ts @@ -1,5 +1,5 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlTree} from '../src/url_tree'; +import {UrlTree, UrlSegment} from '../src/url_tree'; import {TreeNode} from '../src/utils/tree'; import {Params, PRIMARY_OUTLET} from '../src/shared'; import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState, advanceActivatedRoute} from '../src/router_state'; @@ -8,7 +8,7 @@ import {recognize} from '../src/recognize'; import {RouterConfig} from '../src/config'; describe('create router state', () => { - const emptyState = () => createEmptyState(RootComponent); + const emptyState = () => createEmptyState(new UrlTree(new UrlSegment([], {}), {}, null), RootComponent); it('should work create new state', () => { const state = createRouterState(createState([ @@ -57,7 +57,7 @@ function advanceNode(node: TreeNode): void { function createState(config: RouterConfig, url: string): RouterStateSnapshot { let res; - recognize(RootComponent, config, tree(url)).forEach(s => res = s); + recognize(RootComponent, config, tree(url), url).forEach(s => res = s); return res; } diff --git a/modules/@angular/router/test/create_url_tree.spec.ts b/modules/@angular/router/test/create_url_tree.spec.ts index f58984f888..5a602fe9e8 100644 --- a/modules/@angular/router/test/create_url_tree.spec.ts +++ b/modules/@angular/router/test/create_url_tree.spec.ts @@ -1,5 +1,5 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; -import {UrlTree, UrlSegment} from '../src/url_tree'; +import {UrlTree, UrlPathWithParams, UrlSegment} from '../src/url_tree'; import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state'; import {PRIMARY_OUTLET, Params} from '../src/shared'; import {createUrlTree} from '../src/create_url_tree'; @@ -10,185 +10,163 @@ describe('createUrlTree', () => { it("should navigate to the root", () => { const p = serializer.parse("/"); - const t = create(p.root, p, ["/"]); - expect(serializer.serialize(t)).toEqual(""); + const t = createRoot(p, ["/"]); + expect(serializer.serialize(t)).toEqual("/"); }); it("should support nested segments", () => { const p = serializer.parse("/a/b"); - const t = create(p.root, p, ["/one", 11, "two", 22]); + const t = createRoot(p, ["/one", 11, "two", 22]); expect(serializer.serialize(t)).toEqual("/one/11/two/22"); }); it("should preserve secondary segments", () => { const p = serializer.parse("/a/11/b(right:c)"); - const t = create(p.root, p, ["/a", 11, 'd']); + const t = createRoot(p, ["/a", 11, 'd']); expect(serializer.serialize(t)).toEqual("/a/11/d(right:c)"); }); it("should support updating secondary segments", () => { const p = serializer.parse("/a(right:b)"); - const t = create(p.root, p, ["right:c", 11, 'd']); - expect(t.children(t.root)[1].outlet).toEqual("right"); + const t = createRoot(p, ["right:c", 11, 'd']); expect(serializer.serialize(t)).toEqual("/a(right:c/11/d)"); }); it("should support updating secondary segments (nested case)", () => { - const p = serializer.parse("/a/b(right:c)"); - const t = create(p.root, p, ["a", "right:d", 11, 'e']); - expect(serializer.serialize(t)).toEqual("/a/b(right:d/11/e)"); + const p = serializer.parse("/a/(b//right:c)"); + const t = createRoot(p, ["a", "right:d", 11, 'e']); + expect(serializer.serialize(t)).toEqual("/a/(b//right:d/11/e)"); }); it('should update matrix parameters', () => { - const p = serializer.parse("/a;aa=11"); - const t = create(p.root, p, ["/a", {aa: 22, bb: 33}]); - expect(serializer.serialize(t)).toEqual("/a;aa=22;bb=33"); + const p = serializer.parse("/a;pp=11"); + const t = createRoot(p, ["/a", {pp: 22, dd: 33}]); + expect(serializer.serialize(t)).toEqual("/a;pp=22;dd=33"); }); it('should create matrix parameters', () => { const p = serializer.parse("/a"); - const t = create(p.root, p, ["/a", {aa: 22, bb: 33}]); - expect(serializer.serialize(t)).toEqual("/a;aa=22;bb=33"); + const t = createRoot(p, ["/a", {pp: 22, dd: 33}]); + expect(serializer.serialize(t)).toEqual("/a;pp=22;dd=33"); }); it('should create matrix parameters together with other segments', () => { const p = serializer.parse("/a"); - const t = create(p.root, p, ["/a", "/b", {aa: 22, bb: 33}]); + const t = createRoot(p, ["/a", "/b", {aa: 22, bb: 33}]); expect(serializer.serialize(t)).toEqual("/a/b;aa=22;bb=33"); }); - describe("node reuse", () => { - it('should reuse nodes when path is the same', () => { - const p = serializer.parse("/a/b"); - const t = create(p.root, p, ['/a/c']); - - expect(t.root).toBe(p.root); - expect(t.firstChild(t.root)).toBe(p.firstChild(p.root)); - expect(t.firstChild(t.firstChild(t.root))).not.toBe(p.firstChild(p.firstChild(p.root))); - }); - - it("should create new node when params are the same", () => { - const p = serializer.parse("/a;x=1"); - const t = create(p.root, p, ['/a', {'x': 1}]); - - expect(t.firstChild(t.root)).toBe(p.firstChild(p.root)); - }); - - it("should create new node when params are different", () => { - const p = serializer.parse("/a;x=1"); - const t = create(p.root, p, ['/a', {'x': 2}]); - - expect(t.firstChild(t.root)).not.toBe(p.firstChild(p.root)); - }); - }); - describe("relative navigation", () => { it("should work", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - const t = create(c, p, ["c2"]); - expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)"); + const p = serializer.parse("/a/(c//left:cp)(left:ap)"); + const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ["c2"]); + expect(serializer.serialize(t)).toEqual("/a/(c2//left:cp)(left:ap)"); }); it("should work when the first command starts with a ./", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - const t = create(c, p, ["./c2"]); - expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)"); + const p = serializer.parse("/a/(c//left:cp)(left:ap)"); + const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ["./c2"]); + expect(serializer.serialize(t)).toEqual("/a/(c2//left:cp)(left:ap)"); }); it("should work when the first command is ./)", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - const t = create(c, p, ["./", "c2"]); - expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)"); + const p = serializer.parse("/a/(c//left:cp)(left:ap)"); + const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ["./", "c2"]); + expect(serializer.serialize(t)).toEqual("/a/(c2//left:cp)(left:ap)"); }); it("should work when given params", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - const t = create(c, p, [{'x': 99}]); - expect(serializer.serialize(t)).toEqual("/a(left:ap)/c;x=99(left:cp)"); + const p = serializer.parse("/a/(c//left:cp)(left:ap)"); + const t = create(p.root.children[PRIMARY_OUTLET], 0, p, [{'x': 99}]); + expect(serializer.serialize(t)).toEqual("/a/(c;x=99//left:cp)(left:ap)"); }); - it("should support going to a parent", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - const t = create(c, p, ["../a2"]); - expect(serializer.serialize(t)).toEqual("/a2(left:ap)"); - }); - - it("should support going to a parent (nested case)", () => { + it("should work when index > 0", () => { const p = serializer.parse("/a/c"); - const c = p.firstChild(p.firstChild(p.root)); - const t = create(c, p, ["../c2"]); + const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ["c2"]); + expect(serializer.serialize(t)).toEqual("/a/c/c2"); + }); + + it("should support going to a parent (within a segment)", () => { + const p = serializer.parse("/a/c"); + const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ["../c2"]); expect(serializer.serialize(t)).toEqual("/a/c2"); }); it("should work when given ../", () => { const p = serializer.parse("/a/c"); - const c = p.firstChild(p.firstChild(p.root)); - const t = create(c, p, ["../"]); - expect(serializer.serialize(t)).toEqual("/a"); - }); - - it("should navigate to the root", () => { - const p = serializer.parse("/a/c"); - const c = p.firstChild(p.root); - const t = create(c, p, ["../"]); - expect(serializer.serialize(t)).toEqual(""); + const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ["../", "c2"]); + expect(serializer.serialize(t)).toEqual("/a/c2"); }); it("should support setting matrix params", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - const t = create(c, p, ["../", {'x': 5}]); + const p = serializer.parse("/a/(c//left:cp)(left:ap)"); + const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['../', {x: 5}]); expect(serializer.serialize(t)).toEqual("/a;x=5(left:ap)"); }); + xit("should support going to a parent (across segments)", () => { + const p = serializer.parse("/q/(a/(c//left:cp)//left:qp)(left:ap)"); + + const t = create(p.root.children[PRIMARY_OUTLET].children[PRIMARY_OUTLET], 0, p, ['../../q2']); + expect(serializer.serialize(t)).toEqual("/q2(left:ap)"); + }); + + xit("should navigate to the root", () => { + const p = serializer.parse("/a/c"); + const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['../']); + expect(serializer.serialize(t)).toEqual(""); + }); + it("should throw when too many ..", () => { - const p = serializer.parse("/a(left:ap)/c(left:cp)"); - const c = p.firstChild(p.root); - expect(() => create(c, p, ["../../"])).toThrowError("Invalid number of '../'"); + const p = serializer.parse("/a/(c//left:cp)(left:ap)"); + expect(() => create(p.root.children[PRIMARY_OUTLET], 0, p, ['../../'])).toThrowError("Invalid number of '../'"); }); }); it("should set query params", () => { const p = serializer.parse("/"); - const t = create(p.root, p, [], {a: 'hey'}); + const t = createRoot(p, [], {a: 'hey'}); expect(t.queryParams).toEqual({a: 'hey'}); }); it("should stringify query params", () => { const p = serializer.parse("/"); - const t = create(p.root, p, [], {a: 1}); + const t = createRoot(p, [], {a: 1}); expect(t.queryParams).toEqual({a: '1'}); }); it("should reuse old query params when given undefined", () => { const p = serializer.parse("/?a=1"); - const t = create(p.root, p, [], undefined); + const t = createRoot(p, [], undefined); expect(t.queryParams).toEqual({a: '1'}); }); it("should set fragment", () => { const p = serializer.parse("/"); - const t = create(p.root, p, [], {}, "fragment"); + const t = createRoot(p, [], {}, "fragment"); expect(t.fragment).toEqual("fragment"); }); it("should reused old fragment when given undefined", () => { const p = serializer.parse("/#fragment"); - const t = create(p.root, p, [], undefined, undefined); + const t = createRoot(p, [], undefined, undefined); expect(t.fragment).toEqual("fragment"); }); }); -function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) { - if (!start) { - expect(start).toBeDefined(); +function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) { + const s = new ActivatedRouteSnapshot([], {}, PRIMARY_OUTLET, "someComponent", null, tree.root, -1); + const a = new ActivatedRoute(new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, "someComponent", s); + advanceActivatedRoute(a); + return createUrlTree(a, tree, commands, queryParams, fragment); +} + +function create(segment: UrlSegment, startIndex: number, tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) { + if (!segment) { + expect(segment).toBeDefined(); } - const s = new ActivatedRouteSnapshot([], {}, PRIMARY_OUTLET, "someComponent", null, start); + const s = new ActivatedRouteSnapshot([], {}, PRIMARY_OUTLET, "someComponent", null, segment, startIndex); const a = new ActivatedRoute(new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, "someComponent", s); advanceActivatedRoute(a); return createUrlTree(a, tree, commands, queryParams, fragment); diff --git a/modules/@angular/router/test/recognize.spec.ts b/modules/@angular/router/test/recognize.spec.ts index bc368987b0..6fdfcf660b 100644 --- a/modules/@angular/router/test/recognize.spec.ts +++ b/modules/@angular/router/test/recognize.spec.ts @@ -2,28 +2,27 @@ import {DefaultUrlSerializer} from '../src/url_serializer'; import {UrlTree} from '../src/url_tree'; import {Params, PRIMARY_OUTLET} from '../src/shared'; import {ActivatedRouteSnapshot} from '../src/router_state'; +import {RouterConfig} from '../src/config'; import {recognize} from '../src/recognize'; describe('recognize', () => { - it('should work', (done) => { - recognize(RootComponent, [ + it('should work', () => { + checkRecognize([ { path: 'a', component: ComponentA } - ], tree("a")).forEach(s => { + ], "a", s => { checkActivatedRoute(s.root, "", {}, RootComponent); checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); - done(); }); }); - it('should support secondary routes', () => { - recognize(RootComponent, [ + checkRecognize([ { path: 'a', component: ComponentA }, { path: 'b', component: ComponentB, outlet: 'left' }, { path: 'c', component: ComponentC, outlet: 'right' } - ], tree("a(left:b//right:c)")).forEach(s => { + ], "a(left:b//right:c)", s => { const c = s.children(s.root); checkActivatedRoute(c[0], "a", {}, ComponentA); checkActivatedRoute(c[1], "b", {}, ComponentB, 'left'); @@ -31,43 +30,85 @@ describe('recognize', () => { }); }); - it('should match routes in the depth first order', () => { + it('should set url segment and index properly', () => { + const url = tree("a(left:b//right:c)"); recognize(RootComponent, [ + { path: 'a', component: ComponentA }, + { path: 'b', component: ComponentB, outlet: 'left' }, + { path: 'c', component: ComponentC, outlet: 'right' } + ], url, "a(left:b//right:c)").subscribe((s) => { + expect(s.root._urlSegment).toBe(url.root); + expect(s.root._lastPathIndex).toBe(-1); + + const c = s.children(s.root); + expect(c[0]._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]); + expect(c[0]._lastPathIndex).toBe(0); + + expect(c[1]._urlSegment).toBe(url.root.children["left"]); + expect(c[1]._lastPathIndex).toBe(0); + + expect(c[2]._urlSegment).toBe(url.root.children["right"]); + expect(c[2]._lastPathIndex).toBe(0); + }); + }); + + it('should set url segment and index properly (nested case)', () => { + const url = tree("a/b/c"); + recognize(RootComponent, [ + { path: '/a/b', component: ComponentA, children: [ + {path: 'c', component: ComponentC} + ] }, + ], url, "a/b/c").subscribe(s => { + expect(s.root._urlSegment).toBe(url.root); + expect(s.root._lastPathIndex).toBe(-1); + + const compA = s.firstChild(s.root); + expect(compA._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]); + expect(compA._lastPathIndex).toBe(1); + + const compC = s.firstChild(compA); + expect(compC._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]); + expect(compC._lastPathIndex).toBe(2); + }); + }); + + it('should match routes in the depth first order', () => { + checkRecognize([ {path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]}, {path: 'a/:id', component: ComponentC} - ], tree("a/paramA")).forEach(s => { + ], "a/paramA", s => { checkActivatedRoute(s.root, "", {}, RootComponent); checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "paramA", {id: 'paramA'}, ComponentB); }); - recognize(RootComponent, [ + checkRecognize([ {path: 'a', component: ComponentA}, {path: 'a/:id', component: ComponentC} - ], tree("a/paramA")).forEach(s => { + ], "a/paramA", s => { checkActivatedRoute(s.root, "", {}, RootComponent); checkActivatedRoute(s.firstChild(s.root), "a/paramA", {id: 'paramA'}, ComponentC); }); }); it('should use outlet name when matching secondary routes', () => { - recognize(RootComponent, [ + checkRecognize([ { path: 'a', component: ComponentA }, { path: 'b', component: ComponentB, outlet: 'left' }, { path: 'b', component: ComponentC, outlet: 'right' } - ], tree("a(right:b)")).forEach(s => { + ], "a(right:b)", s => { const c = s.children(s.root); checkActivatedRoute(c[0], "a", {}, ComponentA); checkActivatedRoute(c[1], "b", {}, ComponentC, 'right'); }); }); - it('should handle nested secondary routes', () => { - recognize(RootComponent, [ + xit('should handle nested secondary routes', () => { + checkRecognize([ { path: 'a', component: ComponentA }, { path: 'b', component: ComponentB, outlet: 'left' }, { path: 'c', component: ComponentC, outlet: 'right' } - ], tree("a(left:b(right:c))")).forEach(s => { + ], "a(left:b(right:c))", s => { const c = s.children(s.root); checkActivatedRoute(c[0], "a", {}, ComponentA); checkActivatedRoute(c[1], "b", {}, ComponentB, 'left'); @@ -76,12 +117,12 @@ describe('recognize', () => { }); it('should handle non top-level secondary routes', () => { - recognize(RootComponent, [ + checkRecognize([ { path: 'a', component: ComponentA, children: [ { path: 'b', component: ComponentB }, { path: 'c', component: ComponentC, outlet: 'left' } ] }, - ], tree("a/b(left:c))")).forEach(s => { + ], "a/(b//left:c)", s => { const c = s.children(s.firstChild(s.root)); checkActivatedRoute(c[0], "b", {}, ComponentB, PRIMARY_OUTLET); checkActivatedRoute(c[1], "c", {}, ComponentC, 'left'); @@ -89,11 +130,11 @@ describe('recognize', () => { }); it('should sort routes by outlet name', () => { - recognize(RootComponent, [ + checkRecognize([ { path: 'a', component: ComponentA }, { path: 'c', component: ComponentC, outlet: 'c' }, { path: 'b', component: ComponentB, outlet: 'b' } - ], tree("a(c:c//b:b)")).forEach(s => { + ], "a(c:c//b:b)", s => { const c = s.children(s.root); checkActivatedRoute(c[0], "a", {}, ComponentA); checkActivatedRoute(c[1], "b", {}, ComponentB, 'b'); @@ -102,52 +143,52 @@ describe('recognize', () => { }); it('should support matrix parameters', () => { - recognize(RootComponent, [ + checkRecognize([ { path: 'a', component: ComponentA, children: [ - { path: 'b', component: ComponentB }, - { path: 'c', component: ComponentC, outlet: 'left' } + { path: 'b', component: ComponentB } ] - } - ], tree("a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)")).forEach(s => { - checkActivatedRoute(s.firstChild(s.root), "a", {a1: '11', a2: '22'}, ComponentA); - const c = s.children(s.firstChild(s.root)); - checkActivatedRoute(c[0], "b", {b1: '111', b2: '222'}, ComponentB); + }, + { path: 'c', component: ComponentC, outlet: 'left' } + ], "a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)", s => { + const c = s.children(s.root); + checkActivatedRoute(c[0], "a", {a1: '11', a2: '22'}, ComponentA); + checkActivatedRoute(s.firstChild(c[0]), "b", {b1: '111', b2: '222'}, ComponentB); checkActivatedRoute(c[1], "c", {c1: '1111', c2: '2222'}, ComponentC, 'left'); }); }); - + describe("index", () => { it("should support root index routes", () => { - recognize(RootComponent, [ + checkRecognize([ {index: true, component: ComponentA} - ], tree("")).forEach(s => { + ], "", s => { checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); }); }); it("should support nested root index routes", () => { - recognize(RootComponent, [ + checkRecognize([ {index: true, component: ComponentA, children: [{index: true, component: ComponentB}]} - ], tree("")).forEach(s => { + ], "", s => { checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); }); }); it("should support index routes", () => { - recognize(RootComponent, [ + checkRecognize([ {path: 'a', component: ComponentA, children: [ {index: true, component: ComponentB} ]} - ], tree("a")).forEach(s => { + ], "a", s => { checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); }); }); it("should support index routes with children", () => { - recognize(RootComponent, [ + checkRecognize([ { index: true, component: ComponentA, children: [ { index: true, component: ComponentB, children: [ @@ -156,7 +197,7 @@ describe('recognize', () => { } ] } - ], tree("c/10")).forEach(s => { + ], "c/10", s => { checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); checkActivatedRoute( @@ -164,21 +205,96 @@ describe('recognize', () => { }); }); - it("should pass parameters to every nested index route (case with non-index route)", () => { - recognize(RootComponent, [ + xit("should pass parameters to every nested index route (case with non-index route)", () => { + checkRecognize([ {path: 'a', component: ComponentA, children: [{index: true, component: ComponentB}]} - ], tree("/a;a=1")).forEach(s => { + ], "/a;a=1", s => { checkActivatedRoute(s.firstChild(s.root), "a", {a: '1'}, ComponentA); checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {a: '1'}, ComponentB); }); }); }); - + + describe("matching empty url", () => { + it("should support root index routes", () => { + recognize(RootComponent, [ + {path: '', component: ComponentA} + ], tree(""), "").forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); + }); + }); + + it("should support nested root index routes", () => { + recognize(RootComponent, [ + {path: '', component: ComponentA, children: [{path: '', component: ComponentB}]} + ], tree(""), "").forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); + }); + }); + + it('should set url segment and index properly', () => { + const url = tree(""); + recognize(RootComponent, [ + {path: '', component: ComponentA, children: [{path: '', component: ComponentB}]} + ], url, "").forEach(s => { + expect(s.root._urlSegment).toBe(url.root); + expect(s.root._lastPathIndex).toBe(-1); + + const c = s.firstChild(s.root); + expect(c._urlSegment).toBe(url.root); + expect(c._lastPathIndex).toBe(-1); + + const c2 = s.firstChild(s.firstChild(s.root)); + expect(c2._urlSegment).toBe(url.root); + expect(c2._lastPathIndex).toBe(-1); + }); + }); + + it("should support index routes", () => { + recognize(RootComponent, [ + {path: 'a', component: ComponentA, children: [ + {path: '', component: ComponentB} + ]} + ], tree("a"), "a").forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); + }); + }); + + it("should support index routes with children", () => { + recognize(RootComponent, [ + { + path: '', component: ComponentA, children: [ + { path: '', component: ComponentB, children: [ + {path: 'c/:id', component: ComponentC} + ] + } + ] + } + ], tree("c/10"), "c/10").forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {}, ComponentB); + checkActivatedRoute( + s.firstChild(s.firstChild(s.firstChild(s.root))), "c/10", {id: '10'}, ComponentC); + }); + }); + + xit("should pass parameters to every nested index route (case with non-index route)", () => { + recognize(RootComponent, [ + {path: 'a', component: ComponentA, children: [{path: '', component: ComponentB}]} + ], tree("/a;a=1"), "/a;a=1").forEach(s => { + checkActivatedRoute(s.firstChild(s.root), "a", {a: '1'}, ComponentA); + checkActivatedRoute(s.firstChild(s.firstChild(s.root)), "", {a: '1'}, ComponentB); + }); + }); + }); + describe("wildcards", () => { it("should support simple wildcards", () => { - recognize(RootComponent, [ + checkRecognize([ {path: '**', component: ComponentA} - ], tree("a/b/c/d;a1=11")).forEach(s => { + ], "a/b/c/d;a1=11", s => { checkActivatedRoute(s.firstChild(s.root), "a/b/c/d", {a1:'11'}, ComponentA); }); }); @@ -187,7 +303,7 @@ describe('recognize', () => { describe("query parameters", () => { it("should support query params", () => { const config = [{path: 'a', component: ComponentA}]; - recognize(RootComponent, config, tree("a?q=11")).forEach(s => { + checkRecognize(config, "a?q=11", s => { expect(s.queryParams).toEqual({q: '11'}); }); }); @@ -196,7 +312,7 @@ describe('recognize', () => { describe("fragment", () => { it("should support fragment", () => { const config = [{path: 'a', component: ComponentA}]; - recognize(RootComponent, config, tree("a#f1")).forEach(s => { + checkRecognize(config, "a#f1", s => { expect(s.fragment).toEqual("f1"); }); }); @@ -208,7 +324,7 @@ describe('recognize', () => { { path: 'a', component: ComponentA }, { path: 'b', component: ComponentB, outlet: 'aux' }, { path: 'c', component: ComponentC, outlet: 'aux' } - ], tree("a(aux:b//aux:c)")).subscribe((_) => {}, s => { + ], tree("a(aux:b//aux:c)"), "a(aux:b//aux:c)").subscribe((_) => {}, s => { expect(s.toString()).toContain("Two segments cannot have the same outlet name: 'aux:b' and 'aux:c'."); }); }); @@ -216,7 +332,7 @@ describe('recognize', () => { it("should error when no matching routes", () => { recognize(RootComponent, [ { path: 'a', component: ComponentA } - ], tree("invalid")).subscribe((_) => {}, s => { + ], tree("invalid"), "invalid").subscribe((_) => {}, s => { expect(s.toString()).toContain("Cannot match any routes"); }); }); @@ -224,18 +340,24 @@ describe('recognize', () => { it("should error when no matching routes (too short)", () => { recognize(RootComponent, [ { path: 'a/:id', component: ComponentA } - ], tree("a")).subscribe((_) => {}, s => { + ], tree("a"), "a").subscribe((_) => {}, s => { expect(s.toString()).toContain("Cannot match any routes"); }); }); }); }); +function checkRecognize(config: RouterConfig, url: string, callback: any): void { + recognize(RootComponent, config, tree(url), url).subscribe(callback, e => { + throw e; + }); +} + function checkActivatedRoute(actual: ActivatedRouteSnapshot | null, url: string, params: Params, cmp: Function, outlet: string = PRIMARY_OUTLET):void { if (actual === null) { expect(actual).not.toBeNull(); } else { - expect(actual.urlSegments.map(s => s.path).join("/")).toEqual(url); + expect(actual.url.map(s => s.path).join("/")).toEqual(url); expect(actual.params).toEqual(params); expect(actual.component).toBe(cmp); expect(actual.outlet).toEqual(outlet); diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 40ff2aecd5..1934d295d7 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -27,6 +27,7 @@ describe("Integration", () => { beforeEachProviders(() => { let config: RouterConfig = [ + { path: '', component: BlankCmp }, { path: 'simple', component: SimpleCmp } ]; @@ -54,19 +55,20 @@ describe("Integration", () => { router.navigateByUrl('/simple'); advance(fixture); + expect(location.path()).toEqual('/simple'); }))); it('should update location when navigating', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); expect(location.path()).toEqual('/team/22'); @@ -79,6 +81,9 @@ describe("Integration", () => { xit('should navigate back and forward', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'simple', component: SimpleCmp }, @@ -86,7 +91,6 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); router.navigateByUrl('/team/33/simple'); advance(fixture); @@ -106,14 +110,15 @@ describe("Integration", () => { it('should navigate when locations changes', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'user/:name', component: UserCmp } ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - router.navigateByUrl('/team/22/user/victor'); advance(fixture); @@ -125,6 +130,9 @@ describe("Integration", () => { it('should support secondary routes', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'user/:name', component: UserCmp }, @@ -132,9 +140,7 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - - router.navigateByUrl('/team/22/user/victor(right:simple)'); + router.navigateByUrl('/team/22/(user/victor//right:simple)'); advance(fixture); expect(fixture.debugElement.nativeElement) @@ -143,6 +149,9 @@ describe("Integration", () => { it('should deactivate outlets', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'user/:name', component: UserCmp }, @@ -150,9 +159,7 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - - router.navigateByUrl('/team/22/user/victor(right:simple)'); + router.navigateByUrl('/team/22/(user/victor//right:simple)'); advance(fixture); router.navigateByUrl('/team/22/user/victor'); @@ -163,16 +170,18 @@ describe("Integration", () => { it('should deactivate nested outlets', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'user/:name', component: UserCmp }, { path: 'simple', component: SimpleCmp, outlet: 'right' } - ] } + ] }, + { path: '', component: BlankCmp} ]); - const fixture = tcb.createFakeAsync(RootCmp); - - router.navigateByUrl('/team/22/user/victor(right:simple)'); + router.navigateByUrl('/team/22/(user/victor//right:simple)'); advance(fixture); router.navigateByUrl('/'); @@ -183,12 +192,13 @@ describe("Integration", () => { it('should set query params and fragment', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'query', component: QueryParamsAndFragmentCmp } ]); - const fixture = tcb.createFakeAsync(RootCmp); - router.navigateByUrl('/query?name=1#fragment1'); advance(fixture); expect(fixture.debugElement.nativeElement).toHaveText('query: 1 fragment: fragment1'); @@ -200,14 +210,15 @@ describe("Integration", () => { it('should push params only when they change', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'user/:name', component: UserCmp } ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - router.navigateByUrl('/team/22/user/victor'); advance(fixture); const team = fixture.debugElement.children[1].componentInstance; @@ -225,13 +236,14 @@ describe("Integration", () => { it('should work when navigating to /', fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { index: true, component: SimpleCmp }, { path: '/user/:name', component: UserCmp } ]); - const fixture = tcb.createFakeAsync(RootCmp); - router.navigateByUrl('/user/victor'); advance(fixture); @@ -245,6 +257,9 @@ describe("Integration", () => { it("should cancel in-flight navigations", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: '/user/:name', component: UserCmp } ]); @@ -252,7 +267,6 @@ describe("Integration", () => { const recordedEvents = []; router.events.forEach(e => recordedEvents.push(e)); - const fixture = tcb.createFakeAsync(RootCmp); router.navigateByUrl('/user/init'); advance(fixture); @@ -269,7 +283,7 @@ describe("Integration", () => { expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]); - expectEvents(router, recordedEvents.slice(2), [ + expectEvents(recordedEvents, [ [NavigationStart, '/user/init'], [RoutesRecognized, '/user/init'], [NavigationEnd, '/user/init'], @@ -285,6 +299,9 @@ describe("Integration", () => { it("should handle failed navigations gracefully", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: '/user/:name', component: UserCmp } ]); @@ -292,9 +309,6 @@ describe("Integration", () => { const recordedEvents = []; router.events.forEach(e => recordedEvents.push(e)); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - let e; router.navigateByUrl('/invalid').catch(_ => e = _); advance(fixture); @@ -305,7 +319,7 @@ describe("Integration", () => { expect(fixture.debugElement.nativeElement).toHaveText('user fedor'); - expectEvents(router, recordedEvents.slice(2), [ + expectEvents(recordedEvents, [ [NavigationStart, '/invalid'], [NavigationError, '/invalid'], @@ -318,6 +332,9 @@ describe("Integration", () => { describe("router links", () => { it("should support string router links", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'link', component: StringLinkCmp }, @@ -325,9 +342,6 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22/link'); advance(fixture); expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }'); @@ -342,6 +356,9 @@ describe("Integration", () => { it("should support absolute router links", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'link', component: AbsoluteLinkCmp }, @@ -349,9 +366,6 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22/link'); advance(fixture); expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }'); @@ -366,6 +380,9 @@ describe("Integration", () => { it("should support relative router links", fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'link', component: RelativeLinkCmp }, @@ -373,9 +390,6 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22/link'); advance(fixture); expect(fixture.debugElement.nativeElement) @@ -394,11 +408,15 @@ describe("Integration", () => { fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => { let fixture = tcb.createFakeAsync(AbsoluteLinkCmp); advance(fixture); + expect(fixture.debugElement.nativeElement).toHaveText('link'); }))); it("should support query params and fragments", fakeAsync(inject([Router, Location, TestComponentBuilder], (router, location, tcb) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, children: [ { path: 'link', component: LinkWithQueryParamsAndFragment }, @@ -406,9 +424,6 @@ describe("Integration", () => { ] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22/link'); advance(fixture); @@ -426,14 +441,14 @@ describe("Integration", () => { describe("redirects", () => { it("should work", fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: '/old/team/:id', redirectTo: 'team/:id' }, { path: '/team/:id', component: TeamCmp } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('old/team/22'); advance(fixture); @@ -450,17 +465,17 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canActivate: ["alwaysFalse"] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); - expect(location.path()).toEqual(''); + expect(location.path()).toEqual('/'); }))); }); @@ -471,13 +486,13 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canActivate: ["alwaysTrue"] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); @@ -496,13 +511,13 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); @@ -519,16 +534,16 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate'] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); - expect(location.path()).toEqual(''); + expect(location.path()).toEqual('/'); }))); }); }); @@ -544,13 +559,13 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canDeactivate: ["CanDeactivate"] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); @@ -579,13 +594,13 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); expect(location.path()).toEqual('/team/22'); @@ -606,13 +621,13 @@ describe("Integration", () => { it('works', fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => { + const fixture = tcb.createFakeAsync(RootCmp); + advance(fixture); + router.resetConfig([ { path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate'] } ]); - const fixture = tcb.createFakeAsync(RootCmp); - advance(fixture); - router.navigateByUrl('/team/22'); advance(fixture); expect(location.path()).toEqual('/team/22'); @@ -625,10 +640,10 @@ describe("Integration", () => { }); }); -function expectEvents(router: Router, events:Event[], pairs: any[]) { +function expectEvents(events:Event[], pairs: any[]) { for (let i = 0; i < events.length; ++i) { expect((events[i].constructor).name).toBe(pairs[i][0].name); - expect(router.serializeUrl((events[i]).url)).toBe(pairs[i][1]); + expect((events[i]).url).toBe(pairs[i][1]); } } @@ -641,7 +656,7 @@ class StringLinkCmp {} @Component({ selector: 'link-cmp', - template: `link`, + template: `link`, directives: ROUTER_DIRECTIVES }) class AbsoluteLinkCmp {} @@ -668,6 +683,14 @@ class LinkWithQueryParamsAndFragment {} class SimpleCmp { } +@Component({ + selector: 'blank-cmp', + template: ``, + directives: ROUTER_DIRECTIVES +}) +class BlankCmp { +} + @Component({ selector: 'team-cmp', template: `team {{id | async}} { , right: }`, diff --git a/modules/@angular/router/test/url_serializer.spec.ts b/modules/@angular/router/test/url_serializer.spec.ts index 3c94bd0ef2..6b780123c5 100644 --- a/modules/@angular/router/test/url_serializer.spec.ts +++ b/modules/@angular/router/test/url_serializer.spec.ts @@ -1,76 +1,80 @@ -import {DefaultUrlSerializer, serializeSegment} from '../src/url_serializer'; +import {DefaultUrlSerializer, serializePath} from '../src/url_serializer'; import {UrlSegment} from '../src/url_tree'; +import {PRIMARY_OUTLET} from '../src/shared'; describe('url serializer', () => { const url = new DefaultUrlSerializer(); it('should parse the root url', () => { const tree = url.parse("/"); + expectSegment(tree.root, ""); - expect(url.serialize(tree)).toEqual(""); + expect(url.serialize(tree)).toEqual("/"); }); it('should parse non-empty urls', () => { const tree = url.parse("one/two"); - const one = tree.firstChild(tree.root); - - expectSegment(one, "one"); - expectSegment(tree.firstChild(one), "two"); + expectSegment(tree.root.children[PRIMARY_OUTLET], "one/two"); expect(url.serialize(tree)).toEqual("/one/two"); }); it("should parse multiple secondary segments", () => { - const tree = url.parse("/one/two(left:three//right:four)/five"); - const c = tree.children(tree.firstChild(tree.root)); + const tree = url.parse("/one/two(left:three//right:four)"); - expectSegment(c[0], "two"); - expectSegment(c[1], "left:three"); - expectSegment(c[2], "right:four"); + expectSegment(tree.root.children[PRIMARY_OUTLET], "one/two"); + expectSegment(tree.root.children['left'], "three"); + expectSegment(tree.root.children['right'], "four"); - expectSegment(tree.firstChild(c[0]), "five"); - - expect(url.serialize(tree)).toEqual("/one/two(left:three//right:four)/five"); + expect(url.serialize(tree)).toEqual("/one/two(left:three//right:four)"); }); - it("should parse secondary segments that have secondary segments", () => { - const tree = url.parse("/one(left:two(right:three))"); - const c = tree.children(tree.root); + it("should parse scoped secondary segments", () => { + const tree = url.parse("/one/(two//left:three)"); - expectSegment(c[0], "one"); - expectSegment(c[1], "left:two"); - expectSegment(c[2], "right:three"); + const primary = tree.root.children[PRIMARY_OUTLET]; + expectSegment(primary, "one", true); - expect(url.serialize(tree)).toEqual("/one(left:two//right:three)"); + expectSegment(primary.children[PRIMARY_OUTLET], "two"); + expectSegment(primary.children["left"], "three"); + + expect(url.serialize(tree)).toEqual("/one/(two//left:three)"); + }); + + it("should parse scoped secondary segments with unscoped ones", () => { + const tree = url.parse("/one/(two//left:three)(right:four)"); + + const primary = tree.root.children[PRIMARY_OUTLET]; + expectSegment(primary, "one", true); + expectSegment(primary.children[PRIMARY_OUTLET], "two"); + expectSegment(primary.children["left"], "three"); + expectSegment(tree.root.children["right"], "four"); + + expect(url.serialize(tree)).toEqual("/one/(two//left:three)(right:four)"); }); it("should parse secondary segments that have children", () => { const tree = url.parse("/one(left:two/three)"); - const c = tree.children(tree.root); - expectSegment(c[0], "one"); - expectSegment(c[1], "left:two"); - expectSegment(tree.firstChild(c[1]), "three"); + expectSegment(tree.root.children[PRIMARY_OUTLET], "one"); + expectSegment(tree.root.children['left'], "two/three"); expect(url.serialize(tree)).toEqual("/one(left:two/three)"); }); it("should parse an empty secondary segment group", () => { const tree = url.parse("/one()"); - const c = tree.children(tree.root); - expectSegment(c[0], "one"); - expect(tree.children(c[0]).length).toEqual(0); + expectSegment(tree.root.children[PRIMARY_OUTLET], "one"); expect(url.serialize(tree)).toEqual("/one"); }); it("should parse key-value matrix params", () => { const tree = url.parse("/one;a=11a;b=11b(left:two;c=22//right:three;d=33)"); - const c = tree.children(tree.root); - expectSegment(c[0], "one;a=11a;b=11b"); - expectSegment(c[1], "left:two;c=22"); - expectSegment(c[2], "right:three;d=33"); + expectSegment(tree.root.children[PRIMARY_OUTLET], "one;a=11a;b=11b"); + expectSegment(tree.root.children["left"], "two;c=22"); + expectSegment(tree.root.children["right"], "three;d=33"); expect(url.serialize(tree)).toEqual("/one;a=11a;b=11b(left:two;c=22//right:three;d=33)"); }); @@ -78,8 +82,7 @@ describe('url serializer', () => { it("should parse key only matrix params", () => { const tree = url.parse("/one;a"); - const c = tree.firstChild(tree.root); - expectSegment(c, "one;a=true"); + expectSegment(tree.root.children[PRIMARY_OUTLET], "one;a=true"); expect(url.serialize(tree)).toEqual("/one;a=true"); }); @@ -112,6 +115,8 @@ describe('url serializer', () => { }); }); -function expectSegment(segment:UrlSegment | null, expected:string):void { - expect(segment ? serializeSegment(segment) : null).toEqual(expected); +function expectSegment(segment:UrlSegment, expected:string, hasChildren: boolean = false):void { + const p = segment.pathsWithParams.map(p => serializePath(p)).join("/"); + expect(p).toEqual(expected); + expect(Object.keys(segment.children).length > 0).toEqual(hasChildren); }