2016-06-23 12:47:54 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2016-06-08 14:13:41 -04:00
|
|
|
import {Type} from '@angular/core';
|
2016-08-16 00:11:09 -04:00
|
|
|
import {Observable} from 'rxjs/Observable';
|
2016-11-09 18:25:47 -05:00
|
|
|
import {PRIMARY_OUTLET, Params} from './shared';
|
|
|
|
import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
2016-08-19 18:48:09 -04:00
|
|
|
|
2016-06-27 15:27:23 -04:00
|
|
|
/**
|
2016-09-10 19:52:21 -04:00
|
|
|
* @whatItDoes Represents router configuration.
|
|
|
|
*
|
|
|
|
* @description
|
2016-07-06 19:19:52 -04:00
|
|
|
* `Routes` is an array of route configurations. Each one has the following properties:
|
|
|
|
*
|
2016-09-10 19:52:21 -04:00
|
|
|
* - `path` is a string that uses the route matcher DSL.
|
2016-07-06 19:19:52 -04:00
|
|
|
* - `pathMatch` is a string that specifies the matching strategy.
|
|
|
|
* - `component` is a component type.
|
|
|
|
* - `redirectTo` is the url fragment which will replace the current matched segment.
|
|
|
|
* - `outlet` is the name of the outlet the component should be placed into.
|
|
|
|
* - `canActivate` is an array of DI tokens used to look up CanActivate handlers. See {@link
|
2016-07-13 18:15:20 -04:00
|
|
|
* CanActivate} for more info.
|
2016-07-13 18:25:48 -04:00
|
|
|
* - `canActivateChild` is an array of DI tokens used to look up CanActivateChild handlers. See
|
|
|
|
* {@link
|
2016-07-13 18:15:20 -04:00
|
|
|
* CanActivateChild} for more info.
|
2016-07-06 19:19:52 -04:00
|
|
|
* - `canDeactivate` is an array of DI tokens used to look up CanDeactivate handlers. See {@link
|
2016-07-13 18:15:20 -04:00
|
|
|
* CanDeactivate} for more info.
|
2016-07-06 19:19:52 -04:00
|
|
|
* - `data` is additional data provided to the component via `ActivatedRoute`.
|
|
|
|
* - `resolve` is a map of DI tokens used to look up data resolvers. See {@link Resolve} for more
|
|
|
|
* info.
|
|
|
|
* - `children` is an array of child route definitions.
|
|
|
|
*
|
|
|
|
* ### Simple Configuration
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'team/:id',
|
|
|
|
* component: Team,
|
|
|
|
* children: [
|
|
|
|
* {
|
|
|
|
* path: 'user/:name',
|
|
|
|
* component: User
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* When navigating to `/team/11/user/bob`, the router will create the team component with the user
|
|
|
|
* component in it.
|
|
|
|
*
|
|
|
|
* ### Multiple Outlets
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'team/:id',
|
|
|
|
* component: Team
|
|
|
|
* },
|
|
|
|
* {
|
|
|
|
* path: 'chat/:user',
|
|
|
|
* component: Chat
|
2016-09-27 13:10:12 -04:00
|
|
|
* outlet: 'aux'
|
2016-07-06 19:19:52 -04:00
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* When navigating to `/team/11(aux:chat/jim)`, the router will create the team component next to
|
|
|
|
* the chat component. The chat component will be placed into the aux outlet.
|
|
|
|
*
|
|
|
|
* ### Wild Cards
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: '**',
|
|
|
|
* component: Sink
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* Regardless of where you navigate to, the router will instantiate the sink component.
|
|
|
|
*
|
|
|
|
* ### Redirects
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'team/:id',
|
|
|
|
* component: Team,
|
|
|
|
* children: [
|
|
|
|
* {
|
|
|
|
* path: 'legacy/user/:name',
|
|
|
|
* redirectTo: 'user/:name'
|
|
|
|
* },
|
|
|
|
* {
|
|
|
|
* path: 'user/:name',
|
|
|
|
* component: User
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* When navigating to '/team/11/legacy/user/jim', the router will change the url to
|
|
|
|
* '/team/11/user/jim', and then will instantiate the team component with the user component
|
|
|
|
* in it.
|
|
|
|
*
|
|
|
|
* If the `redirectTo` value starts with a '/', then it is an absolute redirect. E.g., if in the
|
|
|
|
* example above we change the `redirectTo` to `/user/:name`, the result url will be '/user/jim'.
|
|
|
|
*
|
|
|
|
* ### Empty Path
|
|
|
|
*
|
2016-09-10 19:52:21 -04:00
|
|
|
* Empty-path route configurations can be used to instantiate components that do not 'consume'
|
2016-07-06 19:19:52 -04:00
|
|
|
* any url segments. Let's look at the following configuration:
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'team/:id',
|
|
|
|
* component: Team,
|
|
|
|
* children: [
|
|
|
|
* {
|
|
|
|
* path: '',
|
|
|
|
* component: AllUsers
|
|
|
|
* },
|
|
|
|
* {
|
|
|
|
* path: 'user/:name',
|
|
|
|
* component: User
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* When navigating to `/team/11`, the router will instantiate the AllUsers component.
|
|
|
|
*
|
|
|
|
* Empty-path routes can have children.
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'team/:id',
|
|
|
|
* component: Team,
|
|
|
|
* children: [
|
|
|
|
* {
|
|
|
|
* path: '',
|
|
|
|
* component: WrapperCmp,
|
|
|
|
* children: [
|
|
|
|
* {
|
|
|
|
* path: 'user/:name',
|
|
|
|
* component: User
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with
|
|
|
|
* the user component in it.
|
|
|
|
*
|
2016-11-15 22:00:20 -05:00
|
|
|
* An empty path route inherits its parent's params and data. This is because it cannot have its
|
|
|
|
* own params, and, as a result, it often uses its parent's params and data as its own.
|
|
|
|
*
|
2016-07-06 19:19:52 -04:00
|
|
|
* ### Matching Strategy
|
|
|
|
*
|
|
|
|
* By default the router will look at what is left in the url, and check if it starts with
|
|
|
|
* the specified path (e.g., `/team/11/user` starts with `team/:id`).
|
|
|
|
*
|
|
|
|
* We can change the matching strategy to make sure that the path covers the whole unconsumed url,
|
|
|
|
* which is akin to `unconsumedUrl === path` or `$` regular expressions.
|
|
|
|
*
|
|
|
|
* This is particularly important when redirecting empty-path routes.
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: '',
|
|
|
|
* pathMatch: 'prefix', //default
|
|
|
|
* redirectTo: 'main'
|
|
|
|
* },
|
|
|
|
* {
|
|
|
|
* path: 'main',
|
|
|
|
* component: Main
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* Since an empty path is a prefix of any url, even when navigating to '/main', the router will
|
|
|
|
* still apply the redirect.
|
|
|
|
*
|
|
|
|
* If `pathMatch: full` is provided, the router will apply the redirect if and only if navigating to
|
|
|
|
* '/'.
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: '',
|
|
|
|
* pathMatch: 'full',
|
|
|
|
* redirectTo: 'main'
|
|
|
|
* },
|
|
|
|
* {
|
|
|
|
* path: 'main',
|
|
|
|
* component: Main
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* ### Componentless Routes
|
|
|
|
*
|
|
|
|
* It is useful at times to have the ability to share parameters between sibling components.
|
|
|
|
*
|
|
|
|
* Say we have two components--ChildCmp and AuxCmp--that we want to put next to each other and both
|
|
|
|
* of them require some id parameter.
|
|
|
|
*
|
|
|
|
* One way to do that would be to have a bogus parent component, so both the siblings can get the id
|
|
|
|
* parameter from it. This is not ideal. Instead, you can use a componentless route.
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'parent/:id',
|
|
|
|
* children: [
|
|
|
|
* { path: 'a', component: MainChild },
|
|
|
|
* { path: 'b', component: AuxChild, outlet: 'aux' }
|
|
|
|
* ]
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* So when navigating to `parent/10/(a//aux:b)`, the route will instantiate the main child and aux
|
|
|
|
* child components next to each other. In this example, the application component
|
|
|
|
* has to have the primary and aux outlets defined.
|
|
|
|
*
|
|
|
|
* The router will also merge the `params`, `data`, and `resolve` of the componentless parent into
|
2016-11-15 22:00:20 -05:00
|
|
|
* the `params`, `data`, and `resolve` of the children. This is done because there is no component
|
|
|
|
* that can inject the activated route of the componentless parent.
|
2016-07-06 19:19:52 -04:00
|
|
|
*
|
|
|
|
* This is especially useful when child components are defined as follows:
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'parent/:id',
|
|
|
|
* children: [
|
|
|
|
* { path: '', component: MainChild },
|
|
|
|
* { path: '', component: AuxChild, outlet: 'aux' }
|
|
|
|
* ]
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* With this configuration in place, navigating to '/parent/10' will create the main child and aux
|
|
|
|
* components.
|
|
|
|
*
|
2016-09-10 19:52:21 -04:00
|
|
|
* ### Lazy Loading
|
|
|
|
*
|
|
|
|
* Lazy loading speeds up our application load time by splitting it into multiple bundles, and
|
|
|
|
* loading them on demand. The router is designed to make lazy loading simple and easy. Instead of
|
|
|
|
* providing the children property, you can provide
|
|
|
|
* the loadChildren property, as follows:
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* [{
|
|
|
|
* path: 'team/:id',
|
|
|
|
* component: Team,
|
|
|
|
* loadChildren: 'team'
|
|
|
|
* }]
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* The router will use registered NgModuleFactoryLoader to fetch an NgModule associated with 'team'.
|
|
|
|
* Then it will
|
|
|
|
* extract the set of routes defined in that NgModule, and will transparently add those routes to
|
|
|
|
* the main configuration.
|
|
|
|
*
|
2016-07-06 19:19:52 -04:00
|
|
|
* @stable use Routes
|
|
|
|
*/
|
|
|
|
export type Routes = Route[];
|
|
|
|
|
2016-11-09 18:25:47 -05:00
|
|
|
/**
|
|
|
|
* @whatItDoes Represents the results of the URL matching.
|
|
|
|
*
|
|
|
|
* * `consumed` is an array of the consumed URL segments.
|
|
|
|
* * `posParams` is a map of positional parameters.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
export type UrlMatchResult = {
|
|
|
|
consumed: UrlSegment[]; posParams?: {[name: string]: UrlSegment};
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @whatItDoes A function matching URLs
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
*
|
|
|
|
* A custom URL matcher can be provided when a combination of `path` and `pathMatch` isn't
|
|
|
|
* expressive enough.
|
|
|
|
*
|
|
|
|
* For instance, the following matcher matches html files.
|
|
|
|
*
|
|
|
|
* ```
|
|
|
|
* function htmlFiles(url: UrlSegment[]) {
|
|
|
|
* return url.length === 1 && url[0].path.endsWith('.html') ? ({consumed: url}) : null;
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* const routes = [{ matcher: htmlFiles, component: HtmlCmp }];
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) =>
|
|
|
|
UrlMatchResult;
|
|
|
|
|
2016-07-06 19:19:52 -04:00
|
|
|
/**
|
2016-09-10 19:52:21 -04:00
|
|
|
* @whatItDoes Represents the static data associated with a particular route.
|
2016-07-06 19:19:52 -04:00
|
|
|
* See {@link Routes} for more details.
|
2016-06-28 17:49:29 -04:00
|
|
|
* @stable
|
2016-06-27 15:27:23 -04:00
|
|
|
*/
|
2016-06-27 17:00:07 -04:00
|
|
|
export type Data = {
|
|
|
|
[name: string]: any
|
|
|
|
};
|
2016-06-27 15:27:23 -04:00
|
|
|
|
|
|
|
/**
|
2016-11-09 18:25:47 -05:00
|
|
|
* @whatItDoes Represents the resolved data associated with a particular route.
|
2016-07-06 19:19:52 -04:00
|
|
|
* See {@link Routes} for more details.
|
2016-06-28 17:49:29 -04:00
|
|
|
* @stable
|
2016-06-27 15:27:23 -04:00
|
|
|
*/
|
2016-06-27 17:00:07 -04:00
|
|
|
export type ResolveData = {
|
|
|
|
[name: string]: any
|
|
|
|
};
|
2016-05-23 19:14:23 -04:00
|
|
|
|
2016-08-16 00:11:09 -04:00
|
|
|
/**
|
2016-09-10 19:52:21 -04:00
|
|
|
* @whatItDoes The type of `loadChildren`.
|
|
|
|
* See {@link Routes} for more details.
|
2016-08-17 18:35:30 -04:00
|
|
|
* @stable
|
2016-08-16 00:11:09 -04:00
|
|
|
*/
|
|
|
|
export type LoadChildrenCallback = () => Type<any>| Promise<Type<any>>| Observable<Type<any>>;
|
|
|
|
|
|
|
|
/**
|
2016-09-10 19:52:21 -04:00
|
|
|
* @whatItDoes The type of `loadChildren`.
|
|
|
|
*
|
|
|
|
* See {@link Routes} for more details.
|
2016-08-17 18:35:30 -04:00
|
|
|
* @stable
|
2016-08-16 00:11:09 -04:00
|
|
|
*/
|
|
|
|
export type LoadChildren = string | LoadChildrenCallback;
|
|
|
|
|
2016-06-27 15:27:23 -04:00
|
|
|
/**
|
2016-07-06 19:19:52 -04:00
|
|
|
* See {@link Routes} for more details.
|
2016-06-28 17:49:29 -04:00
|
|
|
* @stable
|
2016-06-27 15:27:23 -04:00
|
|
|
*/
|
2016-05-23 19:14:23 -04:00
|
|
|
export interface Route {
|
|
|
|
path?: string;
|
2016-07-28 15:15:07 -04:00
|
|
|
pathMatch?: string;
|
2016-11-09 18:25:47 -05:00
|
|
|
matcher?: UrlMatcher;
|
2016-08-16 16:40:28 -04:00
|
|
|
component?: Type<any>;
|
2016-06-28 17:49:29 -04:00
|
|
|
redirectTo?: string;
|
2016-05-23 19:14:23 -04:00
|
|
|
outlet?: string;
|
2016-06-07 12:50:35 -04:00
|
|
|
canActivate?: any[];
|
2016-07-13 18:15:20 -04:00
|
|
|
canActivateChild?: any[];
|
2016-06-07 12:50:35 -04:00
|
|
|
canDeactivate?: any[];
|
2016-07-26 17:39:02 -04:00
|
|
|
canLoad?: any[];
|
2016-06-27 17:00:07 -04:00
|
|
|
data?: Data;
|
|
|
|
resolve?: ResolveData;
|
2016-06-28 17:49:29 -04:00
|
|
|
children?: Route[];
|
2016-08-16 00:11:09 -04:00
|
|
|
loadChildren?: LoadChildren;
|
2016-06-16 17:36:51 -04:00
|
|
|
}
|
|
|
|
|
2016-07-06 19:19:52 -04:00
|
|
|
export function validateConfig(config: Routes): void {
|
2016-11-10 17:55:10 -05:00
|
|
|
// forEach doesn't iterate undefined values
|
|
|
|
for (let i = 0; i < config.length; i++) {
|
|
|
|
validateNode(config[i]);
|
|
|
|
}
|
2016-06-16 17:36:51 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function validateNode(route: Route): void {
|
2016-11-10 17:55:10 -05:00
|
|
|
if (!route) {
|
|
|
|
throw new Error(`
|
|
|
|
Invalid route configuration: Encountered undefined route.
|
|
|
|
The reason might be an extra comma.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
const routes: Routes = [
|
|
|
|
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
|
|
|
|
{ path: 'dashboard', component: DashboardComponent },, << two commas
|
|
|
|
{ path: 'detail/:id', component: HeroDetailComponent }
|
|
|
|
];
|
|
|
|
`);
|
|
|
|
}
|
2016-07-14 11:28:31 -04:00
|
|
|
if (Array.isArray(route)) {
|
|
|
|
throw new Error(`Invalid route configuration: Array cannot be specified`);
|
|
|
|
}
|
2016-10-20 18:00:15 -04:00
|
|
|
if (route.component === undefined && (route.outlet && route.outlet !== PRIMARY_OUTLET)) {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid route configuration of route '${route.path}': a componentless route cannot have a named outlet set`);
|
|
|
|
}
|
2016-06-16 17:36:51 -04:00
|
|
|
if (!!route.redirectTo && !!route.children) {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid configuration of route '${route.path}': redirectTo and children cannot be used together`);
|
|
|
|
}
|
2016-07-06 19:19:52 -04:00
|
|
|
if (!!route.redirectTo && !!route.loadChildren) {
|
2016-07-06 14:02:16 -04:00
|
|
|
throw new Error(
|
2016-07-06 19:19:52 -04:00
|
|
|
`Invalid configuration of route '${route.path}': redirectTo and loadChildren cannot be used together`);
|
2016-07-06 14:02:16 -04:00
|
|
|
}
|
2016-07-06 19:19:52 -04:00
|
|
|
if (!!route.children && !!route.loadChildren) {
|
2016-07-06 14:02:16 -04:00
|
|
|
throw new Error(
|
2016-07-06 19:19:52 -04:00
|
|
|
`Invalid configuration of route '${route.path}': children and loadChildren cannot be used together`);
|
2016-07-06 14:02:16 -04:00
|
|
|
}
|
2016-06-16 17:36:51 -04:00
|
|
|
if (!!route.redirectTo && !!route.component) {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid configuration of route '${route.path}': redirectTo and component cannot be used together`);
|
|
|
|
}
|
2016-11-09 18:25:47 -05:00
|
|
|
if (!!route.path && !!route.matcher) {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid configuration of route '${route.path}': path and matcher cannot be used together`);
|
|
|
|
}
|
2016-07-06 14:02:16 -04:00
|
|
|
if (route.redirectTo === undefined && !route.component && !route.children &&
|
2016-07-06 19:19:52 -04:00
|
|
|
!route.loadChildren) {
|
2016-06-19 17:44:20 -04:00
|
|
|
throw new Error(
|
2016-07-20 17:47:51 -04:00
|
|
|
`Invalid configuration of route '${route.path}': one of the following must be provided (component or redirectTo or children or loadChildren)`);
|
2016-06-19 17:44:20 -04:00
|
|
|
}
|
2016-06-16 17:36:51 -04:00
|
|
|
if (route.path === undefined) {
|
|
|
|
throw new Error(`Invalid route configuration: routes must have path specified`);
|
|
|
|
}
|
|
|
|
if (route.path.startsWith('/')) {
|
2016-06-19 17:44:20 -04:00
|
|
|
throw new Error(
|
|
|
|
`Invalid route configuration of route '${route.path}': path cannot start with a slash`);
|
2016-06-16 17:36:51 -04:00
|
|
|
}
|
2016-08-16 16:40:28 -04:00
|
|
|
if (route.path === '' && route.redirectTo !== undefined && route.pathMatch === undefined) {
|
2016-06-27 23:10:36 -04:00
|
|
|
const exp =
|
|
|
|
`The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
|
|
|
|
throw new Error(
|
|
|
|
`Invalid route configuration of route '{path: "${route.path}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
|
|
|
|
}
|
2016-07-29 12:59:50 -04:00
|
|
|
if (route.pathMatch !== undefined && route.pathMatch !== 'full' && route.pathMatch !== 'prefix') {
|
|
|
|
throw new Error(
|
|
|
|
`Invalid configuration of route '${route.path}': pathMatch can only be set to 'prefix' or 'full'`);
|
|
|
|
}
|
2016-06-27 15:27:23 -04:00
|
|
|
}
|