angular-cn/packages/router/src/config.ts

507 lines
15 KiB
TypeScript
Raw Normal View History

/**
* @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
*/
2017-04-04 19:00:40 -04:00
import {NgModuleFactory, NgModuleRef, Type} from '@angular/core';
import {Observable} from 'rxjs';
import {EmptyOutletComponent} from './components/empty_outlet';
2016-12-02 17:09:09 -05:00
import {PRIMARY_OUTLET} from './shared';
import {UrlSegment, UrlSegmentGroup} from './url_tree';
/**
* @description
*
* Represents router configuration.
*
2016-07-06 19:19:52 -04:00
* `Routes` is an array of route configurations. Each one has the following properties:
*
* - `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.
2016-12-02 17:09:09 -05:00
* - `matcher` defines a custom strategy for path matching and supersedes `path` and `pathMatch`.
2016-07-06 19:19:52 -04:00
* - `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.
2016-12-02 17:09:09 -05:00
* - `canActivate` is an array of DI tokens used to look up CanActivate handlers. See
* `CanActivate` for more info.
* - `canActivateChild` is an array of DI tokens used to look up CanActivateChild handlers. See
* `CanActivateChild` for more info.
2016-12-02 17:09:09 -05:00
* - `canDeactivate` is an array of DI tokens used to look up CanDeactivate handlers. See
* `CanDeactivate` for more info.
* - `canLoad` is an array of DI tokens used to look up CanLoad handlers. See
* `CanLoad` 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 `Resolve` for more
2016-12-02 17:09:09 -05:00
* info.
2017-06-04 22:32:46 -04:00
* - `runGuardsAndResolvers` defines when guards and resolvers will be run. By default they run only
* when the matrix parameters of the route change. Options include:
* - `paramsChange` (default) - Run guards and resolvers when path or matrix params change. This
* mode ignores query param changes.
* - `paramsOrQueryParamsChange` - Guards and resolvers will run when any parameters change. This
* includes path, matrix, and query params.
* - `pathParamsChange` Run guards and resolvers path or any path params change. This mode is
* useful if you want to ignore changes to all optional parameters such as query *and* matrix
* params.
* - `always` - Run guards and resolvers on every navigation.
2016-07-06 19:19:52 -04:00
* - `children` is an array of child route definitions.
* - `loadChildren` is a reference to lazy loaded child routes. See `LoadChildren` for more
2016-12-02 17:09:09 -05:00
* info.
2016-07-06 19:19:52 -04:00
*
* @usageNotes
2016-07-06 19:19:52 -04:00
* ### Simple Configuration
*
* ```
* [{
* path: 'team/:id',
2016-12-02 17:09:09 -05:00
* component: Team,
* children: [{
* path: 'user/:name',
* component: User
* }]
2016-07-06 19:19:52 -04:00
* }]
* ```
*
* 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
2016-12-02 17:09:09 -05:00
* }, {
2016-07-06 19:19:52 -04:00
* path: 'chat/:user',
* component: Chat
* 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,
2016-12-02 17:09:09 -05:00
* children: [{
* path: 'legacy/user/:name',
* redirectTo: 'user/:name'
* }, {
* path: 'user/:name',
* component: User
* }]
2016-07-06 19:19:52 -04:00
* }]
* ```
*
* 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
*
* 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,
2016-12-02 17:09:09 -05:00
* children: [{
* path: '',
* component: AllUsers
* }, {
* path: 'user/:name',
* component: User
* }]
2016-07-06 19:19:52 -04:00
* }]
* ```
*
* When navigating to `/team/11`, the router will instantiate the AllUsers component.
*
* Empty-path routes can have children.
*
* ```
* [{
* path: 'team/:id',
* component: Team,
2016-12-02 17:09:09 -05:00
* children: [{
* path: '',
* component: WrapperCmp,
* children: [{
* path: 'user/:name',
* component: User
* }]
* }]
2016-07-06 19:19:52 -04:00
* }]
* ```
*
* When navigating to `/team/11/user/jim`, the router will instantiate the wrapper component with
* the user component in it.
*
* 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'
2016-12-02 17:09:09 -05:00
* }, {
2016-07-06 19:19:52 -04:00
* 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'
2016-12-02 17:09:09 -05:00
* }, {
2016-07-06 19:19:52 -04:00
* 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
* 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.
*
* ### 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
2016-12-02 17:09:09 -05:00
* 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'.
2016-12-02 17:09:09 -05:00
* Then it will extract the set of routes defined in that NgModule, and will transparently add
* those routes to the main configuration.
*
* @publicApi
2016-07-06 19:19:52 -04:00
*/
export type Routes = Route[];
/**
* @description Represents the results of the URL matching.
*
* * `consumed` is an array of the consumed URL segments.
* * `posParams` is a map of positional parameters.
*
* @publicApi
*/
export type UrlMatchResult = {
consumed: UrlSegment[]; posParams?: {[name: string]: UrlSegment};
};
/**
* @description
*
* A function matching URLs
*
* 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.
*
* ```
* export function htmlFiles(url: UrlSegment[]) {
* return url.length === 1 && url[0].path.endsWith('.html') ? ({consumed: url}) : null;
* }
*
* export const routes = [{ matcher: htmlFiles, component: AnyComponent }];
* ```
*
* @publicApi
*/
export type UrlMatcher = (segments: UrlSegment[], group: UrlSegmentGroup, route: Route) =>
UrlMatchResult;
2016-07-06 19:19:52 -04:00
/**
* @description
*
* Represents the static data associated with a particular route.
*
* See `Routes` for more details.
*
* @publicApi
*/
export type Data = {
[name: string]: any
};
/**
* @description
*
* Represents the resolved data associated with a particular route.
*
* See `Routes` for more details.
*
* @publicApi
*/
export type ResolveData = {
[name: string]: any
};
2016-05-23 19:14:23 -04:00
/**
* @description
*
* The type of `loadChildren`.
*
* See `Routes` for more details.
*
* @publicApi
*/
export type LoadChildrenCallback = () =>
Type<any>| NgModuleFactory<any>| Promise<Type<any>>| Observable<Type<any>>;
/**
* @description
*
* The type of `loadChildren`.
*
* See `Routes` for more details.
*
* @publicApi
*/
export type LoadChildren = string | LoadChildrenCallback;
/**
* @description
*
* The type of `queryParamsHandling`.
*
* See `RouterLink` for more details.
*
*/
export type QueryParamsHandling = 'merge' | 'preserve' | '';
/**
* @description
*
* The type of `runGuardsAndResolvers`.
*
* See `Routes` for more details.
* @publicApi
*/
export type RunGuardsAndResolvers =
'pathParamsChange' | 'paramsChange' | 'paramsOrQueryParamsChange' | 'always';
/**
* See `Routes` for more details.
*
* @publicApi
*/
2016-05-23 19:14:23 -04:00
export interface Route {
path?: string;
pathMatch?: string;
matcher?: UrlMatcher;
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[];
canActivateChild?: any[];
2016-06-07 12:50:35 -04:00
canDeactivate?: any[];
2016-07-26 17:39:02 -04:00
canLoad?: any[];
data?: Data;
resolve?: ResolveData;
children?: Routes;
loadChildren?: LoadChildren;
runGuardsAndResolvers?: RunGuardsAndResolvers;
/**
* Filled for routes with `loadChildren` once the module has been loaded
* @internal
*/
2017-04-04 19:00:40 -04:00
_loadedConfig?: LoadedRouterConfig;
}
export class LoadedRouterConfig {
constructor(public routes: Route[], public module: NgModuleRef<any>) {}
}
export function validateConfig(config: Routes, parentPath: string = ''): void {
// forEach doesn't iterate undefined values
for (let i = 0; i < config.length; i++) {
const route: Route = config[i];
const fullPath: string = getFullPath(parentPath, route);
validateNode(route, fullPath);
}
}
function validateNode(route: Route, fullPath: string): void {
if (!route) {
throw new Error(`
Invalid configuration of route '${fullPath}': 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 }
];
`);
}
if (Array.isArray(route)) {
throw new Error(`Invalid configuration of route '${fullPath}': Array cannot be specified`);
}
if (!route.component && !route.children && !route.loadChildren &&
(route.outlet && route.outlet !== PRIMARY_OUTLET)) {
throw new Error(
`Invalid configuration of route '${fullPath}': a componentless route without children or loadChildren cannot have a named outlet set`);
}
2016-12-02 17:09:09 -05:00
if (route.redirectTo && route.children) {
throw new Error(
`Invalid configuration of route '${fullPath}': redirectTo and children cannot be used together`);
}
2016-12-02 17:09:09 -05:00
if (route.redirectTo && route.loadChildren) {
throw new Error(
`Invalid configuration of route '${fullPath}': redirectTo and loadChildren cannot be used together`);
}
2016-12-02 17:09:09 -05:00
if (route.children && route.loadChildren) {
throw new Error(
`Invalid configuration of route '${fullPath}': children and loadChildren cannot be used together`);
}
2016-12-02 17:09:09 -05:00
if (route.redirectTo && route.component) {
throw new Error(
`Invalid configuration of route '${fullPath}': redirectTo and component cannot be used together`);
}
2016-12-02 17:09:09 -05:00
if (route.path && route.matcher) {
throw new Error(
`Invalid configuration of route '${fullPath}': path and matcher cannot be used together`);
}
2016-12-02 17:09:09 -05:00
if (route.redirectTo === void 0 && !route.component && !route.children && !route.loadChildren) {
throw new Error(
`Invalid configuration of route '${fullPath}'. One of the following must be provided: component, redirectTo, children or loadChildren`);
}
if (route.path === void 0 && route.matcher === void 0) {
throw new Error(
`Invalid configuration of route '${fullPath}': routes must have either a path or a matcher specified`);
}
if (typeof route.path === 'string' && route.path.charAt(0) === '/') {
throw new Error(`Invalid configuration of route '${fullPath}': path cannot start with a slash`);
}
2016-12-02 17:09:09 -05:00
if (route.path === '' && route.redirectTo !== void 0 && route.pathMatch === void 0) {
const exp =
`The default value of 'pathMatch' is 'prefix', but often the intent is to use 'full'.`;
throw new Error(
`Invalid configuration of route '{path: "${fullPath}", redirectTo: "${route.redirectTo}"}': please provide 'pathMatch'. ${exp}`);
}
2016-12-02 17:09:09 -05:00
if (route.pathMatch !== void 0 && route.pathMatch !== 'full' && route.pathMatch !== 'prefix') {
throw new Error(
`Invalid configuration of route '${fullPath}': pathMatch can only be set to 'prefix' or 'full'`);
}
if (route.children) {
validateConfig(route.children, fullPath);
}
}
function getFullPath(parentPath: string, currentRoute: Route): string {
if (!currentRoute) {
return parentPath;
}
if (!parentPath && !currentRoute.path) {
return '';
} else if (parentPath && !currentRoute.path) {
return `${parentPath}/`;
} else if (!parentPath && currentRoute.path) {
return currentRoute.path;
} else {
return `${parentPath}/${currentRoute.path}`;
}
}
/**
* Makes a copy of the config and adds any default required properties.
*/
export function standardizeConfig(r: Route): Route {
const children = r.children && r.children.map(standardizeConfig);
const c = children ? {...r, children} : {...r};
if (!c.component && (children || c.loadChildren) && (c.outlet && c.outlet !== PRIMARY_OUTLET)) {
c.component = EmptyOutletComponent;
}
return c;
}