refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
38
packages/router/.babelrc
Normal file
38
packages/router/.babelrc
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/core": "ng.core",
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/platform-browser": "ng.platformBrowser",
|
||||
"@angular/router": "ng.router",
|
||||
|
||||
"rxjs/BehaviorSubject": "Rx",
|
||||
"rxjs/Observable": "Rx",
|
||||
"rxjs/Subject": "Rx",
|
||||
"rxjs/Subscription": "Rx",
|
||||
"rxjs/util/EmptyError": "Rx",
|
||||
|
||||
"rxjs/observable/from": "Rx.Observable",
|
||||
"rxjs/observable/fromPromise": "Rx.Observable",
|
||||
"rxjs/observable/forkJoin": "Rx.Observable",
|
||||
"rxjs/observable/of": "Rx.Observable",
|
||||
|
||||
"rxjs/operator/toPromise": "Rx.Observable.prototype",
|
||||
"rxjs/operator/map": "Rx.Observable.prototype",
|
||||
"rxjs/operator/mergeAll": "Rx.Observable.prototype",
|
||||
"rxjs/operator/concatAll": "Rx.Observable.prototype",
|
||||
"rxjs/operator/mergeMap": "Rx.Observable.prototype",
|
||||
"rxjs/operator/reduce": "Rx.Observable.prototype",
|
||||
"rxjs/operator/every": "Rx.Observable.prototype",
|
||||
"rxjs/operator/first": "Rx.Observable.prototype",
|
||||
"rxjs/operator/catch": "Rx.Observable.prototype",
|
||||
"rxjs/operator/last": "Rx.Observable.prototype",
|
||||
"rxjs/operator/filter": "Rx.Observable.prototype",
|
||||
"rxjs/operator/concatMap": "Rx.Observable.prototype"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/router"
|
||||
}
|
16
packages/router/.babelrc-testing
Normal file
16
packages/router/.babelrc-testing
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/core": "ng.core",
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/common/testing": "ng.common.testing",
|
||||
"@angular/platform-browser": "ng.platformBrowser",
|
||||
"@angular/router": "ng.router",
|
||||
"@angular/router/testing": "ng.router.testing"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/router/testing"
|
||||
}
|
15
packages/router/.babelrc-upgrade
Normal file
15
packages/router/.babelrc-upgrade
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": [["transform-es2015-modules-umd", {
|
||||
"globals": {
|
||||
"@angular/core": "ng.core",
|
||||
"@angular/common": "ng.common",
|
||||
"@angular/router": "ng.router",
|
||||
"@angular/router/upgrade": "ng.router.upgrade",
|
||||
"@angular/upgrade/static": "ng.upgrade.static"
|
||||
},
|
||||
"exactGlobals": true
|
||||
}]],
|
||||
"moduleId": "@angular/router/upgrade"
|
||||
}
|
6
packages/router/.gitignore
vendored
Normal file
6
packages/router/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
typings/
|
||||
npm-debug.log
|
||||
.idea/
|
||||
package
|
147
packages/router/CHANGELOG.md
Normal file
147
packages/router/CHANGELOG.md
Normal file
@ -0,0 +1,147 @@
|
||||
# 3.0.0-rc.2 (2016-08-31)
|
||||
|
||||
## Features
|
||||
* feat(router): use ES modules for primary build in the npm package ([#11120](https://github.com/angular/angular/issues/11120)) ([9796579](https://github.com/angular/angular/commit/9796579))
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* fix(router): use encodeUri/decodeUri to encode fragment ([bb9dfbc](https://github.com/angular/angular/commit/bb9dfbc))
|
||||
* fix(router): add an option to disable initial navigation ([a2deafc](https://github.com/angular/angular/commit/a2deafc))
|
||||
* fix(router): canLoad should cancel a navigation instead of failing it ([#11001](https://github.com/angular/angular/issues/11001)) ([f1ce760](https://github.com/angular/angular/commit/f1ce760))
|
||||
* fix(router): do not use rx/add/operator ([c350ba2](https://github.com/angular/angular/commit/c350ba2))
|
||||
* fix(router): fix the order of guards, so canActivateChild runs before canActivate ([0bb516f](https://github.com/angular/angular/commit/0bb516f))
|
||||
* fix(router): lazy loading keeps refetching modules ([#10707](https://github.com/angular/angular/issues/10707)) ([cc6749c](https://github.com/angular/angular/commit/cc6749c))
|
||||
* fix(router): location changes and redirects break the back button ([#10742](https://github.com/angular/angular/issues/10742)) ([04c6b2f](https://github.com/angular/angular/commit/04c6b2f))
|
||||
* fix(router): make routerLinkActiveOptions public ([#10758](https://github.com/angular/angular/issues/10758)) ([73c0a9d](https://github.com/angular/angular/commit/73c0a9d))
|
||||
* fix(router): support guards navigating synchronously ([#11150](https://github.com/angular/angular/issues/11150)) ([e2241a2](https://github.com/angular/angular/commit/e2241a2))
|
||||
* fix(router): support relative param-only navigation ([#10613](https://github.com/angular/angular/issues/10613)) ([c7f3aa7](https://github.com/angular/angular/commit/c7f3aa7))
|
||||
* fix(router): update the location before activating components ([2ffecc0](https://github.com/angular/angular/commit/2ffecc0))
|
||||
* fix(router): fix type ([#11181](https://github.com/angular/angular/issues/11181)) ([0f68351](https://github.com/angular/angular/commit/0f68351))
|
||||
* fix(router): merge artifacts ([fc1e45d](https://github.com/angular/angular/commit/fc1e45d)), closes [#11063](https://github.com/angular/angular/issues/11063) [#11102](https://github.com/angular/angular/issues/11102)
|
||||
* fix(router): correct RxJS mapping in rollup config for umd/es5 bundles ([174c016](https://github.com/angular/angular/commit/174c016))
|
||||
|
||||
|
||||
|
||||
# 3.0.0-rc.1 (2016-08-09)
|
||||
|
||||
## Features
|
||||
* feat(router): add support for lazily loaded modules
|
||||
* feat(router): empty-path routes should inherit matrix params
|
||||
* feat(router): add activate and deactivate events to RouterOutlet
|
||||
* feat(router): update routerLink DSL to handle aux routes
|
||||
* feat(router): add support for canActivateChild
|
||||
* feat(router): guards and data resolvers can now return promises
|
||||
* feat(router): rename PRIMARY_OUTLET into primary
|
||||
* feat(router): rename UrlPathWithParams into UrlSegment
|
||||
* feat(router): implement canLoad
|
||||
* feat(router): take advantage of the new way of configuring modules
|
||||
* feat(router): ActivateRoute should expose its route config
|
||||
* feat(router): add isActive to router
|
||||
* feat(router): add a validation to make sure pathMatch is set correctly
|
||||
* feat(router): add parent, children, firstChild to ActivatedRoute
|
||||
* feat(router): add queryParams and fragment to every activated route
|
||||
* feat(router): add route.root returning the root of router state
|
||||
|
||||
## Bug Fixes
|
||||
* fix(router): update links when query params change
|
||||
* fix(router): handle router outlets in ngIf
|
||||
* fix(router): encode/decode params and path segments
|
||||
* fix(router): disallow root segments with matrix params
|
||||
* fix(router): update current state and url before activating components
|
||||
* fix(router): do not fire events on 'duplicate' location events
|
||||
* fix(router): freeze params and queryParams to prevent common source of errors
|
||||
* fix(router): expose initalNavigation
|
||||
* fix(router): back button does not work in IE11 and Safari
|
||||
* fix(router): navigation should not preserve query params and fragment by default
|
||||
* fix(router): routerLinkActive should only set classes after the router has successfully navigated
|
||||
* fix(router): handle urls with only secondary top-level segments
|
||||
* fix(router): router link active should take all descendants into account
|
||||
* fix(router): handle when both primary and secondary are empty-path routes have children
|
||||
* fix(router): updates router module to be offline-compilation friendly
|
||||
* fix(router): relax type defintion of Route to improve dev ergonomics)
|
||||
* fix(router): make an outlet to unregister itself when it is removed from the DOM
|
||||
* fix(router): add segmentPath to the link DSL
|
||||
* fix(router): absolute redirects should work with lazy loading
|
||||
* fix(router): fix matrix params check to handle 'special' objects
|
||||
* fix(router): support outlets in non-absolute positions
|
||||
* fix(router): route.parent should work for secondary children
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
* PRIMARY_OUTLET got renamed into 'primary'
|
||||
* UrlPathWithParams got renamed into UrlSegment
|
||||
* Query params and fragment are not longer preserved by default
|
||||
|
||||
# 3.0.0-beta.2 (2016-06-30)
|
||||
|
||||
## Bug Fixes
|
||||
* fix(router): remove private and internal annotations
|
||||
* fix(router): remove the precompile warning
|
||||
|
||||
# 3.0.0-beta.1 (2016-06-30)
|
||||
|
||||
## Features
|
||||
* feat(router): make router links work on non-a tags
|
||||
* feat(router): add pathMatch property to replace terminal
|
||||
* feat(router): use componentFactoryResolver
|
||||
* feat(router): implement data and resolve
|
||||
|
||||
## Bug Fixes
|
||||
* fix(router): fix RouterLinkActive to handle the case when the link has extra paths
|
||||
* fix(router): redirect should not add unnecessary brackets
|
||||
* fix(router): reexport router directives
|
||||
* fix(router): make the constructor of the router service public
|
||||
* fix(router): top-levels do not work in ngIf
|
||||
* fix(router): canceled navigations should return a promise that is resolved with false
|
||||
* fix(router): handle empty path with query params
|
||||
* fix(router): preserve fragment on initial load
|
||||
|
||||
# 3.0.0-alpha.8 (2016-06-24)
|
||||
|
||||
## Features
|
||||
* feat(router): add support for componentless routes
|
||||
* feat(router): add UMD bundles
|
||||
|
||||
## Bug Fixes
|
||||
* fix(router): handle path:'' redirects and matches
|
||||
* fix(router): wildcard don't get notified on url changes
|
||||
* fix(router): default exact to false in routerLinkActiveOptions
|
||||
* fix(router): doesn't throw on canDeactivate when a route hasn't advanced
|
||||
|
||||
# 3.0.0-alpha.7 (2016-06-17)
|
||||
|
||||
## Features
|
||||
* feat(router): add route config validation
|
||||
* feat(router): do not support paths starting with /
|
||||
* feat(router): drop index property
|
||||
|
||||
## Bug Fixes
|
||||
* fix(router): stringify positional parameters when using routerLink
|
||||
* fix(router): change serialize not to require parenthesis in query string to be encoded
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
No longer supporting paths starting with /
|
||||
|
||||
BEFORE
|
||||
The following two routes were equivalent:
|
||||
{ path: '/a', component: ComponentA }
|
||||
{ path: 'a', component: ComponentA }
|
||||
|
||||
AFTER
|
||||
Only the following works:
|
||||
{ path: 'a', component: ComponentA }
|
||||
|
||||
No longer supporting index routs
|
||||
|
||||
BEFORE
|
||||
The following two routes were equivalent:
|
||||
{ path: '', component: ComponentA }
|
||||
{ index: true, component: ComponentA }
|
||||
|
||||
AFTER
|
||||
Only the following works:
|
||||
{ path: '', component: ComponentA }
|
||||
|
||||
|
||||
# 3.0.0-alpha.6 (2016-06-16)
|
21
packages/router/LICENSE
Normal file
21
packages/router/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2017 Google, Inc. http://angular.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
25
packages/router/README.md
Normal file
25
packages/router/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
Angular Router
|
||||
=========
|
||||
|
||||
Managing state transitions is one of the hardest parts of building applications. This is especially true on the web, where you also need to ensure that the state is reflected in the URL. In addition, we often want to split applications into multiple bundles and load them on demand. Doing this transparently isn’t trivial.
|
||||
|
||||
The Angular router is designed to solve these problems. Using the router, you can declaratively specify application state, manage state transitions while taking care of the URL, and load components on demand.
|
||||
|
||||
## Overview
|
||||
Read the overview of the Router [here](https://vsavkin.com/angular-2-router-d9e30599f9ea).
|
||||
|
||||
## Guide
|
||||
Read the dev guide [here](https://angular.io/docs/ts/latest/guide/router.html).
|
||||
|
||||
## Local development
|
||||
|
||||
```
|
||||
# keep @angular/router fresh
|
||||
$ ./scripts/karma.sh
|
||||
|
||||
# keep @angular/core fresh
|
||||
$ ../../../node_modules/.bin/tsc -p modules --emitDecoratorMetadata -w
|
||||
|
||||
# start karma
|
||||
$ ./scripts/karma.sh
|
||||
```
|
14
packages/router/index.ts
Normal file
14
packages/router/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
// This file is not used to build this module. It is only used during editing
|
||||
// by the TypeScript language service and during build for verification. `ngc`
|
||||
// replaces this file with production index.ts when it rewrites private symbol
|
||||
// names.
|
||||
|
||||
export * from './public_api';
|
74
packages/router/karma-test-shim.js
Normal file
74
packages/router/karma-test-shim.js
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/*global jasmine, __karma__, window*/
|
||||
Error.stackTraceLimit = 5;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
|
||||
|
||||
__karma__.loaded = function() {};
|
||||
|
||||
function isJsFile(path) {
|
||||
return path.slice(-3) == '.js';
|
||||
}
|
||||
|
||||
function isSpecFile(path) {
|
||||
return path.slice(-7) == 'spec.js';
|
||||
}
|
||||
|
||||
function isBuiltFile(path) {
|
||||
var builtPath = '/base/dist/';
|
||||
return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath);
|
||||
}
|
||||
|
||||
var allSpecFiles = Object.keys(window.__karma__.files).filter(isSpecFile).filter(isBuiltFile);
|
||||
|
||||
// Load our SystemJS configuration.
|
||||
System.config({
|
||||
baseURL: '/base',
|
||||
});
|
||||
|
||||
System.config({
|
||||
map: {'rxjs': 'node_modules/rxjs', '@angular': 'dist/all/@angular'},
|
||||
packages: {
|
||||
'@angular/core/testing': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/core': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/compiler/testing': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/compiler': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/common/testing': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/common': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/platform-browser/testing': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/platform-browser': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/platform-browser-dynamic/testing': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/platform-browser-dynamic': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/router/testing': {main: 'index.js', defaultExtension: 'js'},
|
||||
'@angular/router': {main: 'index.js', defaultExtension: 'js'},
|
||||
'rxjs': {main: 'Rx.js', defaultExtension: 'js'},
|
||||
}
|
||||
});
|
||||
|
||||
Promise
|
||||
.all([
|
||||
System.import('@angular/core/testing'),
|
||||
System.import('@angular/platform-browser-dynamic/testing')
|
||||
])
|
||||
.then(function(providers) {
|
||||
var testing = providers[0];
|
||||
var testingBrowser = providers[1];
|
||||
|
||||
testing.TestBed.initTestEnvironment(
|
||||
testingBrowser.BrowserDynamicTestingModule,
|
||||
testingBrowser.platformBrowserDynamicTesting());
|
||||
|
||||
})
|
||||
.then(function() {
|
||||
// Finally, load all spec files.
|
||||
// This will run the tests directly.
|
||||
return Promise.all(
|
||||
allSpecFiles.map(function(moduleName) { return System.import(moduleName); }));
|
||||
})
|
||||
.then(__karma__.start, (v) => console.error(v));
|
108
packages/router/karma.conf.js
Normal file
108
packages/router/karma.conf.js
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
var browserProvidersConf = require('../../../browser-providers.conf.js');
|
||||
|
||||
// Karma configuration
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
|
||||
basePath: '../../../',
|
||||
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
files: [
|
||||
// Polyfills.
|
||||
'node_modules/core-js/client/core.js',
|
||||
'node_modules/reflect-metadata/Reflect.js',
|
||||
'shims_for_IE.js',
|
||||
|
||||
// System.js for module loading
|
||||
'node_modules/systemjs/dist/system-polyfills.js',
|
||||
'node_modules/systemjs/dist/system.src.js',
|
||||
|
||||
// Zone.js dependencies
|
||||
'node_modules/zone.js/dist/zone.js',
|
||||
'node_modules/zone.js/dist/long-stack-trace-zone.js',
|
||||
'node_modules/zone.js/dist/proxy.js',
|
||||
'node_modules/zone.js/dist/sync-test.js',
|
||||
'node_modules/zone.js/dist/jasmine-patch.js',
|
||||
'node_modules/zone.js/dist/async-test.js',
|
||||
'node_modules/zone.js/dist/fake-async-test.js',
|
||||
|
||||
// RxJs.
|
||||
{pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false},
|
||||
{pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false},
|
||||
|
||||
// shim
|
||||
{pattern: 'modules/@angular/router/karma-test-shim.js', included: true, watched: true},
|
||||
|
||||
// Angular modules
|
||||
{pattern: 'dist/all/@angular/core/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/core/src/**/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/core/testing/**/*.js', included: false, watched: false},
|
||||
|
||||
{pattern: 'dist/all/@angular/common/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/common/src/**/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/common/testing/**/*.js', included: false, watched: false},
|
||||
|
||||
{pattern: 'dist/all/@angular/compiler/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/compiler/src/**/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/compiler/testing/**/*.js', included: false, watched: false},
|
||||
|
||||
{pattern: 'dist/all/@angular/platform-browser/*.js', included: false, watched: false},
|
||||
{pattern: 'dist/all/@angular/platform-browser/src/**/*.js', included: false, watched: false},
|
||||
{
|
||||
pattern: 'dist/all/@angular/platform-browser/testing/**/*.js',
|
||||
included: false,
|
||||
watched: false,
|
||||
},
|
||||
|
||||
{pattern: 'dist/all/@angular/platform-browser-dynamic/*.js', included: false, watched: false},
|
||||
{
|
||||
pattern: 'dist/all/@angular/platform-browser-dynamic/src/**/*.js',
|
||||
included: false,
|
||||
watched: false,
|
||||
},
|
||||
{
|
||||
pattern: 'dist/all/@angular/platform-browser-dynamic/testing/**/*.js',
|
||||
included: false,
|
||||
watched: false,
|
||||
},
|
||||
|
||||
// Router
|
||||
{pattern: 'dist/all/@angular/router/**/*.js', included: false, watched: true}
|
||||
],
|
||||
|
||||
customLaunchers: browserProvidersConf.customLaunchers,
|
||||
|
||||
plugins: [
|
||||
'karma-jasmine',
|
||||
'karma-browserstack-launcher',
|
||||
'karma-sauce-launcher',
|
||||
'karma-chrome-launcher',
|
||||
'karma-sourcemap-loader',
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
'**/*.js': ['sourcemap'],
|
||||
},
|
||||
|
||||
reporters: ['dots'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
captureTimeout: 60000,
|
||||
browserDisconnectTimeout: 60000,
|
||||
browserDisconnectTolerance: 3,
|
||||
browserNoActivityTimeout: 60000,
|
||||
});
|
||||
};
|
29
packages/router/package.json
Normal file
29
packages/router/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@angular/router",
|
||||
"version": "0.0.0-ROUTERPLACEHOLDER",
|
||||
"description": "Angular - the routing library",
|
||||
"main": "./bundles/router.umd.js",
|
||||
"module": "./@angular/router.es5.js",
|
||||
"es2015": "./@angular/router.js",
|
||||
"typings": "./typings/router.d.ts",
|
||||
"keywords": [
|
||||
"angular",
|
||||
"router"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/angular/angular.git"
|
||||
},
|
||||
"author": "angular",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/angular/angular/issues"
|
||||
},
|
||||
"homepage": "https://github.com/angular/angular/blob/master/modules/%40angular/router/README.md",
|
||||
"peerDependencies": {
|
||||
"@angular/core": "0.0.0-PLACEHOLDER",
|
||||
"@angular/common": "0.0.0-PLACEHOLDER",
|
||||
"@angular/platform-browser": "0.0.0-PLACEHOLDER",
|
||||
"rxjs": "^5.0.1"
|
||||
}
|
||||
}
|
16
packages/router/public_api.ts
Normal file
16
packages/router/public_api.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the router package.
|
||||
*/
|
||||
export * from './src/index';
|
||||
|
||||
// This file only reexports content of the `src` folder. Keep it that way.
|
4
packages/router/scripts/build.sh
Executable file
4
packages/router/scripts/build.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e -o pipefail
|
||||
../../../node_modules/.bin/tsc -w
|
2
packages/router/scripts/karma.sh
Executable file
2
packages/router/scripts/karma.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
../../../node_modules/.bin/karma start
|
509
packages/router/src/apply_redirects.ts
Normal file
509
packages/router/src/apply_redirects.ts
Normal file
@ -0,0 +1,509 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Injector} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Observer} from 'rxjs/Observer';
|
||||
import {from} from 'rxjs/observable/from';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
import {_catch} from 'rxjs/operator/catch';
|
||||
import {concatAll} from 'rxjs/operator/concatAll';
|
||||
import {first} from 'rxjs/operator/first';
|
||||
import {map} from 'rxjs/operator/map';
|
||||
import {mergeMap} from 'rxjs/operator/mergeMap';
|
||||
import {EmptyError} from 'rxjs/util/EmptyError';
|
||||
|
||||
import {Route, Routes} from './config';
|
||||
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
|
||||
import {PRIMARY_OUTLET, Params, defaultUrlMatcher, navigationCancelingError} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
import {andObservables, forEach, merge, waitForMap, wrapIntoObservable} from './utils/collection';
|
||||
|
||||
class NoMatch {
|
||||
constructor(public segmentGroup: UrlSegmentGroup = null) {}
|
||||
}
|
||||
|
||||
class AbsoluteRedirect {
|
||||
constructor(public urlTree: UrlTree) {}
|
||||
}
|
||||
|
||||
function noMatch(segmentGroup: UrlSegmentGroup): Observable<UrlSegmentGroup> {
|
||||
return new Observable<UrlSegmentGroup>(
|
||||
(obs: Observer<UrlSegmentGroup>) => obs.error(new NoMatch(segmentGroup)));
|
||||
}
|
||||
|
||||
function absoluteRedirect(newTree: UrlTree): Observable<any> {
|
||||
return new Observable<UrlSegmentGroup>(
|
||||
(obs: Observer<UrlSegmentGroup>) => obs.error(new AbsoluteRedirect(newTree)));
|
||||
}
|
||||
|
||||
function namedOutletsRedirect(redirectTo: string): Observable<any> {
|
||||
return new Observable<UrlSegmentGroup>(
|
||||
(obs: Observer<UrlSegmentGroup>) => obs.error(new Error(
|
||||
`Only absolute redirects can have named outlets. redirectTo: '${redirectTo}'`)));
|
||||
}
|
||||
|
||||
function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
|
||||
return new Observable<LoadedRouterConfig>(
|
||||
(obs: Observer<LoadedRouterConfig>) => obs.error(navigationCancelingError(
|
||||
`Cannot load children because the guard of the route "path: '${route.path}'" returned false`)));
|
||||
}
|
||||
|
||||
export function applyRedirects(
|
||||
injector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
|
||||
urlTree: UrlTree, config: Routes): Observable<UrlTree> {
|
||||
return new ApplyRedirects(injector, configLoader, urlSerializer, urlTree, config).apply();
|
||||
}
|
||||
|
||||
class ApplyRedirects {
|
||||
private allowRedirects: boolean = true;
|
||||
|
||||
constructor(
|
||||
private injector: Injector, private configLoader: RouterConfigLoader,
|
||||
private urlSerializer: UrlSerializer, private urlTree: UrlTree, private config: Routes) {}
|
||||
|
||||
apply(): Observable<UrlTree> {
|
||||
const expanded$ =
|
||||
this.expandSegmentGroup(this.injector, this.config, this.urlTree.root, PRIMARY_OUTLET);
|
||||
const urlTrees$ = map.call(
|
||||
expanded$, (rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree(
|
||||
rootSegmentGroup, this.urlTree.queryParams, this.urlTree.fragment));
|
||||
return _catch.call(urlTrees$, (e: any) => {
|
||||
if (e instanceof AbsoluteRedirect) {
|
||||
// after an absolute redirect we do not apply any more redirects!
|
||||
this.allowRedirects = false;
|
||||
// we need to run matching, so we can fetch all lazy-loaded modules
|
||||
return this.match(e.urlTree);
|
||||
}
|
||||
|
||||
if (e instanceof NoMatch) {
|
||||
throw this.noMatchError(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
private match(tree: UrlTree): Observable<UrlTree> {
|
||||
const expanded$ =
|
||||
this.expandSegmentGroup(this.injector, this.config, tree.root, PRIMARY_OUTLET);
|
||||
const mapped$ = map.call(
|
||||
expanded$, (rootSegmentGroup: UrlSegmentGroup) =>
|
||||
this.createUrlTree(rootSegmentGroup, tree.queryParams, tree.fragment));
|
||||
return _catch.call(mapped$, (e: any): Observable<UrlTree> => {
|
||||
if (e instanceof NoMatch) {
|
||||
throw this.noMatchError(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
private noMatchError(e: NoMatch): any {
|
||||
return new Error(`Cannot match any routes. URL Segment: '${e.segmentGroup}'`);
|
||||
}
|
||||
|
||||
private createUrlTree(rootCandidate: UrlSegmentGroup, queryParams: Params, fragment: string):
|
||||
UrlTree {
|
||||
const root = rootCandidate.segments.length > 0 ?
|
||||
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
|
||||
rootCandidate;
|
||||
return new UrlTree(root, queryParams, fragment);
|
||||
}
|
||||
|
||||
private expandSegmentGroup(
|
||||
injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup,
|
||||
outlet: string): Observable<UrlSegmentGroup> {
|
||||
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
|
||||
return map.call(
|
||||
this.expandChildren(injector, routes, segmentGroup),
|
||||
(children: any) => new UrlSegmentGroup([], children));
|
||||
}
|
||||
|
||||
return this.expandSegment(injector, segmentGroup, routes, segmentGroup.segments, outlet, true);
|
||||
}
|
||||
|
||||
private expandChildren(injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup):
|
||||
Observable<{[name: string]: UrlSegmentGroup}> {
|
||||
return waitForMap(
|
||||
segmentGroup.children,
|
||||
(childOutlet, child) => this.expandSegmentGroup(injector, routes, child, childOutlet));
|
||||
}
|
||||
|
||||
private expandSegment(
|
||||
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], segments: UrlSegment[],
|
||||
outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
|
||||
const routes$ = of (...routes);
|
||||
const processedRoutes$ = map.call(routes$, (r: any) => {
|
||||
const expanded$ = this.expandSegmentAgainstRoute(
|
||||
injector, segmentGroup, routes, r, segments, outlet, allowRedirects);
|
||||
return _catch.call(expanded$, (e: any) => {
|
||||
if (e instanceof NoMatch) {
|
||||
return of (null);
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
const concattedProcessedRoutes$ = concatAll.call(processedRoutes$);
|
||||
const first$ = first.call(concattedProcessedRoutes$, (s: any) => !!s);
|
||||
return _catch.call(first$, (e: any, _: any): Observable<UrlSegmentGroup> => {
|
||||
if (e instanceof EmptyError) {
|
||||
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
||||
return of (new UrlSegmentGroup([], {}));
|
||||
}
|
||||
|
||||
throw new NoMatch(segmentGroup);
|
||||
}
|
||||
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
|
||||
boolean {
|
||||
return segments.length === 0 && !segmentGroup.children[outlet];
|
||||
}
|
||||
|
||||
private expandSegmentAgainstRoute(
|
||||
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
|
||||
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
|
||||
if (getOutlet(route) !== outlet) {
|
||||
return noMatch(segmentGroup);
|
||||
}
|
||||
|
||||
if (route.redirectTo !== undefined && !(allowRedirects && this.allowRedirects)) {
|
||||
return noMatch(segmentGroup);
|
||||
}
|
||||
|
||||
if (route.redirectTo === undefined) {
|
||||
return this.matchSegmentAgainstRoute(injector, segmentGroup, route, paths);
|
||||
}
|
||||
|
||||
return this.expandSegmentAgainstRouteUsingRedirect(
|
||||
injector, segmentGroup, routes, route, paths, outlet);
|
||||
}
|
||||
|
||||
private expandSegmentAgainstRouteUsingRedirect(
|
||||
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
|
||||
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
|
||||
if (route.path === '**') {
|
||||
return this.expandWildCardWithParamsAgainstRouteUsingRedirect(
|
||||
injector, routes, route, outlet);
|
||||
}
|
||||
|
||||
return this.expandRegularSegmentAgainstRouteUsingRedirect(
|
||||
injector, segmentGroup, routes, route, segments, outlet);
|
||||
}
|
||||
|
||||
private expandWildCardWithParamsAgainstRouteUsingRedirect(
|
||||
injector: Injector, routes: Route[], route: Route,
|
||||
outlet: string): Observable<UrlSegmentGroup> {
|
||||
const newTree = this.applyRedirectCommands([], route.redirectTo, {});
|
||||
if (route.redirectTo.startsWith('/')) {
|
||||
return absoluteRedirect(newTree);
|
||||
}
|
||||
|
||||
return mergeMap.call(this.lineralizeSegments(route, newTree), (newSegments: UrlSegment[]) => {
|
||||
const group = new UrlSegmentGroup(newSegments, {});
|
||||
return this.expandSegment(injector, group, routes, newSegments, outlet, false);
|
||||
});
|
||||
}
|
||||
|
||||
private expandRegularSegmentAgainstRouteUsingRedirect(
|
||||
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
|
||||
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
|
||||
const {matched, consumedSegments, lastChild, positionalParamSegments} =
|
||||
match(segmentGroup, route, segments);
|
||||
if (!matched) return noMatch(segmentGroup);
|
||||
|
||||
const newTree = this.applyRedirectCommands(
|
||||
consumedSegments, route.redirectTo, <any>positionalParamSegments);
|
||||
if (route.redirectTo.startsWith('/')) {
|
||||
return absoluteRedirect(newTree);
|
||||
}
|
||||
|
||||
return mergeMap.call(this.lineralizeSegments(route, newTree), (newSegments: UrlSegment[]) => {
|
||||
return this.expandSegment(
|
||||
injector, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet,
|
||||
false);
|
||||
});
|
||||
}
|
||||
|
||||
private matchSegmentAgainstRoute(
|
||||
injector: Injector, rawSegmentGroup: UrlSegmentGroup, route: Route,
|
||||
segments: UrlSegment[]): Observable<UrlSegmentGroup> {
|
||||
if (route.path === '**') {
|
||||
if (route.loadChildren) {
|
||||
return map.call(this.configLoader.load(injector, route), (cfg: LoadedRouterConfig) => {
|
||||
(<any>route)._loadedConfig = cfg;
|
||||
return new UrlSegmentGroup(segments, {});
|
||||
});
|
||||
}
|
||||
|
||||
return of (new UrlSegmentGroup(segments, {}));
|
||||
}
|
||||
|
||||
const {matched, consumedSegments, lastChild} = match(rawSegmentGroup, route, segments);
|
||||
if (!matched) return noMatch(rawSegmentGroup);
|
||||
|
||||
const rawSlicedSegments = segments.slice(lastChild);
|
||||
const childConfig$ = this.getChildConfig(injector, route);
|
||||
return mergeMap.call(childConfig$, (routerConfig: LoadedRouterConfig) => {
|
||||
const childInjector = routerConfig.injector;
|
||||
const childConfig = routerConfig.routes;
|
||||
const {segmentGroup, slicedSegments} =
|
||||
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
|
||||
|
||||
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
|
||||
const expanded$ = this.expandChildren(childInjector, childConfig, segmentGroup);
|
||||
return map.call(
|
||||
expanded$, (children: any) => new UrlSegmentGroup(consumedSegments, children));
|
||||
}
|
||||
|
||||
if (childConfig.length === 0 && slicedSegments.length === 0) {
|
||||
return of (new UrlSegmentGroup(consumedSegments, {}));
|
||||
}
|
||||
|
||||
const expanded$ = this.expandSegment(
|
||||
childInjector, segmentGroup, childConfig, slicedSegments, PRIMARY_OUTLET, true);
|
||||
return map.call(
|
||||
expanded$, (cs: UrlSegmentGroup) =>
|
||||
new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children));
|
||||
});
|
||||
}
|
||||
|
||||
private getChildConfig(injector: Injector, route: Route): Observable<LoadedRouterConfig> {
|
||||
if (route.children) {
|
||||
return of (new LoadedRouterConfig(route.children, injector, null, null));
|
||||
}
|
||||
|
||||
if (route.loadChildren) {
|
||||
return mergeMap.call(runGuards(injector, route), (shouldLoad: any) => {
|
||||
|
||||
if (shouldLoad) {
|
||||
return (<any>route)._loadedConfig ?
|
||||
of ((<any>route)._loadedConfig) :
|
||||
map.call(this.configLoader.load(injector, route), (cfg: LoadedRouterConfig) => {
|
||||
(<any>route)._loadedConfig = cfg;
|
||||
return cfg;
|
||||
});
|
||||
}
|
||||
|
||||
return canLoadFails(route);
|
||||
});
|
||||
}
|
||||
|
||||
return of (new LoadedRouterConfig([], injector, null, null));
|
||||
}
|
||||
|
||||
private lineralizeSegments(route: Route, urlTree: UrlTree): Observable<UrlSegment[]> {
|
||||
let res: UrlSegment[] = [];
|
||||
let c = urlTree.root;
|
||||
while (true) {
|
||||
res = res.concat(c.segments);
|
||||
if (c.numberOfChildren === 0) {
|
||||
return of (res);
|
||||
}
|
||||
|
||||
if (c.numberOfChildren > 1 || !c.children[PRIMARY_OUTLET]) {
|
||||
return namedOutletsRedirect(route.redirectTo);
|
||||
}
|
||||
|
||||
c = c.children[PRIMARY_OUTLET];
|
||||
}
|
||||
}
|
||||
|
||||
private applyRedirectCommands(
|
||||
segments: UrlSegment[], redirectTo: string, posParams: {[k: string]: UrlSegment}): UrlTree {
|
||||
return this.applyRedirectCreatreUrlTree(
|
||||
redirectTo, this.urlSerializer.parse(redirectTo), segments, posParams);
|
||||
}
|
||||
|
||||
private applyRedirectCreatreUrlTree(
|
||||
redirectTo: string, urlTree: UrlTree, segments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment}): UrlTree {
|
||||
const newRoot = this.createSegmentGroup(redirectTo, urlTree.root, segments, posParams);
|
||||
return new UrlTree(
|
||||
newRoot, this.createQueryParams(urlTree.queryParams, this.urlTree.queryParams),
|
||||
urlTree.fragment);
|
||||
}
|
||||
|
||||
private createQueryParams(redirectToParams: Params, actualParams: Params): Params {
|
||||
const res: Params = {};
|
||||
forEach(redirectToParams, (v: any, k: string) => {
|
||||
res[k] = v.startsWith(':') ? actualParams[v.substring(1)] : v;
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
private createSegmentGroup(
|
||||
redirectTo: string, group: UrlSegmentGroup, segments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment}): UrlSegmentGroup {
|
||||
const updatedSegments = this.createSegments(redirectTo, group.segments, segments, posParams);
|
||||
|
||||
let children: {[n: string]: UrlSegmentGroup} = {};
|
||||
forEach(group.children, (child: UrlSegmentGroup, name: string) => {
|
||||
children[name] = this.createSegmentGroup(redirectTo, child, segments, posParams);
|
||||
});
|
||||
|
||||
return new UrlSegmentGroup(updatedSegments, children);
|
||||
}
|
||||
|
||||
private createSegments(
|
||||
redirectTo: string, redirectToSegments: UrlSegment[], actualSegments: UrlSegment[],
|
||||
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
|
||||
return redirectToSegments.map(
|
||||
s => s.path.startsWith(':') ? this.findPosParam(redirectTo, s, posParams) :
|
||||
this.findOrReturn(s, actualSegments));
|
||||
}
|
||||
|
||||
private findPosParam(
|
||||
redirectTo: string, redirectToUrlSegment: UrlSegment,
|
||||
posParams: {[k: string]: UrlSegment}): UrlSegment {
|
||||
const pos = posParams[redirectToUrlSegment.path.substring(1)];
|
||||
if (!pos)
|
||||
throw new Error(
|
||||
`Cannot redirect to '${redirectTo}'. Cannot find '${redirectToUrlSegment.path}'.`);
|
||||
return pos;
|
||||
}
|
||||
|
||||
private findOrReturn(redirectToUrlSegment: UrlSegment, actualSegments: UrlSegment[]): UrlSegment {
|
||||
let idx = 0;
|
||||
for (const s of actualSegments) {
|
||||
if (s.path === redirectToUrlSegment.path) {
|
||||
actualSegments.splice(idx);
|
||||
return s;
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
return redirectToUrlSegment;
|
||||
}
|
||||
}
|
||||
|
||||
function runGuards(injector: Injector, route: Route): Observable<boolean> {
|
||||
const canLoad = route.canLoad;
|
||||
if (!canLoad || canLoad.length === 0) return of (true);
|
||||
|
||||
const obs = map.call(from(canLoad), (c: any) => {
|
||||
const guard = injector.get(c);
|
||||
return wrapIntoObservable(guard.canLoad ? guard.canLoad(route) : guard(route));
|
||||
});
|
||||
|
||||
return andObservables(obs);
|
||||
}
|
||||
|
||||
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): {
|
||||
matched: boolean,
|
||||
consumedSegments: UrlSegment[],
|
||||
lastChild: number,
|
||||
positionalParamSegments: {[k: string]: UrlSegment}
|
||||
} {
|
||||
const noMatch =
|
||||
{matched: false, consumedSegments: <any[]>[], lastChild: 0, positionalParamSegments: {}};
|
||||
if (route.path === '') {
|
||||
if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) {
|
||||
return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
|
||||
}
|
||||
|
||||
return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
|
||||
}
|
||||
|
||||
const matcher = route.matcher || defaultUrlMatcher;
|
||||
const res = matcher(segments, segmentGroup, route);
|
||||
if (!res) return noMatch;
|
||||
|
||||
return {
|
||||
matched: true,
|
||||
consumedSegments: res.consumed,
|
||||
lastChild: res.consumed.length,
|
||||
positionalParamSegments: res.posParams
|
||||
};
|
||||
}
|
||||
|
||||
function split(
|
||||
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
||||
config: Route[]) {
|
||||
if (slicedSegments.length > 0 &&
|
||||
containsEmptyPathRedirectsWithNamedOutlets(segmentGroup, slicedSegments, config)) {
|
||||
const s = new UrlSegmentGroup(
|
||||
consumedSegments, createChildrenForEmptySegments(
|
||||
config, new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
|
||||
return {segmentGroup: mergeTrivialChildren(s), slicedSegments: []};
|
||||
}
|
||||
|
||||
if (slicedSegments.length === 0 &&
|
||||
containsEmptyPathRedirects(segmentGroup, slicedSegments, config)) {
|
||||
const s = new UrlSegmentGroup(
|
||||
segmentGroup.segments, addEmptySegmentsToChildrenIfNeeded(
|
||||
segmentGroup, slicedSegments, config, segmentGroup.children));
|
||||
return {segmentGroup: mergeTrivialChildren(s), slicedSegments};
|
||||
}
|
||||
|
||||
return {segmentGroup, slicedSegments};
|
||||
}
|
||||
|
||||
function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
|
||||
if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) {
|
||||
const c = s.children[PRIMARY_OUTLET];
|
||||
return new UrlSegmentGroup(s.segments.concat(c.segments), c.children);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function addEmptySegmentsToChildrenIfNeeded(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
|
||||
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
|
||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||
for (const r of routes) {
|
||||
if (emptyPathRedirect(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
|
||||
res[getOutlet(r)] = new UrlSegmentGroup([], {});
|
||||
}
|
||||
}
|
||||
return merge(children, res);
|
||||
}
|
||||
|
||||
function createChildrenForEmptySegments(
|
||||
routes: Route[], primarySegmentGroup: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
|
||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||
res[PRIMARY_OUTLET] = primarySegmentGroup;
|
||||
for (const r of routes) {
|
||||
if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) {
|
||||
res[getOutlet(r)] = new UrlSegmentGroup([], {});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function containsEmptyPathRedirectsWithNamedOutlets(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
||||
return routes
|
||||
.filter(
|
||||
r => emptyPathRedirect(segmentGroup, slicedSegments, r) &&
|
||||
getOutlet(r) !== PRIMARY_OUTLET)
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
function containsEmptyPathRedirects(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
||||
return routes.filter(r => emptyPathRedirect(segmentGroup, slicedSegments, r)).length > 0;
|
||||
}
|
||||
|
||||
function emptyPathRedirect(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
|
||||
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return r.path === '' && r.redirectTo !== undefined;
|
||||
}
|
||||
|
||||
function getOutlet(route: Route): string {
|
||||
return route.outlet ? route.outlet : PRIMARY_OUTLET;
|
||||
}
|
454
packages/router/src/config.ts
Normal file
454
packages/router/src/config.ts
Normal file
@ -0,0 +1,454 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {NgModuleFactory, Type} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
||||
import {PRIMARY_OUTLET} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents router configuration.
|
||||
*
|
||||
* @description
|
||||
* `Routes` is an array of route configurations. Each one has the following properties:
|
||||
*
|
||||
* - `path` is a string that uses the route matcher DSL.
|
||||
* - `pathMatch` is a string that specifies the matching strategy.
|
||||
* - `matcher` defines a custom strategy for path matching and supersedes `path` and `pathMatch`.
|
||||
* See {@link UrlMatcher} for more info.
|
||||
* - `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 CanActivate} for more info.
|
||||
* - `canActivateChild` is an array of DI tokens used to look up CanActivateChild handlers. See
|
||||
* {@link CanActivateChild} for more info.
|
||||
* - `canDeactivate` is an array of DI tokens used to look up CanDeactivate handlers. See
|
||||
* {@link CanDeactivate} for more info.
|
||||
* - `canLoad` is an array of DI tokens used to look up CanDeactivate handlers. See
|
||||
* {@link CanLoad} for more info.
|
||||
* - `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.
|
||||
* - `runGuardsAndResolvers` defines when guards and resovlers will be run. By default they run only
|
||||
* when the matrix parameters of the route change. When set to `paramsOrQueryParamsChange` they
|
||||
* will also run when query params change. And when set to `always`, they will run every time.
|
||||
* - `children` is an array of child route definitions.
|
||||
* - `loadChildren` is a reference to lazy loaded child routes. See {@link LoadChildren} for more
|
||||
* info.
|
||||
*
|
||||
* ### 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
|
||||
* outlet: 'aux'
|
||||
* }]
|
||||
* ```
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* Empty-path route configurations can be used to instantiate components that do not 'consume'
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* ### 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
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* @stable use Routes
|
||||
*/
|
||||
export type Routes = Route[];
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the static data associated with a particular route.
|
||||
* See {@link Routes} for more details.
|
||||
* @stable
|
||||
*/
|
||||
export type Data = {
|
||||
[name: string]: any
|
||||
};
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the resolved data associated with a particular route.
|
||||
* See {@link Routes} for more details.
|
||||
* @stable
|
||||
*/
|
||||
export type ResolveData = {
|
||||
[name: string]: any
|
||||
};
|
||||
|
||||
/**
|
||||
* @whatItDoes The type of `loadChildren`.
|
||||
* See {@link Routes} for more details.
|
||||
* @stable
|
||||
*/
|
||||
export type LoadChildrenCallback = () =>
|
||||
Type<any>| NgModuleFactory<any>| Promise<Type<any>>| Observable<Type<any>>;
|
||||
|
||||
/**
|
||||
* @whatItDoes The type of `loadChildren`.
|
||||
* See {@link Routes} for more details.
|
||||
* @stable
|
||||
*/
|
||||
export type LoadChildren = string | LoadChildrenCallback;
|
||||
|
||||
/**
|
||||
* @whatItDoes The type of `queryParamsHandling`.
|
||||
* See {@link RouterLink} for more details.
|
||||
* @stable
|
||||
*/
|
||||
export type QueryParamsHandling = 'merge' | 'preserve' | '';
|
||||
|
||||
/**
|
||||
* @whatItDoes The type of `runGuardsAndResolvers`.
|
||||
* See {@link Routes} for more details.
|
||||
* @experimental
|
||||
*/
|
||||
export type RunGuardsAndResolvers = 'paramsChange' | 'paramsOrQueryParamsChange' | 'always';
|
||||
|
||||
/**
|
||||
* See {@link Routes} for more details.
|
||||
* @stable
|
||||
*/
|
||||
export interface Route {
|
||||
path?: string;
|
||||
pathMatch?: string;
|
||||
matcher?: UrlMatcher;
|
||||
component?: Type<any>;
|
||||
redirectTo?: string;
|
||||
outlet?: string;
|
||||
canActivate?: any[];
|
||||
canActivateChild?: any[];
|
||||
canDeactivate?: any[];
|
||||
canLoad?: any[];
|
||||
data?: Data;
|
||||
resolve?: ResolveData;
|
||||
children?: Routes;
|
||||
loadChildren?: LoadChildren;
|
||||
runGuardsAndResolvers?: RunGuardsAndResolvers;
|
||||
}
|
||||
|
||||
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.outlet && route.outlet !== PRIMARY_OUTLET)) {
|
||||
throw new Error(
|
||||
`Invalid configuration of route '${fullPath}': a componentless route cannot have a named outlet set`);
|
||||
}
|
||||
if (route.redirectTo && route.children) {
|
||||
throw new Error(
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and children cannot be used together`);
|
||||
}
|
||||
if (route.redirectTo && route.loadChildren) {
|
||||
throw new Error(
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and loadChildren cannot be used together`);
|
||||
}
|
||||
if (route.children && route.loadChildren) {
|
||||
throw new Error(
|
||||
`Invalid configuration of route '${fullPath}': children and loadChildren cannot be used together`);
|
||||
}
|
||||
if (route.redirectTo && route.component) {
|
||||
throw new Error(
|
||||
`Invalid configuration of route '${fullPath}': redirectTo and component cannot be used together`);
|
||||
}
|
||||
if (route.path && route.matcher) {
|
||||
throw new Error(
|
||||
`Invalid configuration of route '${fullPath}': path and matcher cannot be used together`);
|
||||
}
|
||||
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`);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
}
|
77
packages/router/src/create_router_state.ts
Normal file
77
packages/router/src/create_router_state.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||
|
||||
import {DetachedRouteHandleInternal, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||
import {TreeNode} from './utils/tree';
|
||||
|
||||
export function createRouterState(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: RouterStateSnapshot,
|
||||
prevState: RouterState): RouterState {
|
||||
const root = createNode(routeReuseStrategy, curr._root, prevState ? prevState._root : undefined);
|
||||
return new RouterState(root, curr);
|
||||
}
|
||||
|
||||
function createNode(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState?: TreeNode<ActivatedRoute>): TreeNode<ActivatedRoute> {
|
||||
// reuse an activated route that is currently displayed on the screen
|
||||
if (prevState && routeReuseStrategy.shouldReuseRoute(curr.value, prevState.value.snapshot)) {
|
||||
const value = prevState.value;
|
||||
value._futureSnapshot = curr.value;
|
||||
const children = createOrReuseChildren(routeReuseStrategy, curr, prevState);
|
||||
return new TreeNode<ActivatedRoute>(value, children);
|
||||
|
||||
// retrieve an activated route that is used to be displayed, but is not currently displayed
|
||||
} else if (routeReuseStrategy.retrieve(curr.value)) {
|
||||
const tree: TreeNode<ActivatedRoute> =
|
||||
(<DetachedRouteHandleInternal>routeReuseStrategy.retrieve(curr.value)).route;
|
||||
setFutureSnapshotsOfActivatedRoutes(curr, tree);
|
||||
return tree;
|
||||
|
||||
} else {
|
||||
const value = createActivatedRoute(curr.value);
|
||||
const children = curr.children.map(c => createNode(routeReuseStrategy, c));
|
||||
return new TreeNode<ActivatedRoute>(value, children);
|
||||
}
|
||||
}
|
||||
|
||||
function setFutureSnapshotsOfActivatedRoutes(
|
||||
curr: TreeNode<ActivatedRouteSnapshot>, result: TreeNode<ActivatedRoute>): void {
|
||||
if (curr.value.routeConfig !== result.value.routeConfig) {
|
||||
throw new Error('Cannot reattach ActivatedRouteSnapshot created from a different route');
|
||||
}
|
||||
if (curr.children.length !== result.children.length) {
|
||||
throw new Error('Cannot reattach ActivatedRouteSnapshot with a different number of children');
|
||||
}
|
||||
result.value._futureSnapshot = curr.value;
|
||||
for (let i = 0; i < curr.children.length; ++i) {
|
||||
setFutureSnapshotsOfActivatedRoutes(curr.children[i], result.children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function createOrReuseChildren(
|
||||
routeReuseStrategy: RouteReuseStrategy, curr: TreeNode<ActivatedRouteSnapshot>,
|
||||
prevState: TreeNode<ActivatedRoute>) {
|
||||
return curr.children.map(child => {
|
||||
for (const p of prevState.children) {
|
||||
if (routeReuseStrategy.shouldReuseRoute(p.value.snapshot, child.value)) {
|
||||
return createNode(routeReuseStrategy, child, p);
|
||||
}
|
||||
}
|
||||
return createNode(routeReuseStrategy, child);
|
||||
});
|
||||
}
|
||||
|
||||
function createActivatedRoute(c: ActivatedRouteSnapshot) {
|
||||
return new ActivatedRoute(
|
||||
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
|
||||
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
|
||||
}
|
311
packages/router/src/create_url_tree.ts
Normal file
311
packages/router/src/create_url_tree.ts
Normal file
@ -0,0 +1,311 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {ActivatedRoute} from './router_state';
|
||||
import {PRIMARY_OUTLET, Params} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
|
||||
import {forEach, last, shallowEqual} from './utils/collection';
|
||||
|
||||
export function createUrlTree(
|
||||
route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params,
|
||||
fragment: string): UrlTree {
|
||||
if (commands.length === 0) {
|
||||
return tree(urlTree.root, urlTree.root, urlTree, queryParams, fragment);
|
||||
}
|
||||
|
||||
const nav = computeNavigation(commands);
|
||||
|
||||
if (nav.toRoot()) {
|
||||
return tree(urlTree.root, new UrlSegmentGroup([], {}), urlTree, queryParams, fragment);
|
||||
}
|
||||
|
||||
const startingPosition = findStartingPosition(nav, urlTree, route);
|
||||
|
||||
const segmentGroup = startingPosition.processChildren ?
|
||||
updateSegmentGroupChildren(
|
||||
startingPosition.segmentGroup, startingPosition.index, nav.commands) :
|
||||
updateSegmentGroup(startingPosition.segmentGroup, startingPosition.index, nav.commands);
|
||||
return tree(startingPosition.segmentGroup, segmentGroup, urlTree, queryParams, fragment);
|
||||
}
|
||||
|
||||
function isMatrixParams(command: any): boolean {
|
||||
return typeof command === 'object' && command != null && !command.outlets && !command.segmentPath;
|
||||
}
|
||||
|
||||
function tree(
|
||||
oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup, urlTree: UrlTree,
|
||||
queryParams: Params, fragment: string): UrlTree {
|
||||
if (urlTree.root === oldSegmentGroup) {
|
||||
return new UrlTree(newSegmentGroup, stringify(queryParams), fragment);
|
||||
}
|
||||
|
||||
return new UrlTree(
|
||||
replaceSegment(urlTree.root, oldSegmentGroup, newSegmentGroup), stringify(queryParams),
|
||||
fragment);
|
||||
}
|
||||
|
||||
function replaceSegment(
|
||||
current: UrlSegmentGroup, oldSegment: UrlSegmentGroup,
|
||||
newSegment: UrlSegmentGroup): UrlSegmentGroup {
|
||||
const children: {[key: string]: UrlSegmentGroup} = {};
|
||||
forEach(current.children, (c: UrlSegmentGroup, outletName: string) => {
|
||||
if (c === oldSegment) {
|
||||
children[outletName] = newSegment;
|
||||
} else {
|
||||
children[outletName] = replaceSegment(c, oldSegment, newSegment);
|
||||
}
|
||||
});
|
||||
return new UrlSegmentGroup(current.segments, children);
|
||||
}
|
||||
|
||||
class Navigation {
|
||||
constructor(
|
||||
public isAbsolute: boolean, public numberOfDoubleDots: number, public commands: any[]) {
|
||||
if (isAbsolute && commands.length > 0 && isMatrixParams(commands[0])) {
|
||||
throw new Error('Root segment cannot have matrix parameters');
|
||||
}
|
||||
|
||||
const cmdWithOutlet = commands.find(c => typeof c === 'object' && c != null && c.outlets);
|
||||
if (cmdWithOutlet && cmdWithOutlet !== last(commands)) {
|
||||
throw new Error('{outlets:{}} has to be the last command');
|
||||
}
|
||||
}
|
||||
|
||||
public toRoot(): boolean {
|
||||
return this.isAbsolute && this.commands.length === 1 && this.commands[0] == '/';
|
||||
}
|
||||
}
|
||||
|
||||
/** Transforms commands to a normalized `Navigation` */
|
||||
function computeNavigation(commands: any[]): Navigation {
|
||||
if ((typeof commands[0] === 'string') && commands.length === 1 && commands[0] === '/') {
|
||||
return new Navigation(true, 0, commands);
|
||||
}
|
||||
|
||||
let numberOfDoubleDots = 0;
|
||||
let isAbsolute = false;
|
||||
|
||||
const res: any[] = commands.reduce((res, cmd, cmdIdx) => {
|
||||
if (typeof cmd === 'object' && cmd != null) {
|
||||
if (cmd.outlets) {
|
||||
const outlets: {[k: string]: any} = {};
|
||||
forEach(cmd.outlets, (commands: any, name: string) => {
|
||||
outlets[name] = typeof commands === 'string' ? commands.split('/') : commands;
|
||||
});
|
||||
return [...res, {outlets}];
|
||||
}
|
||||
|
||||
if (cmd.segmentPath) {
|
||||
return [...res, cmd.segmentPath];
|
||||
}
|
||||
}
|
||||
|
||||
if (!(typeof cmd === 'string')) {
|
||||
return [...res, cmd];
|
||||
}
|
||||
|
||||
if (cmdIdx === 0) {
|
||||
cmd.split('/').forEach((urlPart, partIndex) => {
|
||||
if (partIndex == 0 && urlPart === '.') {
|
||||
// skip './a'
|
||||
} else if (partIndex == 0 && urlPart === '') { // '/a'
|
||||
isAbsolute = true;
|
||||
} else if (urlPart === '..') { // '../a'
|
||||
numberOfDoubleDots++;
|
||||
} else if (urlPart != '') {
|
||||
res.push(urlPart);
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return [...res, cmd];
|
||||
}, []);
|
||||
|
||||
return new Navigation(isAbsolute, numberOfDoubleDots, res);
|
||||
}
|
||||
|
||||
class Position {
|
||||
constructor(
|
||||
public segmentGroup: UrlSegmentGroup, public processChildren: boolean, public index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function findStartingPosition(nav: Navigation, tree: UrlTree, route: ActivatedRoute): Position {
|
||||
if (nav.isAbsolute) {
|
||||
return new Position(tree.root, true, 0);
|
||||
}
|
||||
|
||||
if (route.snapshot._lastPathIndex === -1) {
|
||||
return new Position(route.snapshot._urlSegment, true, 0);
|
||||
}
|
||||
|
||||
const modifier = isMatrixParams(nav.commands[0]) ? 0 : 1;
|
||||
const index = route.snapshot._lastPathIndex + modifier;
|
||||
return createPositionApplyingDoubleDots(
|
||||
route.snapshot._urlSegment, index, nav.numberOfDoubleDots);
|
||||
}
|
||||
|
||||
function createPositionApplyingDoubleDots(
|
||||
group: UrlSegmentGroup, index: number, numberOfDoubleDots: number): Position {
|
||||
let g = group;
|
||||
let ci = index;
|
||||
let dd = numberOfDoubleDots;
|
||||
while (dd > ci) {
|
||||
dd -= ci;
|
||||
g = g.parent;
|
||||
if (!g) {
|
||||
throw new Error('Invalid number of \'../\'');
|
||||
}
|
||||
ci = g.segments.length;
|
||||
}
|
||||
return new Position(g, false, ci - dd);
|
||||
}
|
||||
|
||||
function getPath(command: any): any {
|
||||
if (typeof command === 'object' && command != null && command.outlets) {
|
||||
return command.outlets[PRIMARY_OUTLET];
|
||||
}
|
||||
return `${command}`;
|
||||
}
|
||||
|
||||
function getOutlets(commands: any[]): {[k: string]: any[]} {
|
||||
if (!(typeof commands[0] === 'object')) return {[PRIMARY_OUTLET]: commands};
|
||||
if (commands[0].outlets === undefined) return {[PRIMARY_OUTLET]: commands};
|
||||
return commands[0].outlets;
|
||||
}
|
||||
|
||||
function updateSegmentGroup(
|
||||
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
|
||||
if (!segmentGroup) {
|
||||
segmentGroup = new UrlSegmentGroup([], {});
|
||||
}
|
||||
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
|
||||
return updateSegmentGroupChildren(segmentGroup, startIndex, commands);
|
||||
}
|
||||
|
||||
const m = prefixedWith(segmentGroup, startIndex, commands);
|
||||
const slicedCommands = commands.slice(m.commandIndex);
|
||||
if (m.match && m.pathIndex < segmentGroup.segments.length) {
|
||||
const g = new UrlSegmentGroup(segmentGroup.segments.slice(0, m.pathIndex), {});
|
||||
g.children[PRIMARY_OUTLET] =
|
||||
new UrlSegmentGroup(segmentGroup.segments.slice(m.pathIndex), segmentGroup.children);
|
||||
return updateSegmentGroupChildren(g, 0, slicedCommands);
|
||||
} else if (m.match && slicedCommands.length === 0) {
|
||||
return new UrlSegmentGroup(segmentGroup.segments, {});
|
||||
} else if (m.match && !segmentGroup.hasChildren()) {
|
||||
return createNewSegmentGroup(segmentGroup, startIndex, commands);
|
||||
} else if (m.match) {
|
||||
return updateSegmentGroupChildren(segmentGroup, 0, slicedCommands);
|
||||
} else {
|
||||
return createNewSegmentGroup(segmentGroup, startIndex, commands);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSegmentGroupChildren(
|
||||
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
|
||||
if (commands.length === 0) {
|
||||
return new UrlSegmentGroup(segmentGroup.segments, {});
|
||||
} else {
|
||||
const outlets = getOutlets(commands);
|
||||
const children: {[key: string]: UrlSegmentGroup} = {};
|
||||
|
||||
forEach(outlets, (commands: any, outlet: string) => {
|
||||
if (commands !== null) {
|
||||
children[outlet] = updateSegmentGroup(segmentGroup.children[outlet], startIndex, commands);
|
||||
}
|
||||
});
|
||||
|
||||
forEach(segmentGroup.children, (child: UrlSegmentGroup, childOutlet: string) => {
|
||||
if (outlets[childOutlet] === undefined) {
|
||||
children[childOutlet] = child;
|
||||
}
|
||||
});
|
||||
return new UrlSegmentGroup(segmentGroup.segments, children);
|
||||
}
|
||||
}
|
||||
|
||||
function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]) {
|
||||
let currentCommandIndex = 0;
|
||||
let currentPathIndex = startIndex;
|
||||
|
||||
const noMatch = {match: false, pathIndex: 0, commandIndex: 0};
|
||||
while (currentPathIndex < segmentGroup.segments.length) {
|
||||
if (currentCommandIndex >= commands.length) return noMatch;
|
||||
const path = segmentGroup.segments[currentPathIndex];
|
||||
const curr = getPath(commands[currentCommandIndex]);
|
||||
const next =
|
||||
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
|
||||
|
||||
if (currentPathIndex > 0 && curr === undefined) break;
|
||||
|
||||
if (curr && next && (typeof next === 'object') && next.outlets === undefined) {
|
||||
if (!compare(curr, next, path)) return noMatch;
|
||||
currentCommandIndex += 2;
|
||||
} else {
|
||||
if (!compare(curr, {}, path)) return noMatch;
|
||||
currentCommandIndex++;
|
||||
}
|
||||
currentPathIndex++;
|
||||
}
|
||||
|
||||
return {match: true, pathIndex: currentPathIndex, commandIndex: currentCommandIndex};
|
||||
}
|
||||
|
||||
function createNewSegmentGroup(
|
||||
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
|
||||
const paths = segmentGroup.segments.slice(0, startIndex);
|
||||
|
||||
let i = 0;
|
||||
while (i < commands.length) {
|
||||
if (typeof commands[i] === 'object' && commands[i].outlets !== undefined) {
|
||||
const children = createNewSegmentChildren(commands[i].outlets);
|
||||
return new UrlSegmentGroup(paths, children);
|
||||
}
|
||||
|
||||
// if we start with an object literal, we need to reuse the path part from the segment
|
||||
if (i === 0 && isMatrixParams(commands[0])) {
|
||||
const p = segmentGroup.segments[startIndex];
|
||||
paths.push(new UrlSegment(p.path, commands[0]));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const curr = getPath(commands[i]);
|
||||
const next = (i < commands.length - 1) ? commands[i + 1] : null;
|
||||
if (curr && next && isMatrixParams(next)) {
|
||||
paths.push(new UrlSegment(curr, stringify(next)));
|
||||
i += 2;
|
||||
} else {
|
||||
paths.push(new UrlSegment(curr, {}));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return new UrlSegmentGroup(paths, {});
|
||||
}
|
||||
|
||||
function createNewSegmentChildren(outlets: {[name: string]: any}): any {
|
||||
const children: {[key: string]: UrlSegmentGroup} = {};
|
||||
forEach(outlets, (commands: any, outlet: string) => {
|
||||
if (commands !== null) {
|
||||
children[outlet] = createNewSegmentGroup(new UrlSegmentGroup([], {}), 0, commands);
|
||||
}
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
||||
const res: {[key: string]: string} = {};
|
||||
forEach(params, (v: any, k: string) => res[k] = `${v}`);
|
||||
return res;
|
||||
}
|
||||
|
||||
function compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean {
|
||||
return path == segment.path && shallowEqual(params, segment.parameters);
|
||||
}
|
248
packages/router/src/directives/router_link.ts
Normal file
248
packages/router/src/directives/router_link.ts
Normal file
@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {LocationStrategy} from '@angular/common';
|
||||
import {Attribute, Directive, ElementRef, HostBinding, HostListener, Input, OnChanges, OnDestroy, Renderer, isDevMode} from '@angular/core';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
import {QueryParamsHandling} from '../config';
|
||||
import {NavigationEnd} from '../events';
|
||||
import {Router} from '../router';
|
||||
import {ActivatedRoute} from '../router_state';
|
||||
import {UrlTree} from '../url_tree';
|
||||
|
||||
/**
|
||||
* @whatItDoes Lets you link to specific parts of your app.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* Consider the following route configuration:
|
||||
* `[{ path: 'user/:name', component: UserCmp }]`
|
||||
*
|
||||
* When linking to this `user/:name` route, you can write:
|
||||
* `<a routerLink='/user/bob'>link to user component</a>`
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* The RouterLink directives let you link to specific parts of your app.
|
||||
*
|
||||
* When the link is static, you can use the directive as follows:
|
||||
* `<a routerLink="/user/bob">link to user component</a>`
|
||||
*
|
||||
* If you use dynamic values to generate the link, you can pass an array of path
|
||||
* segments, followed by the params for each segment.
|
||||
*
|
||||
* For instance `['/team', teamId, 'user', userName, {details: true}]`
|
||||
* means that we want to generate a link to `/team/11/user/bob;details=true`.
|
||||
*
|
||||
* Multiple static segments can be merged into one
|
||||
* (e.g., `['/team/11/user', userName, {details: true}]`).
|
||||
*
|
||||
* The first segment name can be prepended with `/`, `./`, or `../`:
|
||||
* * If the first segment begins with `/`, the router will look up the route from the root of the
|
||||
* app.
|
||||
* * If the first segment begins with `./`, or doesn't begin with a slash, the router will
|
||||
* instead look in the children of the current activated route.
|
||||
* * And if the first segment begins with `../`, the router will go up one level.
|
||||
*
|
||||
* You can set query params and fragment as follows:
|
||||
*
|
||||
* ```
|
||||
* <a [routerLink]="['/user/bob']" [queryParams]="{debug: true}" fragment="education">
|
||||
* link to user component
|
||||
* </a>
|
||||
* ```
|
||||
* RouterLink will use these to generate this link: `/user/bob#education?debug=true`.
|
||||
*
|
||||
* (Deprecated in v4.0.0 use `queryParamsHandling` instead) You can also tell the
|
||||
* directive to preserve the current query params and fragment:
|
||||
*
|
||||
* ```
|
||||
* <a [routerLink]="['/user/bob']" preserveQueryParams preserveFragment>
|
||||
* link to user component
|
||||
* </a>
|
||||
* ```
|
||||
*
|
||||
* You can tell the directive to how to handle queryParams, available options are:
|
||||
* - 'merge' merge the queryParams into the current queryParams
|
||||
* - 'preserve' prserve the current queryParams
|
||||
* - default / '' use the queryParams only
|
||||
* same options for {@link NavigationExtras.queryParamsHandling}
|
||||
*
|
||||
* ```
|
||||
* <a [routerLink]="['/user/bob']" [queryParams]="{debug: true}" queryParamsHandling="merge">
|
||||
* link to user component
|
||||
* </a>
|
||||
* ```
|
||||
*
|
||||
* The router link directive always treats the provided input as a delta to the current url.
|
||||
*
|
||||
* For instance, if the current url is `/user/(box//aux:team)`.
|
||||
*
|
||||
* Then the following link `<a [routerLink]="['/user/jim']">Jim</a>` will generate the link
|
||||
* `/user/(jim//aux:team)`.
|
||||
*
|
||||
* @ngModule RouterModule
|
||||
*
|
||||
* See {@link Router.createUrlTree} for more information.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: ':not(a)[routerLink]'})
|
||||
export class RouterLink {
|
||||
@Input() queryParams: {[k: string]: any};
|
||||
@Input() fragment: string;
|
||||
@Input() queryParamsHandling: QueryParamsHandling;
|
||||
@Input() preserveFragment: boolean;
|
||||
@Input() skipLocationChange: boolean;
|
||||
@Input() replaceUrl: boolean;
|
||||
private commands: any[] = [];
|
||||
private preserve: boolean;
|
||||
|
||||
constructor(
|
||||
private router: Router, private route: ActivatedRoute,
|
||||
@Attribute('tabindex') tabIndex: string, renderer: Renderer, el: ElementRef) {
|
||||
if (tabIndex == null) {
|
||||
renderer.setElementAttribute(el.nativeElement, 'tabindex', '0');
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set routerLink(commands: any[]|string) {
|
||||
if (commands != null) {
|
||||
this.commands = Array.isArray(commands) ? commands : [commands];
|
||||
} else {
|
||||
this.commands = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 4.0.0 use `queryParamsHandling` instead.
|
||||
*/
|
||||
@Input()
|
||||
set preserveQueryParams(value: boolean) {
|
||||
if (isDevMode() && <any>console && <any>console.warn) {
|
||||
console.warn('preserveQueryParams is deprecated!, use queryParamsHandling instead.');
|
||||
}
|
||||
this.preserve = value;
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onClick(): boolean {
|
||||
const extras = {
|
||||
skipLocationChange: attrBoolValue(this.skipLocationChange),
|
||||
replaceUrl: attrBoolValue(this.replaceUrl),
|
||||
};
|
||||
this.router.navigateByUrl(this.urlTree, extras);
|
||||
return true;
|
||||
}
|
||||
|
||||
get urlTree(): UrlTree {
|
||||
return this.router.createUrlTree(this.commands, {
|
||||
relativeTo: this.route,
|
||||
queryParams: this.queryParams,
|
||||
fragment: this.fragment,
|
||||
preserveQueryParams: attrBoolValue(this.preserve),
|
||||
queryParamsHandling: this.queryParamsHandling,
|
||||
preserveFragment: attrBoolValue(this.preserveFragment),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Lets you link to specific parts of your app.
|
||||
*
|
||||
* See {@link RouterLink} for more information.
|
||||
*
|
||||
* @ngModule RouterModule
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: 'a[routerLink]'})
|
||||
export class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||
@HostBinding('attr.target') @Input() target: string;
|
||||
@Input() queryParams: {[k: string]: any};
|
||||
@Input() fragment: string;
|
||||
@Input() queryParamsHandling: QueryParamsHandling;
|
||||
@Input() preserveFragment: boolean;
|
||||
@Input() skipLocationChange: boolean;
|
||||
@Input() replaceUrl: boolean;
|
||||
private commands: any[] = [];
|
||||
private subscription: Subscription;
|
||||
private preserve: boolean;
|
||||
|
||||
// the url displayed on the anchor element.
|
||||
@HostBinding() href: string;
|
||||
|
||||
constructor(
|
||||
private router: Router, private route: ActivatedRoute,
|
||||
private locationStrategy: LocationStrategy) {
|
||||
this.subscription = router.events.subscribe(s => {
|
||||
if (s instanceof NavigationEnd) {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Input()
|
||||
set routerLink(commands: any[]|string) {
|
||||
if (commands != null) {
|
||||
this.commands = Array.isArray(commands) ? commands : [commands];
|
||||
} else {
|
||||
this.commands = [];
|
||||
}
|
||||
}
|
||||
|
||||
@Input()
|
||||
set preserveQueryParams(value: boolean) {
|
||||
if (isDevMode() && <any>console && <any>console.warn) {
|
||||
console.warn('preserveQueryParams is deprecated, use queryParamsHandling instead.');
|
||||
}
|
||||
this.preserve = value;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: {}): any { this.updateTargetUrlAndHref(); }
|
||||
ngOnDestroy(): any { this.subscription.unsubscribe(); }
|
||||
|
||||
@HostListener('click', ['$event.button', '$event.ctrlKey', '$event.metaKey'])
|
||||
onClick(button: number, ctrlKey: boolean, metaKey: boolean): boolean {
|
||||
if (button !== 0 || ctrlKey || metaKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof this.target === 'string' && this.target != '_self') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const extras = {
|
||||
skipLocationChange: attrBoolValue(this.skipLocationChange),
|
||||
replaceUrl: attrBoolValue(this.replaceUrl),
|
||||
};
|
||||
this.router.navigateByUrl(this.urlTree, extras);
|
||||
return false;
|
||||
}
|
||||
|
||||
private updateTargetUrlAndHref(): void {
|
||||
this.href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.urlTree));
|
||||
}
|
||||
|
||||
get urlTree(): UrlTree {
|
||||
return this.router.createUrlTree(this.commands, {
|
||||
relativeTo: this.route,
|
||||
queryParams: this.queryParams,
|
||||
fragment: this.fragment,
|
||||
preserveQueryParams: attrBoolValue(this.preserve),
|
||||
queryParamsHandling: this.queryParamsHandling,
|
||||
preserveFragment: attrBoolValue(this.preserveFragment),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function attrBoolValue(s: any): boolean {
|
||||
return s === '' || !!s;
|
||||
}
|
142
packages/router/src/directives/router_link_active.ts
Normal file
142
packages/router/src/directives/router_link_active.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {AfterContentInit, ChangeDetectorRef, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer, SimpleChanges} from '@angular/core';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {NavigationEnd} from '../events';
|
||||
import {Router} from '../router';
|
||||
import {RouterLink, RouterLinkWithHref} from './router_link';
|
||||
|
||||
/**
|
||||
* @whatItDoes Lets you add a CSS class to an element when the link's route becomes active.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* <a routerLink="/user/bob" routerLinkActive="active-link">Bob</a>
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* The RouterLinkActive directive lets you add a CSS class to an element when the link's route
|
||||
* becomes active.
|
||||
*
|
||||
* Consider the following example:
|
||||
*
|
||||
* ```
|
||||
* <a routerLink="/user/bob" routerLinkActive="active-link">Bob</a>
|
||||
* ```
|
||||
*
|
||||
* When the url is either '/user' or '/user/bob', the active-link class will
|
||||
* be added to the `a` tag. If the url changes, the class will be removed.
|
||||
*
|
||||
* You can set more than one class, as follows:
|
||||
*
|
||||
* ```
|
||||
* <a routerLink="/user/bob" routerLinkActive="class1 class2">Bob</a>
|
||||
* <a routerLink="/user/bob" [routerLinkActive]="['class1', 'class2']">Bob</a>
|
||||
* ```
|
||||
*
|
||||
* You can configure RouterLinkActive by passing `exact: true`. This will add the classes
|
||||
* only when the url matches the link exactly.
|
||||
*
|
||||
* ```
|
||||
* <a routerLink="/user/bob" routerLinkActive="active-link" [routerLinkActiveOptions]="{exact:
|
||||
* true}">Bob</a>
|
||||
* ```
|
||||
*
|
||||
* You can assign the RouterLinkActive instance to a template variable and directly check
|
||||
* the `isActive` status.
|
||||
* ```
|
||||
* <a routerLink="/user/bob" routerLinkActive #rla="routerLinkActive">
|
||||
* Bob {{ rla.isActive ? '(already open)' : ''}}
|
||||
* </a>
|
||||
* ```
|
||||
*
|
||||
* Finally, you can apply the RouterLinkActive directive to an ancestor of a RouterLink.
|
||||
*
|
||||
* ```
|
||||
* <div routerLinkActive="active-link" [routerLinkActiveOptions]="{exact: true}">
|
||||
* <a routerLink="/user/jim">Jim</a>
|
||||
* <a routerLink="/user/bob">Bob</a>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* This will set the active-link class on the div tag if the url is either '/user/jim' or
|
||||
* '/user/bob'.
|
||||
*
|
||||
* @ngModule RouterModule
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[routerLinkActive]',
|
||||
exportAs: 'routerLinkActive',
|
||||
})
|
||||
export class RouterLinkActive implements OnChanges,
|
||||
OnDestroy, AfterContentInit {
|
||||
@ContentChildren(RouterLink, {descendants: true}) links: QueryList<RouterLink>;
|
||||
@ContentChildren(RouterLinkWithHref, {descendants: true})
|
||||
linksWithHrefs: QueryList<RouterLinkWithHref>;
|
||||
|
||||
private classes: string[] = [];
|
||||
private subscription: Subscription;
|
||||
private active: boolean = false;
|
||||
|
||||
@Input() routerLinkActiveOptions: {exact: boolean} = {exact: false};
|
||||
|
||||
constructor(
|
||||
private router: Router, private element: ElementRef, private renderer: Renderer,
|
||||
private cdr: ChangeDetectorRef) {
|
||||
this.subscription = router.events.subscribe(s => {
|
||||
if (s instanceof NavigationEnd) {
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isActive(): boolean { return this.active; }
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.links.changes.subscribe(_ => this.update());
|
||||
this.linksWithHrefs.changes.subscribe(_ => this.update());
|
||||
this.update();
|
||||
}
|
||||
|
||||
@Input()
|
||||
set routerLinkActive(data: string[]|string) {
|
||||
const classes = Array.isArray(data) ? data : data.split(' ');
|
||||
this.classes = classes.filter(c => !!c);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void { this.update(); }
|
||||
ngOnDestroy(): void { this.subscription.unsubscribe(); }
|
||||
|
||||
private update(): void {
|
||||
if (!this.links || !this.linksWithHrefs || !this.router.navigated) return;
|
||||
const hasActiveLinks = this.hasActiveLinks();
|
||||
|
||||
// react only when status has changed to prevent unnecessary dom updates
|
||||
if (this.active !== hasActiveLinks) {
|
||||
this.active = hasActiveLinks;
|
||||
this.classes.forEach(
|
||||
c => this.renderer.setElementClass(this.element.nativeElement, c, hasActiveLinks));
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private isLinkActive(router: Router): (link: (RouterLink|RouterLinkWithHref)) => boolean {
|
||||
return (link: RouterLink | RouterLinkWithHref) =>
|
||||
router.isActive(link.urlTree, this.routerLinkActiveOptions.exact);
|
||||
}
|
||||
|
||||
private hasActiveLinks(): boolean {
|
||||
return this.links.some(this.isLinkActive(this.router)) ||
|
||||
this.linksWithHrefs.some(this.isLinkActive(this.router));
|
||||
}
|
||||
}
|
113
packages/router/src/directives/router_outlet.ts
Normal file
113
packages/router/src/directives/router_outlet.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Attribute, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, Output, ReflectiveInjector, ResolvedReflectiveProvider, ViewContainerRef} from '@angular/core';
|
||||
import {RouterOutletMap} from '../router_outlet_map';
|
||||
import {ActivatedRoute} from '../router_state';
|
||||
import {PRIMARY_OUTLET} from '../shared';
|
||||
|
||||
/**
|
||||
* @whatItDoes Acts as a placeholder that Angular dynamically fills based on the current router
|
||||
* state.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* <router-outlet></router-outlet>
|
||||
* <router-outlet name='left'></router-outlet>
|
||||
* <router-outlet name='right'></router-outlet>
|
||||
* ```
|
||||
*
|
||||
* A router outlet will emit an activate event any time a new component is being instantiated,
|
||||
* and a deactivate event when it is being destroyed.
|
||||
*
|
||||
* ```
|
||||
* <router-outlet
|
||||
* (activate)='onActivate($event)'
|
||||
* (deactivate)='onDeactivate($event)'></router-outlet>
|
||||
* ```
|
||||
* @ngModule RouterModule
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Directive({selector: 'router-outlet'})
|
||||
export class RouterOutlet implements OnDestroy {
|
||||
private activated: ComponentRef<any>;
|
||||
private _activatedRoute: ActivatedRoute;
|
||||
public outletMap: RouterOutletMap;
|
||||
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
constructor(
|
||||
private parentOutletMap: RouterOutletMap, private location: ViewContainerRef,
|
||||
private resolver: ComponentFactoryResolver, @Attribute('name') private name: string) {
|
||||
parentOutletMap.registerOutlet(name ? name : PRIMARY_OUTLET, this);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void { this.parentOutletMap.removeOutlet(this.name ? this.name : PRIMARY_OUTLET); }
|
||||
|
||||
get locationInjector(): Injector { return this.location.injector; }
|
||||
get locationFactoryResolver(): ComponentFactoryResolver { return this.resolver; }
|
||||
|
||||
get isActivated(): boolean { return !!this.activated; }
|
||||
get component(): Object {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
return this.activated.instance;
|
||||
}
|
||||
get activatedRoute(): ActivatedRoute {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
return this._activatedRoute;
|
||||
}
|
||||
|
||||
detach(): ComponentRef<any> {
|
||||
if (!this.activated) throw new Error('Outlet is not activated');
|
||||
this.location.detach();
|
||||
const r = this.activated;
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
return r;
|
||||
}
|
||||
|
||||
attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute) {
|
||||
this.activated = ref;
|
||||
this._activatedRoute = activatedRoute;
|
||||
this.location.insert(ref.hostView);
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
if (this.activated) {
|
||||
const c = this.component;
|
||||
this.activated.destroy();
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
this.deactivateEvents.emit(c);
|
||||
}
|
||||
}
|
||||
|
||||
activate(
|
||||
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector,
|
||||
providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void {
|
||||
if (this.isActivated) {
|
||||
throw new Error('Cannot activate an already activated outlet');
|
||||
}
|
||||
|
||||
this.outletMap = outletMap;
|
||||
this._activatedRoute = activatedRoute;
|
||||
|
||||
const snapshot = activatedRoute._futureSnapshot;
|
||||
const component: any = <any>snapshot._routeConfig.component;
|
||||
const factory = resolver.resolveComponentFactory(component);
|
||||
|
||||
const inj = ReflectiveInjector.fromResolvedProviders(providers, injector);
|
||||
this.activated = this.location.createComponent(factory, this.location.length, inj, []);
|
||||
this.activated.changeDetectorRef.detectChanges();
|
||||
|
||||
this.activateEvents.emit(this.activated.instance);
|
||||
}
|
||||
}
|
145
packages/router/src/events.ts
Normal file
145
packages/router/src/events.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Route} from './config';
|
||||
import {RouterStateSnapshot} from './router_state';
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered when a navigation starts.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class NavigationStart {
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public id: number,
|
||||
/** @docsNotRequired */
|
||||
public url: string) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered when a navigation ends successfully.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class NavigationEnd {
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public id: number,
|
||||
/** @docsNotRequired */
|
||||
public url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string {
|
||||
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered when a navigation is canceled.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class NavigationCancel {
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public id: number,
|
||||
/** @docsNotRequired */
|
||||
public url: string,
|
||||
/** @docsNotRequired */
|
||||
public reason: string) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered when a navigation fails due to an unexpected error.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class NavigationError {
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public id: number,
|
||||
/** @docsNotRequired */
|
||||
public url: string,
|
||||
/** @docsNotRequired */
|
||||
public error: any) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string {
|
||||
return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered when routes are recognized.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class RoutesRecognized {
|
||||
constructor(
|
||||
/** @docsNotRequired */
|
||||
public id: number,
|
||||
/** @docsNotRequired */
|
||||
public url: string,
|
||||
/** @docsNotRequired */
|
||||
public urlAfterRedirects: string,
|
||||
/** @docsNotRequired */
|
||||
public state: RouterStateSnapshot) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string {
|
||||
return `RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered before lazy loading a route config.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class RouteConfigLoadStart {
|
||||
constructor(public route: Route) {}
|
||||
|
||||
toString(): string { return `RouteConfigLoadStart(path: ${this.route.path})`; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an event triggered when a route has been lazy loaded.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class RouteConfigLoadEnd {
|
||||
constructor(public route: Route) {}
|
||||
|
||||
toString(): string { return `RouteConfigLoadEnd(path: ${this.route.path})`; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents a router event.
|
||||
*
|
||||
* One of:
|
||||
* - {@link NavigationStart},
|
||||
* - {@link NavigationEnd},
|
||||
* - {@link NavigationCancel},
|
||||
* - {@link NavigationError},
|
||||
* - {@link RoutesRecognized},
|
||||
* - {@link RouteConfigLoadStart},
|
||||
* - {@link RouteConfigLoadEnd}
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export type Event = NavigationStart | NavigationEnd | NavigationCancel | NavigationError |
|
||||
RoutesRecognized | RouteConfigLoadStart | RouteConfigLoadEnd;
|
28
packages/router/src/index.ts
Normal file
28
packages/router/src/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
export {Data, LoadChildren, LoadChildrenCallback, ResolveData, Route, Routes, RunGuardsAndResolvers} from './config';
|
||||
export {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||
export {RouterLinkActive} from './directives/router_link_active';
|
||||
export {RouterOutlet} from './directives/router_outlet';
|
||||
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, RouteConfigLoadEnd, RouteConfigLoadStart, RoutesRecognized} from './events';
|
||||
export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './interfaces';
|
||||
export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
|
||||
export {NavigationExtras, Router} from './router';
|
||||
export {ROUTES} from './router_config_loader';
|
||||
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
|
||||
export {RouterOutletMap} from './router_outlet_map';
|
||||
export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||
export {PRIMARY_OUTLET, Params} from './shared';
|
||||
export {UrlHandlingStrategy} from './url_handling_strategy';
|
||||
export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
export {VERSION} from './version';
|
||||
|
||||
export * from './private_export'
|
379
packages/router/src/interfaces.ts
Normal file
379
packages/router/src/interfaces.ts
Normal file
@ -0,0 +1,379 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
import {Route} from './config';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Interface that a class can implement to be a guard deciding if a route can be
|
||||
* activated.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* class UserToken {}
|
||||
* class Permissions {
|
||||
* canActivate(user: UserToken, id: string): boolean {
|
||||
* return true;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class CanActivateTeam implements CanActivate {
|
||||
* constructor(private permissions: Permissions, private currentUser: UserToken) {}
|
||||
*
|
||||
* canActivate(
|
||||
* route: ActivatedRouteSnapshot,
|
||||
* state: RouterStateSnapshot
|
||||
* ): Observable<boolean>|Promise<boolean>|boolean {
|
||||
* return this.permissions.canActivate(this.currentUser, route.params.id);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* canActivate: [CanActivateTeam]
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [CanActivateTeam, UserToken, Permissions]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* You can alternatively provide a function with the `canActivate` signature:
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* canActivate: ['canActivateTeam']
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [
|
||||
* {
|
||||
* provide: 'canActivateTeam',
|
||||
* useValue: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => true
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
|
||||
Observable<boolean>|Promise<boolean>|boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Interface that a class can implement to be a guard deciding if a child route can be
|
||||
* activated.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* class UserToken {}
|
||||
* class Permissions {
|
||||
* canActivate(user: UserToken, id: string): boolean {
|
||||
* return true;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class CanActivateTeam implements CanActivateChild {
|
||||
* constructor(private permissions: Permissions, private currentUser: UserToken) {}
|
||||
*
|
||||
* canActivateChild(
|
||||
* route: ActivatedRouteSnapshot,
|
||||
* state: RouterStateSnapshot
|
||||
* ): Observable<boolean>|Promise<boolean>|boolean {
|
||||
* return this.permissions.canActivate(this.currentUser, route.params.id);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'root',
|
||||
* canActivateChild: [CanActivateTeam],
|
||||
* children: [
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: Team
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [CanActivateTeam, UserToken, Permissions]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* You can alternatively provide a function with the `canActivateChild` signature:
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'root',
|
||||
* canActivateChild: ['canActivateTeam'],
|
||||
* children: [
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: Team
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [
|
||||
* {
|
||||
* provide: 'canActivateTeam',
|
||||
* useValue: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => true
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface CanActivateChild {
|
||||
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot):
|
||||
Observable<boolean>|Promise<boolean>|boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Interface that a class can implement to be a guard deciding if a route can be
|
||||
* deactivated.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* class UserToken {}
|
||||
* class Permissions {
|
||||
* canDeactivate(user: UserToken, id: string): boolean {
|
||||
* return true;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class CanDeactivateTeam implements CanDeactivate<TeamComponent> {
|
||||
* constructor(private permissions: Permissions, private currentUser: UserToken) {}
|
||||
*
|
||||
* canDeactivate(
|
||||
* component: TeamComponent,
|
||||
* currentRoute: ActivatedRouteSnapshot,
|
||||
* currentState: RouterStateSnapshot,
|
||||
* nextState: RouterStateSnapshot
|
||||
* ): Observable<boolean>|Promise<boolean>|boolean {
|
||||
* return this.permissions.canDeactivate(this.currentUser, route.params.id);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* canDeactivate: [CanDeactivateTeam]
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [CanDeactivateTeam, UserToken, Permissions]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* You can alternatively provide a function with the `canDeactivate` signature:
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* canDeactivate: ['canDeactivateTeam']
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [
|
||||
* {
|
||||
* provide: 'canDeactivateTeam',
|
||||
* useValue: (component: TeamComponent, currentRoute: ActivatedRouteSnapshot, currentState:
|
||||
* RouterStateSnapshot, nextState: RouterStateSnapshot) => true
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface CanDeactivate<T> {
|
||||
canDeactivate(
|
||||
component: T, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot,
|
||||
nextState?: RouterStateSnapshot): Observable<boolean>|Promise<boolean>|boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Interface that class can implement to be a data provider.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* class Backend {
|
||||
* fetchTeam(id: string) {
|
||||
* return 'someTeam';
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class TeamResolver implements Resolve<Team> {
|
||||
* constructor(private backend: Backend) {}
|
||||
*
|
||||
* resolve(
|
||||
* route: ActivatedRouteSnapshot,
|
||||
* state: RouterStateSnapshot
|
||||
* ): Observable<any>|Promise<any>|any {
|
||||
* return this.backend.fetchTeam(route.params.id);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* resolve: {
|
||||
* team: TeamResolver
|
||||
* }
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [TeamResolver]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* You can alternatively provide a function with the `resolve` signature:
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* resolve: {
|
||||
* team: 'teamResolver'
|
||||
* }
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [
|
||||
* {
|
||||
* provide: 'teamResolver',
|
||||
* useValue: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => 'team'
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
* @stable
|
||||
*/
|
||||
export interface Resolve<T> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T>|Promise<T>|T;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Interface that a class can implement to be a guard deciding if a children can be
|
||||
* loaded.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* class UserToken {}
|
||||
* class Permissions {
|
||||
* canLoadChildren(user: UserToken, id: string): boolean {
|
||||
* return true;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Injectable()
|
||||
* class CanLoadTeamSection implements CanLoad {
|
||||
* constructor(private permissions: Permissions, private currentUser: UserToken) {}
|
||||
*
|
||||
* canLoad(route: Route): Observable<boolean>|Promise<boolean>|boolean {
|
||||
* return this.permissions.canLoadChildren(this.currentUser, route);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* loadChildren: 'team.js',
|
||||
* canLoad: [CanLoadTeamSection]
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [CanLoadTeamSection, UserToken, Permissions]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* You can alternatively provide a function with the `canLoad` signature:
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot([
|
||||
* {
|
||||
* path: 'team/:id',
|
||||
* component: TeamCmp,
|
||||
* loadChildren: 'team.js',
|
||||
* canLoad: ['canLoadTeamSection']
|
||||
* }
|
||||
* ])
|
||||
* ],
|
||||
* providers: [
|
||||
* {
|
||||
* provide: 'canLoadTeamSection',
|
||||
* useValue: (route: Route) => true
|
||||
* }
|
||||
* ]
|
||||
* })
|
||||
* class AppModule {}
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface CanLoad { canLoad(route: Route): Observable<boolean>|Promise<boolean>|boolean; }
|
12
packages/router/src/private_export.ts
Normal file
12
packages/router/src/private_export.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export {ROUTER_PROVIDERS as ɵROUTER_PROVIDERS} from './router_module';
|
||||
export {flatten as ɵflatten} from './utils/collection';
|
315
packages/router/src/recognize.ts
Normal file
315
packages/router/src/recognize.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Type} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Observer} from 'rxjs/Observer';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
|
||||
import {Data, ResolveData, Route, Routes} from './config';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot, inheritedParamsDataResolve} from './router_state';
|
||||
import {PRIMARY_OUTLET, defaultUrlMatcher} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlTree, mapChildrenIntoArray} from './url_tree';
|
||||
import {forEach, last, merge} from './utils/collection';
|
||||
import {TreeNode} from './utils/tree';
|
||||
|
||||
class NoMatch {}
|
||||
|
||||
export function recognize(
|
||||
rootComponentType: Type<any>, config: Routes, urlTree: UrlTree,
|
||||
url: string): Observable<RouterStateSnapshot> {
|
||||
return new Recognizer(rootComponentType, config, urlTree, url).recognize();
|
||||
}
|
||||
|
||||
class Recognizer {
|
||||
constructor(
|
||||
private rootComponentType: Type<any>, private config: Routes, private urlTree: UrlTree,
|
||||
private url: string) {}
|
||||
|
||||
recognize(): Observable<RouterStateSnapshot> {
|
||||
try {
|
||||
const rootSegmentGroup = split(this.urlTree.root, [], [], this.config).segmentGroup;
|
||||
|
||||
const children = this.processSegmentGroup(this.config, rootSegmentGroup, PRIMARY_OUTLET);
|
||||
|
||||
const root = new ActivatedRouteSnapshot(
|
||||
[], Object.freeze({}), Object.freeze(this.urlTree.queryParams), this.urlTree.fragment, {},
|
||||
PRIMARY_OUTLET, this.rootComponentType, null, this.urlTree.root, -1, {});
|
||||
|
||||
const rootNode = new TreeNode<ActivatedRouteSnapshot>(root, children);
|
||||
const routeState = new RouterStateSnapshot(this.url, rootNode);
|
||||
this.inheriteParamsAndData(routeState._root);
|
||||
return of (routeState);
|
||||
|
||||
} catch (e) {
|
||||
return new Observable<RouterStateSnapshot>(
|
||||
(obs: Observer<RouterStateSnapshot>) => obs.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
inheriteParamsAndData(routeNode: TreeNode<ActivatedRouteSnapshot>): void {
|
||||
const route = routeNode.value;
|
||||
|
||||
const i = inheritedParamsDataResolve(route);
|
||||
route.params = Object.freeze(i.params);
|
||||
route.data = Object.freeze(i.data);
|
||||
|
||||
routeNode.children.forEach(n => this.inheriteParamsAndData(n));
|
||||
}
|
||||
|
||||
processSegmentGroup(config: Route[], segmentGroup: UrlSegmentGroup, outlet: string):
|
||||
TreeNode<ActivatedRouteSnapshot>[] {
|
||||
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
|
||||
return this.processChildren(config, segmentGroup);
|
||||
} else {
|
||||
return this.processSegment(config, segmentGroup, segmentGroup.segments, outlet);
|
||||
}
|
||||
}
|
||||
|
||||
processChildren(config: Route[], segmentGroup: UrlSegmentGroup):
|
||||
TreeNode<ActivatedRouteSnapshot>[] {
|
||||
const children = mapChildrenIntoArray(
|
||||
segmentGroup, (child, childOutlet) => this.processSegmentGroup(config, child, childOutlet));
|
||||
checkOutletNameUniqueness(children);
|
||||
sortActivatedRouteSnapshots(children);
|
||||
return children;
|
||||
}
|
||||
|
||||
processSegment(
|
||||
config: Route[], segmentGroup: UrlSegmentGroup, segments: UrlSegment[],
|
||||
outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
|
||||
for (const r of config) {
|
||||
try {
|
||||
return this.processSegmentAgainstRoute(r, segmentGroup, segments, outlet);
|
||||
} catch (e) {
|
||||
if (!(e instanceof NoMatch)) throw e;
|
||||
}
|
||||
}
|
||||
if (this.noLeftoversInUrl(segmentGroup, segments, outlet)) {
|
||||
return [];
|
||||
} else {
|
||||
throw new NoMatch();
|
||||
}
|
||||
}
|
||||
|
||||
private noLeftoversInUrl(segmentGroup: UrlSegmentGroup, segments: UrlSegment[], outlet: string):
|
||||
boolean {
|
||||
return segments.length === 0 && !segmentGroup.children[outlet];
|
||||
}
|
||||
|
||||
processSegmentAgainstRoute(
|
||||
route: Route, rawSegment: UrlSegmentGroup, segments: UrlSegment[],
|
||||
outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
|
||||
if (route.redirectTo) throw new NoMatch();
|
||||
|
||||
if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch();
|
||||
|
||||
if (route.path === '**') {
|
||||
const params = segments.length > 0 ? last(segments).parameters : {};
|
||||
const snapshot = new ActivatedRouteSnapshot(
|
||||
segments, params, Object.freeze(this.urlTree.queryParams), this.urlTree.fragment,
|
||||
getData(route), outlet, route.component, route, getSourceSegmentGroup(rawSegment),
|
||||
getPathIndexShift(rawSegment) + segments.length, getResolve(route));
|
||||
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
|
||||
}
|
||||
|
||||
const {consumedSegments, parameters, lastChild} = match(rawSegment, route, segments);
|
||||
const rawSlicedSegments = segments.slice(lastChild);
|
||||
const childConfig = getChildConfig(route);
|
||||
|
||||
const {segmentGroup, slicedSegments} =
|
||||
split(rawSegment, consumedSegments, rawSlicedSegments, childConfig);
|
||||
|
||||
const snapshot = new ActivatedRouteSnapshot(
|
||||
consumedSegments, parameters, Object.freeze(this.urlTree.queryParams),
|
||||
this.urlTree.fragment, getData(route), outlet, route.component, route,
|
||||
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + consumedSegments.length,
|
||||
getResolve(route));
|
||||
|
||||
|
||||
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
|
||||
const children = this.processChildren(childConfig, segmentGroup);
|
||||
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
|
||||
|
||||
} else if (childConfig.length === 0 && slicedSegments.length === 0) {
|
||||
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
|
||||
|
||||
} else {
|
||||
const children =
|
||||
this.processSegment(childConfig, segmentGroup, slicedSegments, PRIMARY_OUTLET);
|
||||
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sortActivatedRouteSnapshots(nodes: TreeNode<ActivatedRouteSnapshot>[]): 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);
|
||||
});
|
||||
}
|
||||
|
||||
function getChildConfig(route: Route): Route[] {
|
||||
if (route.children) {
|
||||
return route.children;
|
||||
} else if (route.loadChildren) {
|
||||
return (<any>route)._loadedConfig.routes;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]) {
|
||||
if (route.path === '') {
|
||||
if (route.pathMatch === 'full' && (segmentGroup.hasChildren() || segments.length > 0)) {
|
||||
throw new NoMatch();
|
||||
} else {
|
||||
return {consumedSegments: [], lastChild: 0, parameters: {}};
|
||||
}
|
||||
}
|
||||
|
||||
const matcher = route.matcher || defaultUrlMatcher;
|
||||
const res = matcher(segments, segmentGroup, route);
|
||||
if (!res) throw new NoMatch();
|
||||
|
||||
const posParams: {[n: string]: string} = {};
|
||||
forEach(res.posParams, (v: UrlSegment, k: string) => { posParams[k] = v.path; });
|
||||
const parameters = merge(posParams, res.consumed[res.consumed.length - 1].parameters);
|
||||
|
||||
return {consumedSegments: res.consumed, lastChild: res.consumed.length, parameters};
|
||||
}
|
||||
|
||||
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
|
||||
const names: {[k: string]: ActivatedRouteSnapshot} = {};
|
||||
nodes.forEach(n => {
|
||||
const routeWithSameOutletName = names[n.value.outlet];
|
||||
if (routeWithSameOutletName) {
|
||||
const p = routeWithSameOutletName.url.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;
|
||||
});
|
||||
}
|
||||
|
||||
function getSourceSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup {
|
||||
let s = segmentGroup;
|
||||
while (s._sourceSegment) {
|
||||
s = s._sourceSegment;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function getPathIndexShift(segmentGroup: UrlSegmentGroup): number {
|
||||
let s = segmentGroup;
|
||||
let res = (s._segmentIndexShift ? s._segmentIndexShift : 0);
|
||||
while (s._sourceSegment) {
|
||||
s = s._sourceSegment;
|
||||
res += (s._segmentIndexShift ? s._segmentIndexShift : 0);
|
||||
}
|
||||
return res - 1;
|
||||
}
|
||||
|
||||
function split(
|
||||
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
|
||||
config: Route[]) {
|
||||
if (slicedSegments.length > 0 &&
|
||||
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) {
|
||||
const s = new UrlSegmentGroup(
|
||||
consumedSegments, createChildrenForEmptyPaths(
|
||||
segmentGroup, consumedSegments, config,
|
||||
new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
|
||||
s._sourceSegment = segmentGroup;
|
||||
s._segmentIndexShift = consumedSegments.length;
|
||||
return {segmentGroup: s, slicedSegments: []};
|
||||
|
||||
} else if (
|
||||
slicedSegments.length === 0 &&
|
||||
containsEmptyPathMatches(segmentGroup, slicedSegments, config)) {
|
||||
const s = new UrlSegmentGroup(
|
||||
segmentGroup.segments, addEmptyPathsToChildrenIfNeeded(
|
||||
segmentGroup, slicedSegments, config, segmentGroup.children));
|
||||
s._sourceSegment = segmentGroup;
|
||||
s._segmentIndexShift = consumedSegments.length;
|
||||
return {segmentGroup: s, slicedSegments};
|
||||
|
||||
} else {
|
||||
const s = new UrlSegmentGroup(segmentGroup.segments, segmentGroup.children);
|
||||
s._sourceSegment = segmentGroup;
|
||||
s._segmentIndexShift = consumedSegments.length;
|
||||
return {segmentGroup: s, slicedSegments};
|
||||
}
|
||||
}
|
||||
|
||||
function addEmptyPathsToChildrenIfNeeded(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
|
||||
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
|
||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||
for (const r of routes) {
|
||||
if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
|
||||
const s = new UrlSegmentGroup([], {});
|
||||
s._sourceSegment = segmentGroup;
|
||||
s._segmentIndexShift = segmentGroup.segments.length;
|
||||
res[getOutlet(r)] = s;
|
||||
}
|
||||
}
|
||||
return merge(children, res);
|
||||
}
|
||||
|
||||
function createChildrenForEmptyPaths(
|
||||
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], routes: Route[],
|
||||
primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
|
||||
const res: {[name: string]: UrlSegmentGroup} = {};
|
||||
res[PRIMARY_OUTLET] = primarySegment;
|
||||
primarySegment._sourceSegment = segmentGroup;
|
||||
primarySegment._segmentIndexShift = consumedSegments.length;
|
||||
|
||||
for (const r of routes) {
|
||||
if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) {
|
||||
const s = new UrlSegmentGroup([], {});
|
||||
s._sourceSegment = segmentGroup;
|
||||
s._segmentIndexShift = consumedSegments.length;
|
||||
res[getOutlet(r)] = s;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function containsEmptyPathMatchesWithNamedOutlets(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
||||
return routes
|
||||
.filter(
|
||||
r => emptyPathMatch(segmentGroup, slicedSegments, r) &&
|
||||
getOutlet(r) !== PRIMARY_OUTLET)
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
function containsEmptyPathMatches(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
|
||||
return routes.filter(r => emptyPathMatch(segmentGroup, slicedSegments, r)).length > 0;
|
||||
}
|
||||
|
||||
function emptyPathMatch(
|
||||
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
|
||||
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full')
|
||||
return false;
|
||||
return r.path === '' && r.redirectTo === undefined;
|
||||
}
|
||||
|
||||
function getOutlet(route: Route): string {
|
||||
return route.outlet ? route.outlet : PRIMARY_OUTLET;
|
||||
}
|
||||
|
||||
function getData(route: Route): Data {
|
||||
return route.data ? route.data : {};
|
||||
}
|
||||
|
||||
function getResolve(route: Route): ResolveData {
|
||||
return route.resolve ? route.resolve : {};
|
||||
}
|
50
packages/router/src/route_reuse_strategy.ts
Normal file
50
packages/router/src/route_reuse_strategy.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {ComponentRef} from '@angular/core';
|
||||
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot} from './router_state';
|
||||
import {TreeNode} from './utils/tree';
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the detached route tree.
|
||||
*
|
||||
* This is an opaque value the router will give to a custom route reuse strategy
|
||||
* to store and retrieve later on.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export type DetachedRouteHandle = {};
|
||||
|
||||
/** @internal */
|
||||
export type DetachedRouteHandleInternal = {
|
||||
componentRef: ComponentRef<any>,
|
||||
route: TreeNode<ActivatedRoute>,
|
||||
};
|
||||
|
||||
/**
|
||||
* @whatItDoes Provides a way to customize when activated routes get reused.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export abstract class RouteReuseStrategy {
|
||||
/** Determines if this route (and its subtree) should be detached to be reused later */
|
||||
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
|
||||
|
||||
/** Stores the detached route */
|
||||
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void;
|
||||
|
||||
/** Determines if this route (and its subtree) should be reattached */
|
||||
abstract shouldAttach(route: ActivatedRouteSnapshot): boolean;
|
||||
|
||||
/** Retrieves the previously stored route */
|
||||
abstract retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle;
|
||||
|
||||
/** Determines if a route should be reused */
|
||||
abstract shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
||||
}
|
1226
packages/router/src/router.ts
Normal file
1226
packages/router/src/router.ts
Normal file
File diff suppressed because it is too large
Load Diff
70
packages/router/src/router_config_loader.ts
Normal file
70
packages/router/src/router_config_loader.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Compiler, ComponentFactoryResolver, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {fromPromise} from 'rxjs/observable/fromPromise';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
import {map} from 'rxjs/operator/map';
|
||||
import {mergeMap} from 'rxjs/operator/mergeMap';
|
||||
import {LoadChildren, Route} from './config';
|
||||
import {flatten, wrapIntoObservable} from './utils/collection';
|
||||
|
||||
/**
|
||||
* @docsNotRequired
|
||||
* @experimental
|
||||
*/
|
||||
export const ROUTES = new InjectionToken<Route[][]>('ROUTES');
|
||||
|
||||
export class LoadedRouterConfig {
|
||||
constructor(
|
||||
public routes: Route[], public injector: Injector,
|
||||
public factoryResolver: ComponentFactoryResolver, public injectorFactory: Function) {}
|
||||
}
|
||||
|
||||
export class RouterConfigLoader {
|
||||
constructor(
|
||||
private loader: NgModuleFactoryLoader, private compiler: Compiler,
|
||||
private onLoadStartListener?: (r: Route) => void,
|
||||
private onLoadEndListener?: (r: Route) => void) {}
|
||||
|
||||
load(parentInjector: Injector, route: Route): Observable<LoadedRouterConfig> {
|
||||
if (this.onLoadStartListener) {
|
||||
this.onLoadStartListener(route);
|
||||
}
|
||||
|
||||
const moduleFactory$ = this.loadModuleFactory(route.loadChildren);
|
||||
|
||||
return map.call(moduleFactory$, (factory: NgModuleFactory<any>) => {
|
||||
if (this.onLoadEndListener) {
|
||||
this.onLoadEndListener(route);
|
||||
}
|
||||
|
||||
const module = factory.create(parentInjector);
|
||||
const injectorFactory = (parent: Injector) => factory.create(parent).injector;
|
||||
|
||||
return new LoadedRouterConfig(
|
||||
flatten(module.injector.get(ROUTES)), module.injector, module.componentFactoryResolver,
|
||||
injectorFactory);
|
||||
});
|
||||
}
|
||||
|
||||
private loadModuleFactory(loadChildren: LoadChildren): Observable<NgModuleFactory<any>> {
|
||||
if (typeof loadChildren === 'string') {
|
||||
return fromPromise(this.loader.load(loadChildren));
|
||||
} else {
|
||||
return mergeMap.call(wrapIntoObservable(loadChildren()), (t: any) => {
|
||||
if (t instanceof NgModuleFactory) {
|
||||
return of (t);
|
||||
} else {
|
||||
return fromPromise(this.compiler.compileModuleAsync(t));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
425
packages/router/src/router_module.ts
Normal file
425
packages/router/src/router_module.ts
Normal file
@ -0,0 +1,425 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {APP_BASE_HREF, HashLocationStrategy, LOCATION_INITIALIZED, Location, LocationStrategy, PathLocationStrategy, PlatformLocation} from '@angular/common';
|
||||
import {ANALYZE_FOR_ENTRY_COMPONENTS, APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, ApplicationRef, Compiler, ComponentRef, Inject, Injectable, InjectionToken, Injector, ModuleWithProviders, NgModule, NgModuleFactoryLoader, NgProbeToken, Optional, Provider, SkipSelf, SystemJsNgModuleLoader} from '@angular/core';
|
||||
import {ɵgetDOM as getDOM} from '@angular/platform-browser';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
|
||||
import {Route, Routes} from './config';
|
||||
import {RouterLink, RouterLinkWithHref} from './directives/router_link';
|
||||
import {RouterLinkActive} from './directives/router_link_active';
|
||||
import {RouterOutlet} from './directives/router_outlet';
|
||||
import {RouteReuseStrategy} from './route_reuse_strategy';
|
||||
import {ErrorHandler, Router} from './router';
|
||||
import {ROUTES} from './router_config_loader';
|
||||
import {RouterOutletMap} from './router_outlet_map';
|
||||
import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||
import {ActivatedRoute, RouterStateSnapshot} from './router_state';
|
||||
import {UrlHandlingStrategy} from './url_handling_strategy';
|
||||
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
|
||||
import {flatten} from './utils/collection';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Contains a list of directives
|
||||
* @stable
|
||||
*/
|
||||
const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkWithHref, RouterLinkActive];
|
||||
|
||||
/**
|
||||
* @whatItDoes Is used in DI to configure the router.
|
||||
* @stable
|
||||
*/
|
||||
export const ROUTER_CONFIGURATION = new InjectionToken<ExtraOptions>('ROUTER_CONFIGURATION');
|
||||
|
||||
/**
|
||||
* @docsNotRequired
|
||||
*/
|
||||
export const ROUTER_FORROOT_GUARD = new InjectionToken<void>('ROUTER_FORROOT_GUARD');
|
||||
|
||||
export const ROUTER_PROVIDERS: Provider[] = [
|
||||
Location,
|
||||
{provide: UrlSerializer, useClass: DefaultUrlSerializer},
|
||||
{
|
||||
provide: Router,
|
||||
useFactory: setupRouter,
|
||||
deps: [
|
||||
ApplicationRef, UrlSerializer, RouterOutletMap, Location, Injector, NgModuleFactoryLoader,
|
||||
Compiler, ROUTES, ROUTER_CONFIGURATION, [UrlHandlingStrategy, new Optional()],
|
||||
[RouteReuseStrategy, new Optional()]
|
||||
]
|
||||
},
|
||||
RouterOutletMap,
|
||||
{provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
|
||||
{provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader},
|
||||
RouterPreloader,
|
||||
NoPreloading,
|
||||
PreloadAllModules,
|
||||
{provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}},
|
||||
];
|
||||
|
||||
export function routerNgProbeToken() {
|
||||
return new NgProbeToken('Router', Router);
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Adds router directives and providers.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* RouterModule can be imported multiple times: once per lazily-loaded bundle.
|
||||
* Since the router deals with a global shared resource--location, we cannot have
|
||||
* more than one router service active.
|
||||
*
|
||||
* That is why there are two ways to create the module: `RouterModule.forRoot` and
|
||||
* `RouterModule.forChild`.
|
||||
*
|
||||
* * `forRoot` creates a module that contains all the directives, the given routes, and the router
|
||||
* service itself.
|
||||
* * `forChild` creates a module that contains all the directives and the given routes, but does not
|
||||
* include the router service.
|
||||
*
|
||||
* When registered at the root, the module should be used as follows
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [RouterModule.forRoot(ROUTES)]
|
||||
* })
|
||||
* class MyNgModule {}
|
||||
* ```
|
||||
*
|
||||
* For submodules and lazy loaded submodules the module should be used as follows:
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [RouterModule.forChild(ROUTES)]
|
||||
* })
|
||||
* class MyNgModule {}
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Managing state transitions is one of the hardest parts of building applications. This is
|
||||
* especially true on the web, where you also need to ensure that the state is reflected in the URL.
|
||||
* In addition, we often want to split applications into multiple bundles and load them on demand.
|
||||
* Doing this transparently is not trivial.
|
||||
*
|
||||
* The Angular router solves these problems. Using the router, you can declaratively specify
|
||||
* application states, manage state transitions while taking care of the URL, and load bundles on
|
||||
* demand.
|
||||
*
|
||||
* [Read this developer guide](https://angular.io/docs/ts/latest/guide/router.html) to get an
|
||||
* overview of how the router should be used.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@NgModule({declarations: ROUTER_DIRECTIVES, exports: ROUTER_DIRECTIVES})
|
||||
export class RouterModule {
|
||||
constructor(@Optional() @Inject(ROUTER_FORROOT_GUARD) guard: any) {}
|
||||
|
||||
/**
|
||||
* Creates a module with all the router providers and directives. It also optionally sets up an
|
||||
* application listener to perform an initial navigation.
|
||||
*
|
||||
* Options:
|
||||
* * `enableTracing` makes the router log all its internal events to the console.
|
||||
* * `useHash` enables the location strategy that uses the URL fragment instead of the history
|
||||
* API.
|
||||
* * `initialNavigation` disables the initial navigation.
|
||||
* * `errorHandler` provides a custom error handler.
|
||||
*/
|
||||
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders {
|
||||
return {
|
||||
ngModule: RouterModule,
|
||||
providers: [
|
||||
ROUTER_PROVIDERS,
|
||||
provideRoutes(routes),
|
||||
{
|
||||
provide: ROUTER_FORROOT_GUARD,
|
||||
useFactory: provideForRootGuard,
|
||||
deps: [[Router, new Optional(), new SkipSelf()]]
|
||||
},
|
||||
{provide: ROUTER_CONFIGURATION, useValue: config ? config : {}},
|
||||
{
|
||||
provide: LocationStrategy,
|
||||
useFactory: provideLocationStrategy,
|
||||
deps: [
|
||||
PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION
|
||||
]
|
||||
},
|
||||
{
|
||||
provide: PreloadingStrategy,
|
||||
useExisting: config && config.preloadingStrategy ? config.preloadingStrategy :
|
||||
NoPreloading
|
||||
},
|
||||
{provide: NgProbeToken, multi: true, useFactory: routerNgProbeToken},
|
||||
provideRouterInitializer(),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a module with all the router directives and a provider registering routes.
|
||||
*/
|
||||
static forChild(routes: Routes): ModuleWithProviders {
|
||||
return {ngModule: RouterModule, providers: [provideRoutes(routes)]};
|
||||
}
|
||||
}
|
||||
|
||||
export function provideLocationStrategy(
|
||||
platformLocationStrategy: PlatformLocation, baseHref: string, options: ExtraOptions = {}) {
|
||||
return options.useHash ? new HashLocationStrategy(platformLocationStrategy, baseHref) :
|
||||
new PathLocationStrategy(platformLocationStrategy, baseHref);
|
||||
}
|
||||
|
||||
export function provideForRootGuard(router: Router): any {
|
||||
if (router) {
|
||||
throw new Error(
|
||||
`RouterModule.forRoot() called twice. Lazy loaded modules should use RouterModule.forChild() instead.`);
|
||||
}
|
||||
return 'guarded';
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Registers routes.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [RouterModule.forChild(ROUTES)],
|
||||
* providers: [provideRoutes(EXTRA_ROUTES)]
|
||||
* })
|
||||
* class MyNgModule {}
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export function provideRoutes(routes: Routes): any {
|
||||
return [
|
||||
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
|
||||
{provide: ROUTES, multi: true, useValue: routes},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents an option to configure when the initial navigation is performed.
|
||||
*
|
||||
* @description
|
||||
* * 'enabled' - the initial navigation starts before the root component is created.
|
||||
* The bootstrap is blocked until the initial navigation is complete.
|
||||
* * 'disabled' - the initial navigation is not performed. The location listener is set up before
|
||||
* the root component gets created.
|
||||
* * 'legacy_enabled'- the initial navigation starts after the root component has been created.
|
||||
* The bootstrap is not blocked until the initial navigation is complete. @deprecated
|
||||
* * 'legacy_disabled'- the initial navigation is not performed. The location listener is set up
|
||||
* after @deprecated
|
||||
* the root component gets created.
|
||||
* * `true` - same as 'legacy_enabled'. @deprecated
|
||||
* * `false` - same as 'legacy_disabled'. @deprecated
|
||||
*
|
||||
* The 'enabled' option should be used for applications unless there is a reason to have
|
||||
* more control over when the router starts its initial navigation due to some complex
|
||||
* initialization logic. In this case, 'disabled' should be used.
|
||||
*
|
||||
* The 'legacy_enabled' and 'legacy_disabled' should not be used for new applications.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export type InitialNavigation =
|
||||
true | false | 'enabled' | 'disabled' | 'legacy_enabled' | 'legacy_disabled';
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents options to configure the router.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface ExtraOptions {
|
||||
/**
|
||||
* Makes the router log all its internal events to the console.
|
||||
*/
|
||||
enableTracing?: boolean;
|
||||
|
||||
/**
|
||||
* Enables the location strategy that uses the URL fragment instead of the history API.
|
||||
*/
|
||||
useHash?: boolean;
|
||||
|
||||
/**
|
||||
* Disables the initial navigation.
|
||||
*/
|
||||
initialNavigation?: InitialNavigation;
|
||||
|
||||
/**
|
||||
* A custom error handler.
|
||||
*/
|
||||
errorHandler?: ErrorHandler;
|
||||
|
||||
/**
|
||||
* Configures a preloading strategy. See {@link PreloadAllModules}.
|
||||
*/
|
||||
preloadingStrategy?: any;
|
||||
}
|
||||
|
||||
export function setupRouter(
|
||||
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
|
||||
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
|
||||
config: Route[][], opts: ExtraOptions = {}, urlHandlingStrategy?: UrlHandlingStrategy,
|
||||
routeReuseStrategy?: RouteReuseStrategy) {
|
||||
const router = new Router(
|
||||
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
|
||||
|
||||
if (urlHandlingStrategy) {
|
||||
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||
}
|
||||
|
||||
if (routeReuseStrategy) {
|
||||
router.routeReuseStrategy = routeReuseStrategy;
|
||||
}
|
||||
|
||||
if (opts.errorHandler) {
|
||||
router.errorHandler = opts.errorHandler;
|
||||
}
|
||||
|
||||
if (opts.enableTracing) {
|
||||
const dom = getDOM();
|
||||
router.events.subscribe(e => {
|
||||
dom.logGroup(`Router Event: ${(<any>e.constructor).name}`);
|
||||
dom.log(e.toString());
|
||||
dom.log(e);
|
||||
dom.logGroupEnd();
|
||||
});
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
export function rootRoute(router: Router): ActivatedRoute {
|
||||
return router.routerState.root;
|
||||
}
|
||||
|
||||
/**
|
||||
* To initialize the router properly we need to do in two steps:
|
||||
*
|
||||
* We need to start the navigation in a APP_INITIALIZER to block the bootstrap if
|
||||
* a resolver or a guards executes asynchronously. Second, we need to actually run
|
||||
* activation in a BOOTSTRAP_LISTENER. We utilize the afterPreactivation
|
||||
* hook provided by the router to do that.
|
||||
*
|
||||
* The router navigation starts, reaches the point when preactivation is done, and then
|
||||
* pauses. It waits for the hook to be resolved. We then resolve it only in a bootstrap listener.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RouterInitializer {
|
||||
private initNavigation: boolean = false;
|
||||
private resultOfPreactivationDone = new Subject<void>();
|
||||
|
||||
constructor(private injector: Injector) {}
|
||||
|
||||
appInitializer(): Promise<any> {
|
||||
const p: Promise<any> = this.injector.get(LOCATION_INITIALIZED, Promise.resolve(null));
|
||||
return p.then(() => {
|
||||
let resolve: Function = null;
|
||||
const res = new Promise(r => resolve = r);
|
||||
const router = this.injector.get(Router);
|
||||
const opts = this.injector.get(ROUTER_CONFIGURATION);
|
||||
|
||||
if (this.isLegacyDisabled(opts) || this.isLegacyEnabled(opts)) {
|
||||
resolve(true);
|
||||
|
||||
} else if (opts.initialNavigation === 'disabled') {
|
||||
router.setUpLocationChangeListener();
|
||||
resolve(true);
|
||||
|
||||
} else if (opts.initialNavigation === 'enabled') {
|
||||
router.hooks.afterPreactivation = () => {
|
||||
// only the initial navigation should be delayed
|
||||
if (!this.initNavigation) {
|
||||
this.initNavigation = true;
|
||||
resolve(true);
|
||||
return this.resultOfPreactivationDone;
|
||||
|
||||
// subsequent navigations should not be delayed
|
||||
} else {
|
||||
return of (null);
|
||||
}
|
||||
};
|
||||
router.initialNavigation();
|
||||
|
||||
} else {
|
||||
throw new Error(`Invalid initialNavigation options: '${opts.initialNavigation}'`);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
bootstrapListener(bootstrappedComponentRef: ComponentRef<any>): void {
|
||||
const opts = this.injector.get(ROUTER_CONFIGURATION);
|
||||
const preloader = this.injector.get(RouterPreloader);
|
||||
const router = this.injector.get(Router);
|
||||
const ref = this.injector.get(ApplicationRef);
|
||||
|
||||
if (bootstrappedComponentRef !== ref.components[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isLegacyEnabled(opts)) {
|
||||
router.initialNavigation();
|
||||
} else if (this.isLegacyDisabled(opts)) {
|
||||
router.setUpLocationChangeListener();
|
||||
}
|
||||
|
||||
preloader.setUpPreloading();
|
||||
router.resetRootComponentType(ref.componentTypes[0]);
|
||||
this.resultOfPreactivationDone.next(null);
|
||||
this.resultOfPreactivationDone.complete();
|
||||
}
|
||||
|
||||
private isLegacyEnabled(opts: ExtraOptions): boolean {
|
||||
return opts.initialNavigation === 'legacy_enabled' || opts.initialNavigation === true ||
|
||||
opts.initialNavigation === undefined;
|
||||
}
|
||||
|
||||
private isLegacyDisabled(opts: ExtraOptions): boolean {
|
||||
return opts.initialNavigation === 'legacy_disabled' || opts.initialNavigation === false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppInitializer(r: RouterInitializer) {
|
||||
return r.appInitializer.bind(r);
|
||||
}
|
||||
|
||||
export function getBootstrapListener(r: RouterInitializer) {
|
||||
return r.bootstrapListener.bind(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* A token for the router initializer that will be called after the app is bootstrapped.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const ROUTER_INITIALIZER =
|
||||
new InjectionToken<(compRef: ComponentRef<any>) => void>('Router Initializer');
|
||||
|
||||
export function provideRouterInitializer() {
|
||||
return [
|
||||
RouterInitializer,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useFactory: getAppInitializer,
|
||||
deps: [RouterInitializer]
|
||||
},
|
||||
{provide: ROUTER_INITIALIZER, useFactory: getBootstrapListener, deps: [RouterInitializer]},
|
||||
{provide: APP_BOOTSTRAP_LISTENER, multi: true, useExisting: ROUTER_INITIALIZER},
|
||||
];
|
||||
}
|
29
packages/router/src/router_outlet_map.ts
Normal file
29
packages/router/src/router_outlet_map.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {RouterOutlet} from './directives/router_outlet';
|
||||
|
||||
/**
|
||||
* @whatItDoes Contains all the router outlets created in a component.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class RouterOutletMap {
|
||||
/** @internal */
|
||||
_outlets: {[name: string]: RouterOutlet} = {};
|
||||
|
||||
/**
|
||||
* Adds an outlet to this map.
|
||||
*/
|
||||
registerOutlet(name: string, outlet: RouterOutlet): void { this._outlets[name] = outlet; }
|
||||
|
||||
/**
|
||||
* Removes an outlet from this map.
|
||||
*/
|
||||
removeOutlet(name: string): void { this._outlets[name] = undefined; }
|
||||
}
|
128
packages/router/src/router_preloader.ts
Normal file
128
packages/router/src/router_preloader.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
*@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
|
||||
*/
|
||||
|
||||
import {Compiler, Injectable, Injector, NgModuleFactoryLoader} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
import {from} from 'rxjs/observable/from';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
import {_catch} from 'rxjs/operator/catch';
|
||||
import {concatMap} from 'rxjs/operator/concatMap';
|
||||
import {filter} from 'rxjs/operator/filter';
|
||||
import {mergeAll} from 'rxjs/operator/mergeAll';
|
||||
import {mergeMap} from 'rxjs/operator/mergeMap';
|
||||
import {Route, Routes} from './config';
|
||||
import {NavigationEnd, RouteConfigLoadEnd, RouteConfigLoadStart} from './events';
|
||||
import {Router} from './router';
|
||||
import {RouterConfigLoader} from './router_config_loader';
|
||||
|
||||
/**
|
||||
* @whatItDoes Provides a preloading strategy.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export abstract class PreloadingStrategy {
|
||||
abstract preload(route: Route, fn: () => Observable<any>): Observable<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Provides a preloading strategy that preloads all modules as quicky as possible.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* RouteModule.forRoot(ROUTES, {preloadingStrategy: PreloadAllModules})
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class PreloadAllModules implements PreloadingStrategy {
|
||||
preload(route: Route, fn: () => Observable<any>): Observable<any> {
|
||||
return _catch.call(fn(), () => of (null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Provides a preloading strategy that does not preload any modules.
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* This strategy is enabled by default.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export class NoPreloading implements PreloadingStrategy {
|
||||
preload(route: Route, fn: () => Observable<any>): Observable<any> { return of (null); }
|
||||
}
|
||||
|
||||
/**
|
||||
* The preloader optimistically loads all router configurations to
|
||||
* make navigations into lazily-loaded sections of the application faster.
|
||||
*
|
||||
* The preloader runs in the background. When the router bootstraps, the preloader
|
||||
* starts listening to all navigation events. After every such event, the preloader
|
||||
* will check if any configurations can be loaded lazily.
|
||||
*
|
||||
* If a route is protected by `canLoad` guards, the preloaded will not load it.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Injectable()
|
||||
export class RouterPreloader {
|
||||
private loader: RouterConfigLoader;
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler,
|
||||
private injector: Injector, private preloadingStrategy: PreloadingStrategy) {
|
||||
const onStartLoad = (r: Route) => router.triggerEvent(new RouteConfigLoadStart(r));
|
||||
const onEndLoad = (r: Route) => router.triggerEvent(new RouteConfigLoadEnd(r));
|
||||
|
||||
this.loader = new RouterConfigLoader(moduleLoader, compiler, onStartLoad, onEndLoad);
|
||||
};
|
||||
|
||||
setUpPreloading(): void {
|
||||
const navigations = filter.call(this.router.events, (e: any) => e instanceof NavigationEnd);
|
||||
this.subscription = concatMap.call(navigations, () => this.preload()).subscribe(() => {});
|
||||
}
|
||||
|
||||
preload(): Observable<any> { return this.processRoutes(this.injector, this.router.config); }
|
||||
|
||||
ngOnDestroy(): void { this.subscription.unsubscribe(); }
|
||||
|
||||
private processRoutes(injector: Injector, routes: Routes): Observable<void> {
|
||||
const res: Observable<any>[] = [];
|
||||
for (const c of routes) {
|
||||
// we already have the config loaded, just recurse
|
||||
if (c.loadChildren && !c.canLoad && (<any>c)._loadedConfig) {
|
||||
const childConfig = (<any>c)._loadedConfig;
|
||||
res.push(this.processRoutes(childConfig.injector, childConfig.routes));
|
||||
|
||||
// no config loaded, fetch the config
|
||||
} else if (c.loadChildren && !c.canLoad) {
|
||||
res.push(this.preloadConfig(injector, c));
|
||||
|
||||
// recurse into children
|
||||
} else if (c.children) {
|
||||
res.push(this.processRoutes(injector, c.children));
|
||||
}
|
||||
}
|
||||
return mergeAll.call(from(res));
|
||||
}
|
||||
|
||||
private preloadConfig(injector: Injector, route: Route): Observable<void> {
|
||||
return this.preloadingStrategy.preload(route, () => {
|
||||
const loaded = this.loader.load(injector, route);
|
||||
return mergeMap.call(loaded, (config: any): any => {
|
||||
const c: any = route;
|
||||
c._loadedConfig = config;
|
||||
return this.processRoutes(config.injector, config.routes);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
364
packages/router/src/router_state.ts
Normal file
364
packages/router/src/router_state.ts
Normal file
@ -0,0 +1,364 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Type} from '@angular/core';
|
||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
|
||||
import {Data, ResolveData, Route} from './config';
|
||||
import {PRIMARY_OUTLET, Params} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlTree, equalSegments} from './url_tree';
|
||||
import {merge, shallowEqual, shallowEqualArrays} from './utils/collection';
|
||||
import {Tree, TreeNode} from './utils/tree';
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the state of the router.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @Component({templateUrl:'template.html'})
|
||||
* class MyComponent {
|
||||
* constructor(router: Router) {
|
||||
* const state: RouterState = router.routerState;
|
||||
* const root: ActivatedRoute = state.root;
|
||||
* const child = root.firstChild;
|
||||
* const id: Observable<string> = child.params.map(p => p.id);
|
||||
* //...
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
* RouterState is a tree of activated routes. Every node in this tree knows about the "consumed" URL
|
||||
* segments,
|
||||
* the extracted parameters, and the resolved data.
|
||||
*
|
||||
* See {@link ActivatedRoute} for more information.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class RouterState extends Tree<ActivatedRoute> {
|
||||
/** @internal */
|
||||
constructor(
|
||||
root: TreeNode<ActivatedRoute>,
|
||||
/** The current snapshot of the router state */
|
||||
public snapshot: RouterStateSnapshot) {
|
||||
super(root);
|
||||
setRouterStateSnapshot<RouterState, ActivatedRoute>(this, root);
|
||||
}
|
||||
|
||||
toString(): string { return this.snapshot.toString(); }
|
||||
}
|
||||
|
||||
export function createEmptyState(urlTree: UrlTree, rootComponent: Type<any>): RouterState {
|
||||
const snapshot = createEmptyStateSnapshot(urlTree, rootComponent);
|
||||
const emptyUrl = new BehaviorSubject([new UrlSegment('', {})]);
|
||||
const emptyParams = new BehaviorSubject({});
|
||||
const emptyData = new BehaviorSubject({});
|
||||
const emptyQueryParams = new BehaviorSubject({});
|
||||
const fragment = new BehaviorSubject('');
|
||||
const activated = new ActivatedRoute(
|
||||
emptyUrl, emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent,
|
||||
snapshot.root);
|
||||
activated.snapshot = snapshot.root;
|
||||
return new RouterState(new TreeNode<ActivatedRoute>(activated, []), snapshot);
|
||||
}
|
||||
|
||||
export function createEmptyStateSnapshot(
|
||||
urlTree: UrlTree, rootComponent: Type<any>): RouterStateSnapshot {
|
||||
const emptyParams = {};
|
||||
const emptyData = {};
|
||||
const emptyQueryParams = {};
|
||||
const fragment = '';
|
||||
const activated = new ActivatedRouteSnapshot(
|
||||
[], emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, null,
|
||||
urlTree.root, -1, {});
|
||||
return new RouterStateSnapshot('', new TreeNode<ActivatedRouteSnapshot>(activated, []));
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Contains the information about a route associated with a component loaded in an
|
||||
* outlet.
|
||||
* An `ActivatedRoute` can also be used to traverse the router state tree.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @Component({...})
|
||||
* class MyComponent {
|
||||
* constructor(route: ActivatedRoute) {
|
||||
* const id: Observable<string> = route.params.map(p => p.id);
|
||||
* const url: Observable<string> = route.url.map(segments => segments.join(''));
|
||||
* // route.data includes both `data` and `resolve`
|
||||
* const user = route.data.map(d => d.user);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class ActivatedRoute {
|
||||
/** The current snapshot of this route */
|
||||
snapshot: ActivatedRouteSnapshot;
|
||||
/** @internal */
|
||||
_futureSnapshot: ActivatedRouteSnapshot;
|
||||
/** @internal */
|
||||
_routerState: RouterState;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** An observable of the URL segments matched by this route */
|
||||
public url: Observable<UrlSegment[]>,
|
||||
/** An observable of the matrix parameters scoped to this route */
|
||||
public params: Observable<Params>,
|
||||
/** An observable of the query parameters shared by all the routes */
|
||||
public queryParams: Observable<Params>,
|
||||
/** An observable of the URL fragment shared by all the routes */
|
||||
public fragment: Observable<string>,
|
||||
/** An observable of the static and resolved data of this route. */
|
||||
public data: Observable<Data>,
|
||||
/** The outlet name of the route. It's a constant */
|
||||
public outlet: string,
|
||||
/** The component of the route. It's a constant */
|
||||
// TODO(vsavkin): remove |string
|
||||
public component: Type<any>|string, futureSnapshot: ActivatedRouteSnapshot) {
|
||||
this._futureSnapshot = futureSnapshot;
|
||||
}
|
||||
|
||||
/** The configuration used to match this route */
|
||||
get routeConfig(): Route { return this._futureSnapshot.routeConfig; }
|
||||
|
||||
/** The root of the router state */
|
||||
get root(): ActivatedRoute { return this._routerState.root; }
|
||||
|
||||
/** The parent of this route in the router state tree */
|
||||
get parent(): ActivatedRoute { return this._routerState.parent(this); }
|
||||
|
||||
/** The first child of this route in the router state tree */
|
||||
get firstChild(): ActivatedRoute { return this._routerState.firstChild(this); }
|
||||
|
||||
/** The children of this route in the router state tree */
|
||||
get children(): ActivatedRoute[] { return this._routerState.children(this); }
|
||||
|
||||
/** The path from the root of the router state tree to this route */
|
||||
get pathFromRoot(): ActivatedRoute[] { return this._routerState.pathFromRoot(this); }
|
||||
|
||||
toString(): string {
|
||||
return this.snapshot ? this.snapshot.toString() : `Future(${this._futureSnapshot})`;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type Inherited = {
|
||||
params: Params,
|
||||
data: Data,
|
||||
resolve: Data,
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export function inheritedParamsDataResolve(route: ActivatedRouteSnapshot): Inherited {
|
||||
const pathToRoot = route.pathFromRoot;
|
||||
|
||||
let inhertingStartingFrom = pathToRoot.length - 1;
|
||||
|
||||
while (inhertingStartingFrom >= 1) {
|
||||
const current = pathToRoot[inhertingStartingFrom];
|
||||
const parent = pathToRoot[inhertingStartingFrom - 1];
|
||||
// current route is an empty path => inherits its parent's params and data
|
||||
if (current.routeConfig && current.routeConfig.path === '') {
|
||||
inhertingStartingFrom--;
|
||||
|
||||
// parent is componentless => current route should inherit its params and data
|
||||
} else if (!parent.component) {
|
||||
inhertingStartingFrom--;
|
||||
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return pathToRoot.slice(inhertingStartingFrom).reduce((res, curr) => {
|
||||
const params = merge(res.params, curr.params);
|
||||
const data = merge(res.data, curr.data);
|
||||
const resolve = merge(res.resolve, curr._resolvedData);
|
||||
return {params, data, resolve};
|
||||
}, <any>{params: {}, data: {}, resolve: {}});
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Contains the information about a route associated with a component loaded in an
|
||||
* outlet
|
||||
* at a particular moment in time. ActivatedRouteSnapshot can also be used to traverse the router
|
||||
* state tree.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @Component({templateUrl:'./my-component.html'})
|
||||
* class MyComponent {
|
||||
* constructor(route: ActivatedRoute) {
|
||||
* const id: string = route.snapshot.params.id;
|
||||
* const url: string = route.snapshot.url.join('');
|
||||
* const user = route.snapshot.data.user;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class ActivatedRouteSnapshot {
|
||||
/** @internal **/
|
||||
_routeConfig: Route;
|
||||
/** @internal **/
|
||||
_urlSegment: UrlSegmentGroup;
|
||||
/** @internal */
|
||||
_lastPathIndex: number;
|
||||
/** @internal */
|
||||
_resolve: ResolveData;
|
||||
/** @internal */
|
||||
_resolvedData: Data;
|
||||
/** @internal */
|
||||
_routerState: RouterStateSnapshot;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** The URL segments matched by this route */
|
||||
public url: UrlSegment[],
|
||||
/** The matrix parameters scoped to this route */
|
||||
public params: Params,
|
||||
/** The query parameters shared by all the routes */
|
||||
public queryParams: Params,
|
||||
/** The URL fragment shared by all the routes */
|
||||
public fragment: string,
|
||||
/** The static and resolved data of this route */
|
||||
public data: Data,
|
||||
/** The outlet name of the route */
|
||||
public outlet: string,
|
||||
/** The component of the route */
|
||||
public component: Type<any>|string, routeConfig: Route, urlSegment: UrlSegmentGroup,
|
||||
lastPathIndex: number, resolve: ResolveData) {
|
||||
this._routeConfig = routeConfig;
|
||||
this._urlSegment = urlSegment;
|
||||
this._lastPathIndex = lastPathIndex;
|
||||
this._resolve = resolve;
|
||||
}
|
||||
|
||||
/** The configuration used to match this route */
|
||||
get routeConfig(): Route { return this._routeConfig; }
|
||||
|
||||
/** The root of the router state */
|
||||
get root(): ActivatedRouteSnapshot { return this._routerState.root; }
|
||||
|
||||
/** The parent of this route in the router state tree */
|
||||
get parent(): ActivatedRouteSnapshot { return this._routerState.parent(this); }
|
||||
|
||||
/** The first child of this route in the router state tree */
|
||||
get firstChild(): ActivatedRouteSnapshot { return this._routerState.firstChild(this); }
|
||||
|
||||
/** The children of this route in the router state tree */
|
||||
get children(): ActivatedRouteSnapshot[] { return this._routerState.children(this); }
|
||||
|
||||
/** The path from the root of the router state tree to this route */
|
||||
get pathFromRoot(): ActivatedRouteSnapshot[] { return this._routerState.pathFromRoot(this); }
|
||||
|
||||
toString(): string {
|
||||
const url = this.url.map(segment => segment.toString()).join('/');
|
||||
const matched = this._routeConfig ? this._routeConfig.path : '';
|
||||
return `Route(url:'${url}', path:'${matched}')`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the state of the router at a moment in time.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @Component({templateUrl:'template.html'})
|
||||
* class MyComponent {
|
||||
* constructor(router: Router) {
|
||||
* const state: RouterState = router.routerState;
|
||||
* const snapshot: RouterStateSnapshot = state.snapshot;
|
||||
* const root: ActivatedRouteSnapshot = snapshot.root;
|
||||
* const child = root.firstChild;
|
||||
* const id: Observable<string> = child.params.map(p => p.id);
|
||||
* //...
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
* RouterStateSnapshot is a tree of activated route snapshots. Every node in this tree knows about
|
||||
* the "consumed" URL segments, the extracted parameters, and the resolved data.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** The url from which this snapshot was created */
|
||||
public url: string, root: TreeNode<ActivatedRouteSnapshot>) {
|
||||
super(root);
|
||||
setRouterStateSnapshot<RouterStateSnapshot, ActivatedRouteSnapshot>(this, root);
|
||||
}
|
||||
|
||||
toString(): string { return serializeNode(this._root); }
|
||||
}
|
||||
|
||||
function setRouterStateSnapshot<U, T extends{_routerState: U}>(state: U, node: TreeNode<T>): void {
|
||||
node.value._routerState = state;
|
||||
node.children.forEach(c => setRouterStateSnapshot(state, c));
|
||||
}
|
||||
|
||||
function serializeNode(node: TreeNode<ActivatedRouteSnapshot>): string {
|
||||
const c = node.children.length > 0 ? ` { ${node.children.map(serializeNode).join(", ")} } ` : '';
|
||||
return `${node.value}${c}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The expectation is that the activate route is created with the right set of parameters.
|
||||
* So we push new values into the observables only when they are not the initial values.
|
||||
* And we detect that by checking if the snapshot field is set.
|
||||
*/
|
||||
export function advanceActivatedRoute(route: ActivatedRoute): void {
|
||||
if (route.snapshot) {
|
||||
const currentSnapshot = route.snapshot;
|
||||
route.snapshot = route._futureSnapshot;
|
||||
if (!shallowEqual(currentSnapshot.queryParams, route._futureSnapshot.queryParams)) {
|
||||
(<any>route.queryParams).next(route._futureSnapshot.queryParams);
|
||||
}
|
||||
if (currentSnapshot.fragment !== route._futureSnapshot.fragment) {
|
||||
(<any>route.fragment).next(route._futureSnapshot.fragment);
|
||||
}
|
||||
if (!shallowEqual(currentSnapshot.params, route._futureSnapshot.params)) {
|
||||
(<any>route.params).next(route._futureSnapshot.params);
|
||||
}
|
||||
if (!shallowEqualArrays(currentSnapshot.url, route._futureSnapshot.url)) {
|
||||
(<any>route.url).next(route._futureSnapshot.url);
|
||||
}
|
||||
if (!equalParamsAndUrlSegments(currentSnapshot, route._futureSnapshot)) {
|
||||
(<any>route.data).next(route._futureSnapshot.data);
|
||||
}
|
||||
} else {
|
||||
route.snapshot = route._futureSnapshot;
|
||||
|
||||
// this is for resolved data
|
||||
(<any>route.data).next(route._futureSnapshot.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function equalParamsAndUrlSegments(
|
||||
a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
|
||||
const equalUrlParams = shallowEqual(a.params, b.params) && equalSegments(a.url, b.url);
|
||||
const parentsMismatch = !a.parent !== !b.parent;
|
||||
|
||||
return equalUrlParams && !parentsMismatch &&
|
||||
(!a.parent || equalParamsAndUrlSegments(a.parent, b.parent));
|
||||
}
|
72
packages/router/src/shared.ts
Normal file
72
packages/router/src/shared.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
|
||||
import {Route, UrlMatchResult} from './config';
|
||||
import {UrlSegment, UrlSegmentGroup} from './url_tree';
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Name of the primary outlet.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export const PRIMARY_OUTLET = 'primary';
|
||||
|
||||
/**
|
||||
* A collection of parameters.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export type Params = {
|
||||
[key: string]: any
|
||||
};
|
||||
|
||||
const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';
|
||||
|
||||
export function navigationCancelingError(message: string) {
|
||||
const error = Error('NavigationCancelingError: ' + message);
|
||||
(error as any)[NAVIGATION_CANCELING_ERROR] = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
export function isNavigationCancelingError(error: Error) {
|
||||
return (error as any)[NAVIGATION_CANCELING_ERROR];
|
||||
}
|
||||
|
||||
export function defaultUrlMatcher(
|
||||
segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult {
|
||||
const path = route.path;
|
||||
const parts = path.split('/');
|
||||
const posParams: {[key: string]: UrlSegment} = {};
|
||||
const consumed: UrlSegment[] = [];
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (currentIndex >= segments.length) return null;
|
||||
const current = segments[currentIndex];
|
||||
|
||||
const p = parts[i];
|
||||
const isPosParam = p.startsWith(':');
|
||||
|
||||
if (!isPosParam && p !== current.path) return null;
|
||||
if (isPosParam) {
|
||||
posParams[p.substring(1)] = current;
|
||||
}
|
||||
consumed.push(current);
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
if (route.pathMatch === 'full' &&
|
||||
(segmentGroup.hasChildren() || currentIndex < segments.length)) {
|
||||
return null;
|
||||
} else {
|
||||
return {consumed, posParams};
|
||||
}
|
||||
}
|
46
packages/router/src/url_handling_strategy.ts
Normal file
46
packages/router/src/url_handling_strategy.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {UrlTree} from './url_tree';
|
||||
|
||||
/**
|
||||
* @whatItDoes Provides a way to migrate AngularJS applications to Angular.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export abstract class UrlHandlingStrategy {
|
||||
/**
|
||||
* Tells the router if this URL should be processed.
|
||||
*
|
||||
* When it returns true, the router will execute the regular navigation.
|
||||
* When it returns false, the router will set the router state to an empty state.
|
||||
* As a result, all the active components will be destroyed.
|
||||
*
|
||||
*/
|
||||
abstract shouldProcessUrl(url: UrlTree): boolean;
|
||||
|
||||
/**
|
||||
* Extracts the part of the URL that should be handled by the router.
|
||||
* The rest of the URL will remain untouched.
|
||||
*/
|
||||
abstract extract(url: UrlTree): UrlTree;
|
||||
|
||||
/**
|
||||
* Merges the URL fragment with the rest of the URL.
|
||||
*/
|
||||
abstract merge(newUrlPart: UrlTree, rawUrl: UrlTree): UrlTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
export class DefaultUrlHandlingStrategy implements UrlHandlingStrategy {
|
||||
shouldProcessUrl(url: UrlTree): boolean { return true; }
|
||||
extract(url: UrlTree): UrlTree { return url; }
|
||||
merge(newUrlPart: UrlTree, wholeUrl: UrlTree): UrlTree { return newUrlPart; }
|
||||
}
|
568
packages/router/src/url_tree.ts
Normal file
568
packages/router/src/url_tree.ts
Normal file
@ -0,0 +1,568 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {PRIMARY_OUTLET} from './shared';
|
||||
import {forEach, shallowEqual} from './utils/collection';
|
||||
|
||||
export function createEmptyUrlTree() {
|
||||
return new UrlTree(new UrlSegmentGroup([], {}), {}, null);
|
||||
}
|
||||
|
||||
export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean {
|
||||
if (exact) {
|
||||
return equalQueryParams(container.queryParams, containee.queryParams) &&
|
||||
equalSegmentGroups(container.root, containee.root);
|
||||
}
|
||||
|
||||
return containsQueryParams(container.queryParams, containee.queryParams) &&
|
||||
containsSegmentGroup(container.root, containee.root);
|
||||
}
|
||||
|
||||
function equalQueryParams(
|
||||
container: {[k: string]: string}, containee: {[k: string]: string}): boolean {
|
||||
return shallowEqual(container, containee);
|
||||
}
|
||||
|
||||
function equalSegmentGroups(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
|
||||
if (!equalPath(container.segments, containee.segments)) return false;
|
||||
if (container.numberOfChildren !== containee.numberOfChildren) return false;
|
||||
for (const c in containee.children) {
|
||||
if (!container.children[c]) return false;
|
||||
if (!equalSegmentGroups(container.children[c], containee.children[c])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function containsQueryParams(
|
||||
container: {[k: string]: string}, containee: {[k: string]: string}): boolean {
|
||||
return Object.keys(containee).length <= Object.keys(container).length &&
|
||||
Object.keys(containee).every(key => containee[key] === container[key]);
|
||||
}
|
||||
|
||||
function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
|
||||
return containsSegmentGroupHelper(container, containee, containee.segments);
|
||||
}
|
||||
|
||||
function containsSegmentGroupHelper(
|
||||
container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[]): boolean {
|
||||
if (container.segments.length > containeePaths.length) {
|
||||
const current = container.segments.slice(0, containeePaths.length);
|
||||
if (!equalPath(current, containeePaths)) return false;
|
||||
if (containee.hasChildren()) return false;
|
||||
return true;
|
||||
|
||||
} else if (container.segments.length === containeePaths.length) {
|
||||
if (!equalPath(container.segments, containeePaths)) return false;
|
||||
for (const c in containee.children) {
|
||||
if (!container.children[c]) return false;
|
||||
if (!containsSegmentGroup(container.children[c], containee.children[c])) return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
} else {
|
||||
const current = containeePaths.slice(0, container.segments.length);
|
||||
const next = containeePaths.slice(container.segments.length);
|
||||
if (!equalPath(container.segments, current)) return false;
|
||||
if (!container.children[PRIMARY_OUTLET]) return false;
|
||||
return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the parsed URL.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @Component({templateUrl:'template.html'})
|
||||
* class MyComponent {
|
||||
* constructor(router: Router) {
|
||||
* const tree: UrlTree =
|
||||
* router.parseUrl('/team/33/(user/victor//support:help)?debug=true#fragment');
|
||||
* const f = tree.fragment; // return 'fragment'
|
||||
* const q = tree.queryParams; // returns {debug: 'true'}
|
||||
* const g: UrlSegmentGroup = tree.root.children[PRIMARY_OUTLET];
|
||||
* const s: UrlSegment[] = g.segments; // returns 2 segments 'team' and '33'
|
||||
* g.children[PRIMARY_OUTLET].segments; // returns 2 segments 'user' and 'victor'
|
||||
* g.children['support'].segments; // return 1 segment 'help'
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Since a router state is a tree, and the URL is nothing but a serialized state, the URL is a
|
||||
* serialized tree.
|
||||
* UrlTree is a data structure that provides a lot of affordances in dealing with URLs
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class UrlTree {
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** The root segment group of the URL tree */
|
||||
public root: UrlSegmentGroup,
|
||||
/** The query params of the URL */
|
||||
public queryParams: {[key: string]: string},
|
||||
/** The fragment of the URL */
|
||||
public fragment: string) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return new DefaultUrlSerializer().serialize(this); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the parsed URL segment group.
|
||||
*
|
||||
* See {@link UrlTree} for more information.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class UrlSegmentGroup {
|
||||
/** @internal */
|
||||
_sourceSegment: UrlSegmentGroup;
|
||||
/** @internal */
|
||||
_segmentIndexShift: number;
|
||||
/** The parent node in the url tree */
|
||||
parent: UrlSegmentGroup = null;
|
||||
|
||||
constructor(
|
||||
/** The URL segments of this group. See {@link UrlSegment} for more information */
|
||||
public segments: UrlSegment[],
|
||||
/** The list of children of this group */
|
||||
public children: {[key: string]: UrlSegmentGroup}) {
|
||||
forEach(children, (v: any, k: any) => v.parent = this);
|
||||
}
|
||||
|
||||
/** Wether the segment has child segments */
|
||||
hasChildren(): boolean { return this.numberOfChildren > 0; }
|
||||
|
||||
/** Number of child segments */
|
||||
get numberOfChildren(): number { return Object.keys(this.children).length; }
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return serializePaths(this); }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents a single URL segment.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @Component({templateUrl:'template.html'})
|
||||
* class MyComponent {
|
||||
* constructor(router: Router) {
|
||||
* const tree: UrlTree = router.parseUrl('/team;id=33');
|
||||
* const g: UrlSegmentGroup = tree.root.children[PRIMARY_OUTLET];
|
||||
* const s: UrlSegment[] = g.segments;
|
||||
* s[0].path; // returns 'team'
|
||||
* s[0].parameters; // returns {id: 33}
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* A UrlSegment is a part of a URL between the two slashes. It contains a path and the matrix
|
||||
* parameters associated with the segment.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class UrlSegment {
|
||||
constructor(
|
||||
/** The path part of a URL segment */
|
||||
public path: string,
|
||||
|
||||
/** The matrix parameters associated with a segment */
|
||||
public parameters: {[name: string]: string}) {}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return serializePath(this); }
|
||||
}
|
||||
|
||||
export function equalSegments(a: UrlSegment[], b: UrlSegment[]): 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;
|
||||
if (!shallowEqual(a[i].parameters, b[i].parameters)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function equalPath(a: UrlSegment[], b: UrlSegment[]): 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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function mapChildrenIntoArray<T>(
|
||||
segment: UrlSegmentGroup, fn: (v: UrlSegmentGroup, k: string) => T[]): T[] {
|
||||
let res: T[] = [];
|
||||
forEach(segment.children, (child: UrlSegmentGroup, childOutlet: string) => {
|
||||
if (childOutlet === PRIMARY_OUTLET) {
|
||||
res = res.concat(fn(child, childOutlet));
|
||||
}
|
||||
});
|
||||
forEach(segment.children, (child: UrlSegmentGroup, childOutlet: string) => {
|
||||
if (childOutlet !== PRIMARY_OUTLET) {
|
||||
res = res.concat(fn(child, childOutlet));
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Serializes and deserializes a URL string into a URL tree.
|
||||
*
|
||||
* @description The url serialization strategy is customizable. You can
|
||||
* make all URLs case insensitive by providing a custom UrlSerializer.
|
||||
*
|
||||
* See {@link DefaultUrlSerializer} for an example of a URL serializer.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export abstract class UrlSerializer {
|
||||
/** Parse a url into a {@link UrlTree} */
|
||||
abstract parse(url: string): UrlTree;
|
||||
|
||||
/** Converts a {@link UrlTree} into a url */
|
||||
abstract serialize(tree: UrlTree): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes A default implementation of the {@link UrlSerializer}.
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* Example URLs:
|
||||
*
|
||||
* ```
|
||||
* /inbox/33(popup:compose)
|
||||
* /inbox/33;open=true/messages/44
|
||||
* ```
|
||||
*
|
||||
* DefaultUrlSerializer uses parentheses to serialize secondary segments (e.g., popup:compose), the
|
||||
* colon syntax to specify the outlet, and the ';parameter=value' syntax (e.g., open=true) to
|
||||
* specify route specific parameters.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export class DefaultUrlSerializer implements UrlSerializer {
|
||||
/** Parses a url into a {@link UrlTree} */
|
||||
parse(url: string): UrlTree {
|
||||
const p = new UrlParser(url);
|
||||
return new UrlTree(p.parseRootSegment(), p.parseQueryParams(), p.parseFragment());
|
||||
}
|
||||
|
||||
/** Converts a {@link UrlTree} into a url */
|
||||
serialize(tree: UrlTree): string {
|
||||
const segment = `/${serializeSegment(tree.root, true)}`;
|
||||
const query = serializeQueryParams(tree.queryParams);
|
||||
const fragment =
|
||||
tree.fragment !== null && tree.fragment !== undefined ? `#${encodeURI(tree.fragment)}` : '';
|
||||
return `${segment}${query}${fragment}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializePaths(segment: UrlSegmentGroup): string {
|
||||
return segment.segments.map(p => serializePath(p)).join('/');
|
||||
}
|
||||
|
||||
function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
|
||||
if (segment.hasChildren() && root) {
|
||||
const primary = segment.children[PRIMARY_OUTLET] ?
|
||||
serializeSegment(segment.children[PRIMARY_OUTLET], false) :
|
||||
'';
|
||||
const children: string[] = [];
|
||||
forEach(segment.children, (v: UrlSegmentGroup, k: string) => {
|
||||
if (k !== PRIMARY_OUTLET) {
|
||||
children.push(`${k}:${serializeSegment(v, false)}`);
|
||||
}
|
||||
});
|
||||
if (children.length > 0) {
|
||||
return `${primary}(${children.join('//')})`;
|
||||
} else {
|
||||
return `${primary}`;
|
||||
}
|
||||
|
||||
} else if (segment.hasChildren() && !root) {
|
||||
const children = mapChildrenIntoArray(segment, (v: UrlSegmentGroup, k: string) => {
|
||||
if (k === PRIMARY_OUTLET) {
|
||||
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
|
||||
} else {
|
||||
return [`${k}:${serializeSegment(v, false)}`];
|
||||
}
|
||||
});
|
||||
return `${serializePaths(segment)}/(${children.join('//')})`;
|
||||
|
||||
} else {
|
||||
return serializePaths(segment);
|
||||
}
|
||||
}
|
||||
|
||||
export function encode(s: string): string {
|
||||
return encodeURIComponent(s);
|
||||
}
|
||||
|
||||
export function decode(s: string): string {
|
||||
return decodeURIComponent(s);
|
||||
}
|
||||
|
||||
export function serializePath(path: UrlSegment): string {
|
||||
return `${encode(path.path)}${serializeParams(path.parameters)}`;
|
||||
}
|
||||
|
||||
function serializeParams(params: {[key: string]: string}): string {
|
||||
return pairs(params).map(p => `;${encode(p.first)}=${encode(p.second)}`).join('');
|
||||
}
|
||||
|
||||
function serializeQueryParams(params: {[key: string]: any}): string {
|
||||
const strParams: string[] = Object.keys(params).map((name) => {
|
||||
const value = params[name];
|
||||
return Array.isArray(value) ? value.map(v => `${encode(name)}=${encode(v)}`).join('&') :
|
||||
`${encode(name)}=${encode(value)}`;
|
||||
});
|
||||
|
||||
return strParams.length ? `?${strParams.join("&")}` : '';
|
||||
}
|
||||
|
||||
class Pair<A, B> {
|
||||
constructor(public first: A, public second: B) {}
|
||||
}
|
||||
|
||||
function pairs<T>(obj: {[key: string]: T}): Pair<string, T>[] {
|
||||
const res: Pair<string, T>[] = [];
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res.push(new Pair<string, T>(prop, obj[prop]));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const SEGMENT_RE = /^[^\/()?;=&#]+/;
|
||||
function matchSegments(str: string): string {
|
||||
SEGMENT_RE.lastIndex = 0;
|
||||
const match = str.match(SEGMENT_RE);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
const QUERY_PARAM_RE = /^[^=?&#]+/;
|
||||
// Return the name of the query param at the start of the string or an empty string
|
||||
function matchQueryParams(str: string): string {
|
||||
QUERY_PARAM_RE.lastIndex = 0;
|
||||
const match = str.match(SEGMENT_RE);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
const QUERY_PARAM_VALUE_RE = /^[^?&#]+/;
|
||||
// Return the value of the query param at the start of the string or an empty string
|
||||
function matchUrlQueryParamValue(str: string): string {
|
||||
QUERY_PARAM_VALUE_RE.lastIndex = 0;
|
||||
const match = str.match(QUERY_PARAM_VALUE_RE);
|
||||
return match ? match[0] : '';
|
||||
}
|
||||
|
||||
class UrlParser {
|
||||
private remaining: string;
|
||||
constructor(private url: string) { this.remaining = url; }
|
||||
|
||||
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
|
||||
|
||||
capture(str: string): void {
|
||||
if (!this.remaining.startsWith(str)) {
|
||||
throw new Error(`Expected "${str}".`);
|
||||
}
|
||||
this.remaining = this.remaining.substring(str.length);
|
||||
}
|
||||
|
||||
parseRootSegment(): UrlSegmentGroup {
|
||||
if (this.remaining.startsWith('/')) {
|
||||
this.capture('/');
|
||||
}
|
||||
|
||||
if (this.remaining === '' || this.remaining.startsWith('?') || this.remaining.startsWith('#')) {
|
||||
return new UrlSegmentGroup([], {});
|
||||
}
|
||||
|
||||
return new UrlSegmentGroup([], this.parseChildren());
|
||||
}
|
||||
|
||||
parseChildren(): {[key: string]: UrlSegmentGroup} {
|
||||
if (this.remaining.length == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (this.peekStartsWith('/')) {
|
||||
this.capture('/');
|
||||
}
|
||||
|
||||
const paths: any[] = [];
|
||||
if (!this.peekStartsWith('(')) {
|
||||
paths.push(this.parseSegments());
|
||||
}
|
||||
|
||||
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
|
||||
this.capture('/');
|
||||
paths.push(this.parseSegments());
|
||||
}
|
||||
|
||||
let children: {[key: string]: UrlSegmentGroup} = {};
|
||||
if (this.peekStartsWith('/(')) {
|
||||
this.capture('/');
|
||||
children = this.parseParens(true);
|
||||
}
|
||||
|
||||
let res: {[key: string]: UrlSegmentGroup} = {};
|
||||
if (this.peekStartsWith('(')) {
|
||||
res = this.parseParens(false);
|
||||
}
|
||||
|
||||
if (paths.length > 0 || Object.keys(children).length > 0) {
|
||||
res[PRIMARY_OUTLET] = new UrlSegmentGroup(paths, children);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
parseSegments(): UrlSegment {
|
||||
const path = matchSegments(this.remaining);
|
||||
if (path === '' && this.peekStartsWith(';')) {
|
||||
throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`);
|
||||
}
|
||||
|
||||
this.capture(path);
|
||||
let matrixParams: {[key: string]: any} = {};
|
||||
if (this.peekStartsWith(';')) {
|
||||
matrixParams = this.parseMatrixParams();
|
||||
}
|
||||
return new UrlSegment(decode(path), matrixParams);
|
||||
}
|
||||
|
||||
parseQueryParams(): {[key: string]: any} {
|
||||
const params: {[key: string]: any} = {};
|
||||
if (this.peekStartsWith('?')) {
|
||||
this.capture('?');
|
||||
this.parseQueryParam(params);
|
||||
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
|
||||
this.capture('&');
|
||||
this.parseQueryParam(params);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
parseFragment(): string {
|
||||
if (this.peekStartsWith('#')) {
|
||||
return decodeURI(this.remaining.substring(1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
parseMatrixParams(): {[key: string]: any} {
|
||||
const params: {[key: string]: any} = {};
|
||||
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
|
||||
this.capture(';');
|
||||
this.parseParam(params);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
parseParam(params: {[key: string]: any}): void {
|
||||
const key = matchSegments(this.remaining);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
this.capture(key);
|
||||
let value: any = '';
|
||||
if (this.peekStartsWith('=')) {
|
||||
this.capture('=');
|
||||
const valueMatch = matchSegments(this.remaining);
|
||||
if (valueMatch) {
|
||||
value = valueMatch;
|
||||
this.capture(value);
|
||||
}
|
||||
}
|
||||
|
||||
params[decode(key)] = decode(value);
|
||||
}
|
||||
|
||||
// Parse a single query parameter `name[=value]`
|
||||
parseQueryParam(params: {[key: string]: any}): void {
|
||||
const key = matchQueryParams(this.remaining);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
this.capture(key);
|
||||
let value: any = '';
|
||||
if (this.peekStartsWith('=')) {
|
||||
this.capture('=');
|
||||
const valueMatch = matchUrlQueryParamValue(this.remaining);
|
||||
if (valueMatch) {
|
||||
value = valueMatch;
|
||||
this.capture(value);
|
||||
}
|
||||
}
|
||||
|
||||
const decodedKey = decode(key);
|
||||
const decodedVal = decode(value);
|
||||
|
||||
if (params.hasOwnProperty(decodedKey)) {
|
||||
// Append to existing values
|
||||
let currentVal = params[decodedKey];
|
||||
if (!Array.isArray(currentVal)) {
|
||||
currentVal = [currentVal];
|
||||
params[decodedKey] = currentVal;
|
||||
}
|
||||
currentVal.push(decodedVal);
|
||||
} else {
|
||||
// Create a new value
|
||||
params[decodedKey] = decodedVal;
|
||||
}
|
||||
}
|
||||
|
||||
parseParens(allowPrimary: boolean): {[key: string]: UrlSegmentGroup} {
|
||||
const segments: {[key: string]: UrlSegmentGroup} = {};
|
||||
this.capture('(');
|
||||
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
||||
const path = matchSegments(this.remaining);
|
||||
|
||||
const next = this.remaining[path.length];
|
||||
|
||||
// if is is not one of these characters, then the segment was unescaped
|
||||
// or the group was not closed
|
||||
if (next !== '/' && next !== ')' && next !== ';') {
|
||||
throw new Error(`Cannot parse url '${this.url}'`);
|
||||
}
|
||||
|
||||
let outletName: string;
|
||||
if (path.indexOf(':') > -1) {
|
||||
outletName = path.substr(0, path.indexOf(':'));
|
||||
this.capture(outletName);
|
||||
this.capture(':');
|
||||
} else if (allowPrimary) {
|
||||
outletName = PRIMARY_OUTLET;
|
||||
}
|
||||
|
||||
const children = this.parseChildren();
|
||||
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
|
||||
new UrlSegmentGroup([], children);
|
||||
if (this.peekStartsWith('//')) {
|
||||
this.capture('//');
|
||||
}
|
||||
}
|
||||
this.capture(')');
|
||||
return segments;
|
||||
}
|
||||
}
|
140
packages/router/src/utils/collection.ts
Normal file
140
packages/router/src/utils/collection.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {NgModuleFactory, ɵisObservable as isObservable, ɵisPromise as isPromise} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {fromPromise} from 'rxjs/observable/fromPromise';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
import {concatAll} from 'rxjs/operator/concatAll';
|
||||
import {every} from 'rxjs/operator/every';
|
||||
import * as l from 'rxjs/operator/last';
|
||||
import {map} from 'rxjs/operator/map';
|
||||
import {mergeAll} from 'rxjs/operator/mergeAll';
|
||||
import {PRIMARY_OUTLET} from '../shared';
|
||||
|
||||
export function shallowEqualArrays(a: any[], b: any[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (!shallowEqual(a[i], b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shallowEqual(a: {[x: string]: any}, b: {[x: string]: any}): boolean {
|
||||
const k1 = Object.keys(a);
|
||||
const k2 = Object.keys(b);
|
||||
if (k1.length != k2.length) {
|
||||
return false;
|
||||
}
|
||||
let key: string;
|
||||
for (let i = 0; i < k1.length; i++) {
|
||||
key = k1[i];
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function flatten<T>(a: T[][]): T[] {
|
||||
const target: T[] = [];
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
for (let j = 0; j < a[i].length; ++j) {
|
||||
target.push(a[i][j]);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
export function first<T>(a: T[]): T {
|
||||
return a.length > 0 ? a[0] : null;
|
||||
}
|
||||
|
||||
export function last<T>(a: T[]): T {
|
||||
return a.length > 0 ? a[a.length - 1] : null;
|
||||
}
|
||||
|
||||
export function and(bools: boolean[]): boolean {
|
||||
return !bools.some(v => !v);
|
||||
}
|
||||
|
||||
export function merge<V>(m1: {[key: string]: V}, m2: {[key: string]: V}): {[key: string]: V} {
|
||||
const m: {[key: string]: V} = {};
|
||||
|
||||
for (const attr in m1) {
|
||||
if (m1.hasOwnProperty(attr)) {
|
||||
m[attr] = m1[attr];
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr in m2) {
|
||||
if (m2.hasOwnProperty(attr)) {
|
||||
m[attr] = m2[attr];
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
export function forEach<K, V>(map: {[key: string]: V}, callback: (v: V, k: string) => void): void {
|
||||
for (const prop in map) {
|
||||
if (map.hasOwnProperty(prop)) {
|
||||
callback(map[prop], prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function waitForMap<A, B>(
|
||||
obj: {[k: string]: A}, fn: (k: string, a: A) => Observable<B>): Observable<{[k: string]: B}> {
|
||||
const waitFor: Observable<B>[] = [];
|
||||
const res: {[k: string]: B} = {};
|
||||
|
||||
forEach(obj, (a: A, k: string) => {
|
||||
if (k === PRIMARY_OUTLET) {
|
||||
waitFor.push(map.call(fn(k, a), (_: B) => {
|
||||
res[k] = _;
|
||||
return _;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
forEach(obj, (a: A, k: string) => {
|
||||
if (k !== PRIMARY_OUTLET) {
|
||||
waitFor.push(map.call(fn(k, a), (_: B) => {
|
||||
res[k] = _;
|
||||
return _;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
if (waitFor.length > 0) {
|
||||
const concatted$ = concatAll.call(of (...waitFor));
|
||||
const last$ = l.last.call(concatted$);
|
||||
return map.call(last$, () => res);
|
||||
}
|
||||
|
||||
return of (res);
|
||||
}
|
||||
|
||||
export function andObservables(observables: Observable<Observable<any>>): Observable<boolean> {
|
||||
const merged$ = mergeAll.call(observables);
|
||||
return every.call(merged$, (result: any) => result === true);
|
||||
}
|
||||
|
||||
export function wrapIntoObservable<T>(value: T | NgModuleFactory<T>| Promise<T>| Observable<T>):
|
||||
Observable<T> {
|
||||
if (isObservable(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isPromise(value)) {
|
||||
return fromPromise(value);
|
||||
}
|
||||
|
||||
return of (value);
|
||||
}
|
84
packages/router/src/utils/tree.ts
Normal file
84
packages/router/src/utils/tree.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export class Tree<T> {
|
||||
/** @internal */
|
||||
_root: TreeNode<T>;
|
||||
|
||||
constructor(root: TreeNode<T>) { this._root = root; }
|
||||
|
||||
get root(): T { return this._root.value; }
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
parent(t: T): T {
|
||||
const p = this.pathFromRoot(t);
|
||||
return p.length > 1 ? p[p.length - 2] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
children(t: T): T[] {
|
||||
const n = findNode(t, this._root);
|
||||
return n ? n.children.map(t => t.value) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
firstChild(t: T): T {
|
||||
const n = findNode(t, this._root);
|
||||
return n && n.children.length > 0 ? n.children[0].value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
siblings(t: T): T[] {
|
||||
const p = findPath(t, this._root, []);
|
||||
if (p.length < 2) return [];
|
||||
|
||||
const c = p[p.length - 2].children.map(c => c.value);
|
||||
return c.filter(cc => cc !== t);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
pathFromRoot(t: T): T[] { return findPath(t, this._root, []).map(s => s.value); }
|
||||
}
|
||||
|
||||
function findNode<T>(expected: T, c: TreeNode<T>): TreeNode<T> {
|
||||
if (expected === c.value) return c;
|
||||
for (const cc of c.children) {
|
||||
const r = findNode(expected, cc);
|
||||
if (r) return r;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findPath<T>(expected: T, c: TreeNode<T>, collected: TreeNode<T>[]): TreeNode<T>[] {
|
||||
collected.push(c);
|
||||
if (expected === c.value) return collected;
|
||||
|
||||
for (const cc of c.children) {
|
||||
const cloned = collected.slice(0);
|
||||
const r = findPath(expected, cc, cloned);
|
||||
if (r.length > 0) return r;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export class TreeNode<T> {
|
||||
constructor(public value: T, public children: TreeNode<T>[]) {}
|
||||
|
||||
toString(): string { return `TreeNode(${this.value})`; }
|
||||
}
|
19
packages/router/src/version.ts
Normal file
19
packages/router/src/version.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the common package.
|
||||
*/
|
||||
|
||||
import {Version} from '@angular/core';
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
export const VERSION = new Version('0.0.0-ROUTERPLACEHOLDER');
|
647
packages/router/test/apply_redirects.spec.ts
Normal file
647
packages/router/test/apply_redirects.spec.ts
Normal file
@ -0,0 +1,647 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {of } from 'rxjs/observable/of';
|
||||
|
||||
import {applyRedirects} from '../src/apply_redirects';
|
||||
import {Routes} from '../src/config';
|
||||
import {LoadedRouterConfig} from '../src/router_config_loader';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree, equalSegments} from '../src/url_tree';
|
||||
|
||||
describe('applyRedirects', () => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
|
||||
it('should return the same url tree when no redirects', () => {
|
||||
checkRedirect(
|
||||
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
|
||||
'/a/b', (t: UrlTree) => { compareTrees(t, tree('/a/b')); });
|
||||
});
|
||||
|
||||
it('should add new segments when needed', () => {
|
||||
checkRedirect(
|
||||
[{path: 'a/b', redirectTo: 'a/b/c'}, {path: '**', component: ComponentC}], '/a/b',
|
||||
(t: UrlTree) => { compareTrees(t, tree('/a/b/c')); });
|
||||
});
|
||||
|
||||
it('should handle positional parameters', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'},
|
||||
{path: '**', component: ComponentC}
|
||||
],
|
||||
'/a/1/b/2', (t: UrlTree) => { compareTrees(t, tree('/newa/1/newb/2')); });
|
||||
});
|
||||
|
||||
it('should throw when cannot handle a positional parameter', () => {
|
||||
applyRedirects(null, null, serializer, tree('/a/1'), [
|
||||
{path: 'a/:id', redirectTo: 'a/:other'}
|
||||
]).subscribe(() => {}, (e) => {
|
||||
expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.');
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass matrix parameters', () => {
|
||||
checkRedirect(
|
||||
[{path: 'a/:id', redirectTo: 'd/a/:id/e'}, {path: '**', component: ComponentC}],
|
||||
'/a;p1=1/1;p2=2', (t: UrlTree) => { compareTrees(t, tree('/d/a;p1=1/1;p2=2/e')); });
|
||||
});
|
||||
|
||||
it('should handle preserve secondary routes', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: 'a/:id', redirectTo: 'd/a/:id/e'},
|
||||
{path: 'c/d', component: ComponentA, outlet: 'aux'}, {path: '**', component: ComponentC}
|
||||
],
|
||||
'/a/1(aux:c/d)', (t: UrlTree) => { compareTrees(t, tree('/d/a/1/e(aux:c/d)')); });
|
||||
});
|
||||
|
||||
it('should redirect secondary routes', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: 'a/:id', component: ComponentA},
|
||||
{path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'},
|
||||
{path: '**', component: ComponentC, outlet: 'aux'}
|
||||
],
|
||||
'/a/1(aux:c/d)', (t: UrlTree) => { 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: UrlTree) => { compareTrees(t, tree('a/b')); });
|
||||
});
|
||||
|
||||
it('should support redirects with both main and aux', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'},
|
||||
|
||||
{path: 'cc', component: ComponentC, outlet: 'aux'},
|
||||
{path: 'b', redirectTo: 'cc', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/(b//aux:b)', (t: UrlTree) => { compareTrees(t, tree('a/(bb//aux:cc)')); });
|
||||
});
|
||||
|
||||
it('should support redirects with both main and aux (with a nested redirect)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'bb', component: ComponentB}, {path: 'b', redirectTo: 'bb'},
|
||||
|
||||
{
|
||||
path: 'cc',
|
||||
component: ComponentC,
|
||||
outlet: 'aux',
|
||||
children: [{path: 'dd', component: ComponentC}, {path: 'd', redirectTo: 'dd'}]
|
||||
},
|
||||
{path: 'b', redirectTo: 'cc/d', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/(b//aux:b)', (t: UrlTree) => { compareTrees(t, tree('a/(bb//aux:cc/dd)')); });
|
||||
});
|
||||
|
||||
it('should redirect wild cards', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: '404', component: ComponentA},
|
||||
{path: '**', redirectTo: '/404'},
|
||||
],
|
||||
'/a/1(aux:c/d)', (t: UrlTree) => { compareTrees(t, tree('/404')); });
|
||||
});
|
||||
|
||||
it('should support absolute redirects', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [{path: 'b/:id', redirectTo: '/absolute/:id?a=1&b=:b#f1'}]
|
||||
},
|
||||
{path: '**', component: ComponentC}
|
||||
],
|
||||
'/a/b/1?b=2', (t: UrlTree) => { compareTrees(t, tree('/absolute/1?a=1&b=2#f1')); });
|
||||
});
|
||||
|
||||
describe('lazy loading', () => {
|
||||
it('should load config on demand', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
const loader = {
|
||||
load: (injector: any, p: any) => {
|
||||
if (injector !== 'providedInjector') throw 'Invalid Injector';
|
||||
return of (loadedConfig);
|
||||
}
|
||||
};
|
||||
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a/b'), config)
|
||||
.forEach(r => {
|
||||
compareTrees(r, tree('/a/b'));
|
||||
expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the case when the loader errors', () => {
|
||||
const loader = {
|
||||
load: (p: any) => new Observable<any>((obs: any) => obs.error(new Error('Loading Error')))
|
||||
};
|
||||
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
|
||||
|
||||
applyRedirects(null, <any>loader, serializer, tree('a/b'), config)
|
||||
.subscribe(() => {}, (e) => { expect(e.message).toEqual('Loading Error'); });
|
||||
});
|
||||
|
||||
it('should load when all canLoad guards return true', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const guard = () => true;
|
||||
const injector = {get: () => guard};
|
||||
|
||||
const config = [{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
canLoad: ['guard1', 'guard2'],
|
||||
loadChildren: 'children'
|
||||
}];
|
||||
|
||||
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config).forEach(r => {
|
||||
compareTrees(r, tree('/a/b'));
|
||||
});
|
||||
});
|
||||
|
||||
it('should not load when any canLoad guards return false', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const trueGuard = () => true;
|
||||
const falseGuard = () => false;
|
||||
const injector = {get: (guardName: any) => guardName === 'guard1' ? trueGuard : falseGuard};
|
||||
|
||||
const config = [{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
canLoad: ['guard1', 'guard2'],
|
||||
loadChildren: 'children'
|
||||
}];
|
||||
|
||||
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
|
||||
.subscribe(
|
||||
() => { throw 'Should not reach'; },
|
||||
(e) => {
|
||||
expect(e.message).toEqual(
|
||||
`NavigationCancelingError: Cannot load children because the guard of the route "path: 'a'" returned false`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not load when any canLoad guards is rejected (promises)', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const trueGuard = () => Promise.resolve(true);
|
||||
const falseGuard = () => Promise.reject('someError');
|
||||
const injector = {get: (guardName: any) => guardName === 'guard1' ? trueGuard : falseGuard};
|
||||
|
||||
const config = [{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
canLoad: ['guard1', 'guard2'],
|
||||
loadChildren: 'children'
|
||||
}];
|
||||
|
||||
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
|
||||
.subscribe(
|
||||
() => { throw 'Should not reach'; }, (e) => { expect(e).toEqual('someError'); });
|
||||
});
|
||||
|
||||
it('should work with objects implementing the CanLoad interface', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const guard = {canLoad: () => Promise.resolve(true)};
|
||||
const injector = {get: () => guard};
|
||||
|
||||
const config =
|
||||
[{path: 'a', component: ComponentA, canLoad: ['guard'], loadChildren: 'children'}];
|
||||
|
||||
applyRedirects(<any>injector, <any>loader, serializer, tree('a/b'), config)
|
||||
.subscribe(
|
||||
(r) => { compareTrees(r, tree('/a/b')); }, (e) => { throw 'Should not reach'; });
|
||||
|
||||
});
|
||||
|
||||
it('should work with absolute redirects', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const config =
|
||||
[{path: '', pathMatch: 'full', redirectTo: '/a'}, {path: 'a', loadChildren: 'children'}];
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree(''), config)
|
||||
.forEach(r => {
|
||||
compareTrees(r, tree('a'));
|
||||
expect((<any>config[1])._loadedConfig).toBe(loadedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
it('should load the configuration only once', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
|
||||
let called = false;
|
||||
const loader = {
|
||||
load: (injector: any, p: any) => {
|
||||
if (called) throw new Error('Should not be called twice');
|
||||
called = true;
|
||||
return of (loadedConfig);
|
||||
}
|
||||
};
|
||||
|
||||
const config = [{path: 'a', loadChildren: 'children'}];
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k1'), config)
|
||||
.subscribe(r => {});
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k2'), config)
|
||||
.subscribe(
|
||||
r => {
|
||||
compareTrees(r, tree('a?k2'));
|
||||
expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
|
||||
},
|
||||
(e) => { throw 'Should not reach'; });
|
||||
});
|
||||
|
||||
it('should load the configuration of a wildcard route', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const config = [{path: '**', loadChildren: 'children'}];
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
|
||||
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
|
||||
});
|
||||
|
||||
it('should load the configuration after a local redirect from a wildcard route', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const config =
|
||||
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: 'not-found'}];
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
|
||||
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
|
||||
});
|
||||
|
||||
it('should load the configuration after an absolute redirect from a wildcard route', () => {
|
||||
const loadedConfig = new LoadedRouterConfig(
|
||||
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
|
||||
<any>'injectorFactory');
|
||||
|
||||
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
|
||||
|
||||
const config =
|
||||
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: '/not-found'}];
|
||||
|
||||
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
|
||||
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty paths', () => {
|
||||
it('redirect from an empty path should work (local redirect)', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
]
|
||||
},
|
||||
{path: '', redirectTo: 'a'}
|
||||
],
|
||||
'b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
|
||||
});
|
||||
|
||||
it('redirect from an empty path should work (absolute redirect)', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
]
|
||||
},
|
||||
{path: '', redirectTo: '/a/b'}
|
||||
],
|
||||
'', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
|
||||
});
|
||||
|
||||
it('should redirect empty path route only when terminal', () => {
|
||||
const config: Routes = [
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
]
|
||||
},
|
||||
{path: '', redirectTo: 'a', pathMatch: 'full'}
|
||||
];
|
||||
|
||||
applyRedirects(null, null, serializer, tree('b'), config)
|
||||
.subscribe(
|
||||
(_) => { throw 'Should not be reached'; },
|
||||
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'b\''); });
|
||||
});
|
||||
|
||||
it('redirect from an empty path should work (nested case)', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'}]
|
||||
},
|
||||
{path: '', redirectTo: 'a'}
|
||||
],
|
||||
'', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
|
||||
});
|
||||
|
||||
it('redirect to an empty path should work', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: '', component: ComponentA, children: [{path: 'b', component: ComponentB}]},
|
||||
{path: 'a', redirectTo: ''}
|
||||
],
|
||||
'a/b', (t: UrlTree) => { compareTrees(t, tree('b')); });
|
||||
});
|
||||
|
||||
describe('aux split is in the middle', () => {
|
||||
it('should create a new url segment (non-terminal)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
{path: '', redirectTo: 'c', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/b', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
|
||||
});
|
||||
|
||||
it('should create a new url segment (terminal)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/b', (t: UrlTree) => { compareTrees(t, tree('a/b')); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('split at the end (no right child)', () => {
|
||||
it('should create a new child (non-terminal)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
{path: '', redirectTo: 'c', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
|
||||
});
|
||||
|
||||
it('should create a new child (terminal)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
|
||||
});
|
||||
|
||||
it('should work only only primary outlet', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB}, {path: '', redirectTo: 'b'},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/(aux:c)', (t: UrlTree) => { compareTrees(t, tree('a/(b//aux:c)')); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('split at the end (right child)', () => {
|
||||
it('should create a new child (non-terminal)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
|
||||
{path: '', redirectTo: 'b'}, {
|
||||
path: 'c',
|
||||
component: ComponentC,
|
||||
outlet: 'aux',
|
||||
children: [{path: 'e', component: ComponentC}]
|
||||
},
|
||||
{path: '', redirectTo: 'c', outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/(d//aux:e)', (t: UrlTree) => { compareTrees(t, tree('a/(b/d//aux:c/e)')); });
|
||||
});
|
||||
|
||||
it('should not create a new child (terminal)', () => {
|
||||
const config: Routes = [{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB, children: [{path: 'd', component: ComponentB}]},
|
||||
{path: '', redirectTo: 'b'}, {
|
||||
path: 'c',
|
||||
component: ComponentC,
|
||||
outlet: 'aux',
|
||||
children: [{path: 'e', component: ComponentC}]
|
||||
},
|
||||
{path: '', pathMatch: 'full', redirectTo: 'c', outlet: 'aux'}
|
||||
]
|
||||
}];
|
||||
|
||||
applyRedirects(null, null, serializer, tree('a/(d//aux:e)'), config)
|
||||
.subscribe(
|
||||
(_) => { throw 'Should not be reached'; },
|
||||
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a\''); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty URL leftovers', () => {
|
||||
it('should not error when no children matching and no url is left', () => {
|
||||
checkRedirect(
|
||||
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
|
||||
'/a', (t: UrlTree) => { compareTrees(t, tree('a')); });
|
||||
});
|
||||
|
||||
it('should not error when no children matching and no url is left (aux routes)', () => {
|
||||
checkRedirect(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', redirectTo: 'c', outlet: 'aux'},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'/a', (t: UrlTree) => { compareTrees(t, tree('a/(aux:c)')); });
|
||||
});
|
||||
|
||||
it('should error when no children matching and some url is left', () => {
|
||||
applyRedirects(
|
||||
null, null, serializer, tree('/a/c'),
|
||||
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}])
|
||||
.subscribe(
|
||||
(_) => { throw 'Should not be reached'; },
|
||||
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a/c\''); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom path matchers', () => {
|
||||
it('should use custom path matcher', () => {
|
||||
const matcher = (s: any, g: any, r: any) => {
|
||||
if (s[0].path === 'a') {
|
||||
return {consumed: s.slice(0, 2), posParams: {id: s[1]}};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
checkRedirect(
|
||||
[{
|
||||
matcher: matcher,
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', component: ComponentB}]
|
||||
}],
|
||||
'/a/1/b', (t: UrlTree) => { compareTrees(t, tree('a/1/b')); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirecting to named outlets', () => {
|
||||
it('should work when using absolute redirects', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: 'a/:id', redirectTo: '/b/:id(aux:c/:id)'},
|
||||
{path: 'b/:id', component: ComponentB},
|
||||
{path: 'c/:id', component: ComponentC, outlet: 'aux'}
|
||||
],
|
||||
'a/1;p=99', (t: UrlTree) => { compareTrees(t, tree('/b/1;p=99(aux:c/1;p=99)')); });
|
||||
});
|
||||
|
||||
it('should work when using absolute redirects (wildcard)', () => {
|
||||
checkRedirect(
|
||||
[
|
||||
{path: '**', redirectTo: '/b(aux:c)'}, {path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'}
|
||||
],
|
||||
'a/1', (t: UrlTree) => { compareTrees(t, tree('/b(aux:c)')); });
|
||||
});
|
||||
|
||||
it('should throw when using non-absolute redirects', () => {
|
||||
applyRedirects(
|
||||
null, null, serializer, tree('a'),
|
||||
[
|
||||
{path: 'a', redirectTo: 'b(aux:c)'},
|
||||
])
|
||||
.subscribe(
|
||||
() => { throw new Error('should not be reached'); },
|
||||
(e) => {
|
||||
expect(e.message).toEqual(
|
||||
'Only absolute redirects can have named outlets. redirectTo: \'b(aux:c)\'');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkRedirect(config: Routes, url: string, callback: any): void {
|
||||
applyRedirects(null, null, new DefaultUrlSerializer(), tree(url), config)
|
||||
.subscribe(callback, e => { throw e; });
|
||||
}
|
||||
|
||||
function tree(url: string): UrlTree {
|
||||
return new DefaultUrlSerializer().parse(url);
|
||||
}
|
||||
|
||||
function compareTrees(actual: UrlTree, expected: UrlTree): void {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
const error =
|
||||
`"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`;
|
||||
compareSegments(actual.root, expected.root, error);
|
||||
expect(actual.queryParams).toEqual(expected.queryParams);
|
||||
expect(actual.fragment).toEqual(expected.fragment);
|
||||
}
|
||||
|
||||
function compareSegments(actual: UrlSegmentGroup, expected: UrlSegmentGroup, error: string): void {
|
||||
expect(actual).toBeDefined(error);
|
||||
expect(equalSegments(actual.segments, expected.segments)).toEqual(true, error);
|
||||
|
||||
expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, error);
|
||||
|
||||
Object.keys(expected.children).forEach(key => {
|
||||
compareSegments(actual.children[key], expected.children[key], error);
|
||||
});
|
||||
}
|
||||
|
||||
class ComponentA {}
|
||||
class ComponentB {}
|
||||
class ComponentC {}
|
145
packages/router/test/config.spec.ts
Normal file
145
packages/router/test/config.spec.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {validateConfig} from '../src/config';
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
|
||||
describe('config', () => {
|
||||
describe('validateConfig', () => {
|
||||
it('should not throw when no errors', () => {
|
||||
expect(
|
||||
() => validateConfig([{path: 'a', redirectTo: 'b'}, {path: 'b', component: ComponentA}]))
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw when a matcher is provided', () => {
|
||||
expect(() => validateConfig([{matcher: <any>'someFunc', component: ComponentA}]))
|
||||
.not.toThrow();
|
||||
});
|
||||
|
||||
it('should throw for undefined route', () => {
|
||||
expect(() => {
|
||||
validateConfig([{path: 'a', component: ComponentA}, , {path: 'b', component: ComponentB}]);
|
||||
}).toThrowError(/Invalid configuration of route ''/);
|
||||
});
|
||||
|
||||
it('should throw for undefined route in children', () => {
|
||||
expect(() => {
|
||||
validateConfig([{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
,
|
||||
]
|
||||
}]);
|
||||
}).toThrowError(/Invalid configuration of route 'a'/);
|
||||
});
|
||||
|
||||
it('should throw when Array is passed', () => {
|
||||
expect(() => {
|
||||
validateConfig([
|
||||
{path: 'a', component: ComponentA},
|
||||
[{path: 'b', component: ComponentB}, {path: 'c', component: ComponentC}]
|
||||
]);
|
||||
}).toThrowError(`Invalid configuration of route '': Array cannot be specified`);
|
||||
});
|
||||
|
||||
it('should throw when redirectTo and children are used together', () => {
|
||||
expect(() => {
|
||||
validateConfig(
|
||||
[{path: 'a', redirectTo: 'b', children: [{path: 'b', component: ComponentA}]}]);
|
||||
})
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a': redirectTo and children cannot be used together`);
|
||||
});
|
||||
|
||||
it('should validate children and report full path', () => {
|
||||
expect(() => validateConfig([{path: 'a', children: [{path: 'b'}]}]))
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a/b'. One of the following must be provided: component, redirectTo, children or loadChildren`);
|
||||
});
|
||||
|
||||
it('should properly report deeply nested path', () => {
|
||||
expect(() => validateConfig([{
|
||||
path: 'a',
|
||||
children: [{path: 'b', children: [{path: 'c', children: [{path: 'd'}]}]}]
|
||||
}]))
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a/b/c/d'. One of the following must be provided: component, redirectTo, children or loadChildren`);
|
||||
});
|
||||
|
||||
it('should throw when redirectTo and loadChildren are used together', () => {
|
||||
expect(() => { validateConfig([{path: 'a', redirectTo: 'b', loadChildren: 'value'}]); })
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a': redirectTo and loadChildren cannot be used together`);
|
||||
});
|
||||
|
||||
it('should throw when children and loadChildren are used together', () => {
|
||||
expect(() => { validateConfig([{path: 'a', children: [], loadChildren: 'value'}]); })
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a': children and loadChildren cannot be used together`);
|
||||
});
|
||||
|
||||
it('should throw when component and redirectTo are used together', () => {
|
||||
expect(() => { validateConfig([{path: 'a', component: ComponentA, redirectTo: 'b'}]); })
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a': redirectTo and component cannot be used together`);
|
||||
});
|
||||
|
||||
it('should throw when path and matcher are used together', () => {
|
||||
expect(() => { validateConfig([{path: 'a', matcher: <any>'someFunc', children: []}]); })
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a': path and matcher cannot be used together`);
|
||||
});
|
||||
|
||||
it('should throw when path and matcher are missing', () => {
|
||||
expect(() => { validateConfig([{component: null, redirectTo: 'b'}]); })
|
||||
.toThrowError(
|
||||
`Invalid configuration of route '': routes must have either a path or a matcher specified`);
|
||||
});
|
||||
|
||||
it('should throw when none of component and children or direct are missing', () => {
|
||||
expect(() => { validateConfig([{path: 'a'}]); })
|
||||
.toThrowError(
|
||||
`Invalid configuration of route 'a'. One of the following must be provided: component, redirectTo, children or loadChildren`);
|
||||
});
|
||||
|
||||
it('should throw when path starts with a slash', () => {
|
||||
expect(() => {
|
||||
validateConfig([<any>{path: '/a', redirectTo: 'b'}]);
|
||||
}).toThrowError(`Invalid configuration of route '/a': path cannot start with a slash`);
|
||||
});
|
||||
|
||||
it('should throw when emptyPath is used with redirectTo without explicitly providing matching',
|
||||
() => {
|
||||
expect(() => {
|
||||
validateConfig([<any>{path: '', redirectTo: 'b'}]);
|
||||
}).toThrowError(/Invalid configuration of route '{path: "", redirectTo: "b"}'/);
|
||||
});
|
||||
|
||||
it('should throw when pathPatch is invalid', () => {
|
||||
expect(() => { validateConfig([{path: 'a', pathMatch: 'invalid', component: ComponentB}]); })
|
||||
.toThrowError(
|
||||
/Invalid configuration of route 'a': pathMatch can only be set to 'prefix' or 'full'/);
|
||||
});
|
||||
|
||||
it('should throw when pathPatch is invalid', () => {
|
||||
expect(() => { validateConfig([{path: 'a', outlet: 'aux', children: []}]); })
|
||||
.toThrowError(
|
||||
/Invalid configuration of route 'a': a componentless route cannot have a named outlet set/);
|
||||
|
||||
expect(() => validateConfig([{path: 'a', outlet: '', children: []}])).not.toThrow();
|
||||
expect(() => validateConfig([{path: 'a', outlet: PRIMARY_OUTLET, children: []}]))
|
||||
.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class ComponentA {}
|
||||
class ComponentB {}
|
||||
class ComponentC {}
|
124
packages/router/test/create_router_state.spec.ts
Normal file
124
packages/router/test/create_router_state.spec.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Routes} from '../src/config';
|
||||
import {createRouterState} from '../src/create_router_state';
|
||||
import {recognize} from '../src/recognize';
|
||||
import {DefaultRouteReuseStrategy} from '../src/router';
|
||||
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
|
||||
import {TreeNode} from '../src/utils/tree';
|
||||
|
||||
describe('create router state', () => {
|
||||
const reuseStrategy = new DefaultRouteReuseStrategy();
|
||||
|
||||
const emptyState = () =>
|
||||
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
|
||||
|
||||
it('should work create new state', () => {
|
||||
const state = createRouterState(
|
||||
reuseStrategy, createState(
|
||||
[
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)'),
|
||||
emptyState());
|
||||
|
||||
checkActivatedRoute(state.root, RootComponent);
|
||||
|
||||
const c = state.children(state.root);
|
||||
checkActivatedRoute(c[0], ComponentA);
|
||||
checkActivatedRoute(c[1], ComponentB, 'left');
|
||||
checkActivatedRoute(c[2], ComponentC, 'right');
|
||||
});
|
||||
|
||||
it('should reuse existing nodes when it can', () => {
|
||||
const config = [
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||
];
|
||||
|
||||
const prevState =
|
||||
createRouterState(reuseStrategy, createState(config, 'a(left:b)'), emptyState());
|
||||
advanceState(prevState);
|
||||
const state = createRouterState(reuseStrategy, createState(config, 'a(left:c)'), prevState);
|
||||
|
||||
expect(prevState.root).toBe(state.root);
|
||||
const prevC = prevState.children(prevState.root);
|
||||
const currC = state.children(state.root);
|
||||
|
||||
expect(prevC[0]).toBe(currC[0]);
|
||||
expect(prevC[1]).not.toBe(currC[1]);
|
||||
checkActivatedRoute(currC[1], ComponentC, 'left');
|
||||
});
|
||||
|
||||
it('should handle componentless routes', () => {
|
||||
const config = [{
|
||||
path: 'a/:id',
|
||||
children: [
|
||||
{path: 'b', component: ComponentA}, {path: 'c', component: ComponentB, outlet: 'right'}
|
||||
]
|
||||
}];
|
||||
|
||||
|
||||
const prevState = createRouterState(
|
||||
reuseStrategy, createState(config, 'a/1;p=11/(b//right:c)'), emptyState());
|
||||
advanceState(prevState);
|
||||
const state =
|
||||
createRouterState(reuseStrategy, createState(config, 'a/2;p=22/(b//right:c)'), prevState);
|
||||
|
||||
expect(prevState.root).toBe(state.root);
|
||||
const prevP = prevState.firstChild(prevState.root);
|
||||
const currP = state.firstChild(state.root);
|
||||
expect(prevP).toBe(currP);
|
||||
|
||||
const prevC = prevState.children(prevP);
|
||||
const currC = state.children(currP);
|
||||
|
||||
expect(currP._futureSnapshot.params).toEqual({id: '2', p: '22'});
|
||||
checkActivatedRoute(currC[0], ComponentA);
|
||||
checkActivatedRoute(currC[1], ComponentB, 'right');
|
||||
});
|
||||
});
|
||||
|
||||
function advanceState(state: RouterState): void {
|
||||
advanceNode(state._root);
|
||||
}
|
||||
|
||||
function advanceNode(node: TreeNode<ActivatedRoute>): void {
|
||||
advanceActivatedRoute(node.value);
|
||||
node.children.forEach(advanceNode);
|
||||
}
|
||||
|
||||
function createState(config: Routes, url: string): RouterStateSnapshot {
|
||||
let res: RouterStateSnapshot;
|
||||
recognize(RootComponent, config, tree(url), url).forEach(s => res = s);
|
||||
return res;
|
||||
}
|
||||
|
||||
function checkActivatedRoute(
|
||||
actual: ActivatedRoute, cmp: Function, outlet: string = PRIMARY_OUTLET): void {
|
||||
if (actual === null) {
|
||||
expect(actual).toBeDefined();
|
||||
} else {
|
||||
expect(actual.component).toBe(cmp);
|
||||
expect(actual.outlet).toEqual(outlet);
|
||||
}
|
||||
}
|
||||
|
||||
function tree(url: string): UrlTree {
|
||||
return new DefaultUrlSerializer().parse(url);
|
||||
}
|
||||
|
||||
class RootComponent {}
|
||||
class ComponentA {}
|
||||
class ComponentB {}
|
||||
class ComponentC {}
|
256
packages/router/test/create_url_tree.spec.ts
Normal file
256
packages/router/test/create_url_tree.spec.ts
Normal file
@ -0,0 +1,256 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||
|
||||
import {createUrlTree} from '../src/create_url_tree';
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
|
||||
import {PRIMARY_OUTLET, Params} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
|
||||
|
||||
describe('createUrlTree', () => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
|
||||
it('should navigate to the root', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, ['/']);
|
||||
expect(serializer.serialize(t)).toEqual('/');
|
||||
});
|
||||
|
||||
it('should error when navigating to the root segment with params', () => {
|
||||
const p = serializer.parse('/');
|
||||
expect(() => createRoot(p, ['/', {p: 11}]))
|
||||
.toThrowError(/Root segment cannot have matrix parameters/);
|
||||
});
|
||||
|
||||
it('should support nested segments', () => {
|
||||
const p = serializer.parse('/a/b');
|
||||
const t = createRoot(p, ['/one', 11, 'two', 22]);
|
||||
expect(serializer.serialize(t)).toEqual('/one/11/two/22');
|
||||
});
|
||||
|
||||
it('should stringify positional parameters', () => {
|
||||
const p = serializer.parse('/a/b');
|
||||
const t = createRoot(p, ['/one', 11]);
|
||||
const params = t.root.children[PRIMARY_OUTLET].segments;
|
||||
expect(params[0].path).toEqual('one');
|
||||
expect(params[1].path).toEqual('11');
|
||||
});
|
||||
|
||||
it('should support first segments contaings slashes', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, [{segmentPath: '/one'}, 'two/three']);
|
||||
expect(serializer.serialize(t)).toEqual('/%2Fone/two%2Fthree');
|
||||
});
|
||||
|
||||
it('should preserve secondary segments', () => {
|
||||
const p = serializer.parse('/a/11/b(right:c)');
|
||||
const t = createRoot(p, ['/a', 11, 'd']);
|
||||
expect(serializer.serialize(t)).toEqual('/a/11/d(right:c)');
|
||||
});
|
||||
|
||||
it('should support updating secondary segments (absolute)', () => {
|
||||
const p = serializer.parse('/a(right:b)');
|
||||
const t = createRoot(p, ['/', {outlets: {right: ['c']}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a(right:c)');
|
||||
});
|
||||
|
||||
it('should support updating secondary segments', () => {
|
||||
const p = serializer.parse('/a(right:b)');
|
||||
const t = createRoot(p, [{outlets: {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 = createRoot(p, ['a', {outlets: {right: ['d', 11, 'e']}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a/(b//right:d/11/e)');
|
||||
});
|
||||
|
||||
it('should throw when outlets is not the last command', () => {
|
||||
const p = serializer.parse('/a');
|
||||
expect(() => createRoot(p, ['a', {outlets: {right: ['c']}}, 'c']))
|
||||
.toThrowError('{outlets:{}} has to be the last command');
|
||||
});
|
||||
|
||||
it('should support updating using a string', () => {
|
||||
const p = serializer.parse('/a(right:b)');
|
||||
const t = createRoot(p, [{outlets: {right: 'c/11/d'}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a(right:c/11/d)');
|
||||
});
|
||||
|
||||
it('should support updating primary and secondary segments at once', () => {
|
||||
const p = serializer.parse('/a(right:b)');
|
||||
const t = createRoot(p, [{outlets: {primary: 'y/z', right: 'c/11/d'}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/y/z(right:c/11/d)');
|
||||
});
|
||||
|
||||
it('should support removing primary segment', () => {
|
||||
const p = serializer.parse('/a/(b//right:c)');
|
||||
const t = createRoot(p, ['a', {outlets: {primary: null, right: 'd'}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a/(right:d)');
|
||||
});
|
||||
|
||||
it('should support removing secondary segments', () => {
|
||||
const p = serializer.parse('/a(right:b)');
|
||||
const t = createRoot(p, [{outlets: {right: null}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a');
|
||||
});
|
||||
|
||||
it('should update matrix parameters', () => {
|
||||
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 = 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 = createRoot(p, ['/a', 'b', {aa: 22, bb: 33}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a/b;aa=22;bb=33');
|
||||
});
|
||||
|
||||
describe('relative navigation', () => {
|
||||
it('should work', () => {
|
||||
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/(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/(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 support parameters-only navigation', () => {
|
||||
const p = serializer.parse('/a');
|
||||
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, [{k: 99}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a;k=99');
|
||||
});
|
||||
|
||||
it('should support parameters-only navigation (nested case)', () => {
|
||||
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;x=99(left:ap)');
|
||||
});
|
||||
|
||||
it('should support parameters-only navigation (with a double dot)', () => {
|
||||
const p = serializer.parse('/a/(c//left:cp)(left:ap)');
|
||||
const t =
|
||||
create(p.root.children[PRIMARY_OUTLET].children[PRIMARY_OUTLET], 0, p, ['../', {x: 5}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a;x=5(left:ap)');
|
||||
});
|
||||
|
||||
it('should work when index > 0', () => {
|
||||
const p = serializer.parse('/a/c');
|
||||
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 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)');
|
||||
});
|
||||
|
||||
it('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 work with ../ when absolute url', () => {
|
||||
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 with position = -1', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = create(p.root, -1, p, ['11']);
|
||||
expect(serializer.serialize(t)).toEqual('/11');
|
||||
});
|
||||
|
||||
it('should throw when too many ..', () => {
|
||||
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 support updating secondary segments', () => {
|
||||
const p = serializer.parse('/a/b');
|
||||
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, [{outlets: {right: ['c']}}]);
|
||||
expect(serializer.serialize(t)).toEqual('/a/b/(right:c)');
|
||||
});
|
||||
});
|
||||
|
||||
it('should set query params', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, [], {a: 'hey'});
|
||||
expect(t.queryParams).toEqual({a: 'hey'});
|
||||
});
|
||||
|
||||
it('should stringify query params', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, [], <any>{a: 1});
|
||||
expect(t.queryParams).toEqual({a: '1'});
|
||||
});
|
||||
|
||||
it('should set fragment', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, [], {}, 'fragment');
|
||||
expect(t.fragment).toEqual('fragment');
|
||||
});
|
||||
});
|
||||
|
||||
function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
|
||||
const s = new ActivatedRouteSnapshot(
|
||||
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1,
|
||||
<any>null);
|
||||
const a = new ActivatedRoute(
|
||||
new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null),
|
||||
new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s);
|
||||
advanceActivatedRoute(a);
|
||||
return createUrlTree(a, tree, commands, queryParams, fragment);
|
||||
}
|
||||
|
||||
function create(
|
||||
segment: UrlSegmentGroup, startIndex: number, tree: UrlTree, commands: any[],
|
||||
queryParams?: Params, fragment?: string) {
|
||||
if (!segment) {
|
||||
expect(segment).toBeDefined();
|
||||
}
|
||||
const s = new ActivatedRouteSnapshot(
|
||||
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, <any>segment,
|
||||
startIndex, <any>null);
|
||||
const a = new ActivatedRoute(
|
||||
new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null),
|
||||
new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s);
|
||||
advanceActivatedRoute(a);
|
||||
return createUrlTree(a, tree, commands, queryParams, fragment);
|
||||
}
|
3316
packages/router/test/integration.spec.ts
Normal file
3316
packages/router/test/integration.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
730
packages/router/test/recognize.spec.ts
Normal file
730
packages/router/test/recognize.spec.ts
Normal file
@ -0,0 +1,730 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Routes} from '../src/config';
|
||||
import {recognize} from '../src/recognize';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from '../src/router_state';
|
||||
import {PRIMARY_OUTLET, Params} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlTree} from '../src/url_tree';
|
||||
|
||||
describe('recognize', () => {
|
||||
it('should work', () => {
|
||||
checkRecognize([{path: 'a', component: ComponentA}], 'a', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
});
|
||||
});
|
||||
|
||||
it('should freeze params object', () => {
|
||||
checkRecognize([{path: 'a/:id', component: ComponentA}], 'a/10', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
const child = s.firstChild(s.root);
|
||||
expect(Object.isFrozen(child.params)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support secondary routes', () => {
|
||||
checkRecognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'c', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(left:b//right:c)', (s: RouterStateSnapshot) => {
|
||||
const c = s.children(s.root);
|
||||
checkActivatedRoute(c[0], 'a', {}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {}, ComponentB, 'left');
|
||||
checkActivatedRoute(c[2], 'c', {}, ComponentC, 'right');
|
||||
});
|
||||
});
|
||||
|
||||
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: RouterStateSnapshot) => {
|
||||
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(<any>compA);
|
||||
expect(compC._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(compC._lastPathIndex).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set url segment and index properly (wildcard)', () => {
|
||||
const url = tree('a/b/c');
|
||||
recognize(
|
||||
RootComponent,
|
||||
[
|
||||
{path: 'a', component: ComponentA, children: [{path: '**', component: ComponentB}]},
|
||||
],
|
||||
url, 'a/b/c')
|
||||
.subscribe((s: RouterStateSnapshot) => {
|
||||
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(0);
|
||||
|
||||
const compC = s.firstChild(<any>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}
|
||||
],
|
||||
'a/paramA', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
checkActivatedRoute(
|
||||
s.firstChild(<any>s.firstChild(s.root)), 'paramA', {id: 'paramA'}, ComponentB);
|
||||
});
|
||||
|
||||
checkRecognize(
|
||||
[{path: 'a', component: ComponentA}, {path: 'a/:id', component: ComponentC}], 'a/paramA',
|
||||
(s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a/paramA', {id: 'paramA'}, ComponentC);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use outlet name when matching secondary routes', () => {
|
||||
checkRecognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'left'},
|
||||
{path: 'b', component: ComponentC, outlet: 'right'}
|
||||
],
|
||||
'a(right:b)', (s: RouterStateSnapshot) => {
|
||||
const c = s.children(s.root);
|
||||
checkActivatedRoute(c[0], 'a', {}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {}, ComponentC, 'right');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non top-level secondary routes', () => {
|
||||
checkRecognize(
|
||||
[
|
||||
{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||
]
|
||||
},
|
||||
],
|
||||
'a/(b//left:c)', (s: RouterStateSnapshot) => {
|
||||
const c = s.children(<any>s.firstChild(s.root));
|
||||
checkActivatedRoute(c[0], 'b', {}, ComponentB, PRIMARY_OUTLET);
|
||||
checkActivatedRoute(c[1], 'c', {}, ComponentC, 'left');
|
||||
});
|
||||
});
|
||||
|
||||
it('should sort routes by outlet name', () => {
|
||||
checkRecognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'c', component: ComponentC, outlet: 'c'},
|
||||
{path: 'b', component: ComponentB, outlet: 'b'}
|
||||
],
|
||||
'a(c:c//b:b)', (s: RouterStateSnapshot) => {
|
||||
const c = s.children(s.root);
|
||||
checkActivatedRoute(c[0], 'a', {}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {}, ComponentB, 'b');
|
||||
checkActivatedRoute(c[2], 'c', {}, ComponentC, 'c');
|
||||
});
|
||||
});
|
||||
|
||||
it('should support matrix parameters', () => {
|
||||
checkRecognize(
|
||||
[
|
||||
{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]},
|
||||
{path: 'c', component: ComponentC, outlet: 'left'}
|
||||
],
|
||||
'a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)', (s: RouterStateSnapshot) => {
|
||||
const c = s.children(s.root);
|
||||
checkActivatedRoute(c[0], 'a', {a1: '11', a2: '22'}, ComponentA);
|
||||
checkActivatedRoute(s.firstChild(<any>c[0]), 'b', {b1: '111', b2: '222'}, ComponentB);
|
||||
checkActivatedRoute(c[1], 'c', {c1: '1111', c2: '2222'}, ComponentC, 'left');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data', () => {
|
||||
it('should set static data', () => {
|
||||
checkRecognize(
|
||||
[{path: 'a', data: {one: 1}, component: ComponentA}], 'a', (s: RouterStateSnapshot) => {
|
||||
const r: ActivatedRouteSnapshot = s.firstChild(s.root);
|
||||
expect(r.data).toEqual({one: 1});
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge componentless route\'s data', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
data: {one: 1},
|
||||
children: [{path: 'b', data: {two: 2}, component: ComponentB}]
|
||||
}],
|
||||
'a/b', (s: RouterStateSnapshot) => {
|
||||
const r: ActivatedRouteSnapshot = s.firstChild(<any>s.firstChild(s.root));
|
||||
expect(r.data).toEqual({one: 1, two: 2});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set resolved data', () => {
|
||||
checkRecognize(
|
||||
[{path: 'a', resolve: {one: 'some-token'}, component: ComponentA}], 'a',
|
||||
(s: RouterStateSnapshot) => {
|
||||
const r: ActivatedRouteSnapshot = s.firstChild(s.root);
|
||||
expect(r._resolve).toEqual({one: 'some-token'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty path', () => {
|
||||
describe('root', () => {
|
||||
it('should work', () => {
|
||||
checkRecognize([{path: '', component: ComponentA}], '', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
|
||||
});
|
||||
});
|
||||
|
||||
it('should match when terminal', () => {
|
||||
checkRecognize(
|
||||
[{path: '', pathMatch: 'full', component: ComponentA}], '',
|
||||
(s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work (nested case)', () => {
|
||||
checkRecognize(
|
||||
[{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}], '',
|
||||
(s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), '', {}, ComponentA);
|
||||
checkActivatedRoute(s.firstChild(<any>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: RouterStateSnapshot) => {
|
||||
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(<any>s.firstChild(s.root));
|
||||
expect(c2._urlSegment).toBe(url.root);
|
||||
expect(c2._lastPathIndex).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should inherit params', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}
|
||||
]
|
||||
}],
|
||||
'/a;p=1', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {p: '1'}, ComponentA);
|
||||
checkActivatedRoute(s.firstChild(s.firstChild(s.root)), '', {p: '1'}, ComponentB);
|
||||
checkActivatedRoute(
|
||||
s.firstChild(s.firstChild(s.firstChild(s.root))), '', {p: '1'}, ComponentC);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aux split is in the middle', () => {
|
||||
it('should match (non-terminal)', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/b', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
|
||||
const c = s.children(s.firstChild(s.root));
|
||||
checkActivatedRoute(c[0], 'b', {}, ComponentB);
|
||||
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
|
||||
});
|
||||
});
|
||||
|
||||
it('should match (non-termianl) when both primary and secondary and primary has a child',
|
||||
() => {
|
||||
const config = [{
|
||||
path: 'parent',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: ComponentD,
|
||||
outlet: 'secondary',
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
||||
checkRecognize(config, 'parent/b', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
checkActivatedRoute(s.firstChild(s.root), 'parent', {}, undefined);
|
||||
|
||||
const cc = s.children(s.firstChild(s.root));
|
||||
checkActivatedRoute(cc[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(cc[1], '', {}, ComponentD, 'secondary');
|
||||
|
||||
checkActivatedRoute(s.firstChild(cc[0]), 'b', {}, ComponentB);
|
||||
});
|
||||
});
|
||||
|
||||
it('should match (terminal)', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'a/b', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
|
||||
const c = s.children(s.firstChild(s.root));
|
||||
expect(c.length).toEqual(1);
|
||||
checkActivatedRoute(c[0], 'b', {}, ComponentB);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set url segment and index properly', () => {
|
||||
const url = tree('a/b');
|
||||
recognize(
|
||||
RootComponent, [{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
url, 'a/b')
|
||||
.forEach((s: RouterStateSnapshot) => {
|
||||
expect(s.root._urlSegment).toBe(url.root);
|
||||
expect(s.root._lastPathIndex).toBe(-1);
|
||||
|
||||
const a = s.firstChild(s.root);
|
||||
expect(a._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(a._lastPathIndex).toBe(0);
|
||||
|
||||
const b = s.firstChild(a);
|
||||
expect(b._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(b._lastPathIndex).toBe(1);
|
||||
|
||||
const c = s.children(a)[1];
|
||||
expect(c._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(c._lastPathIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set url segment and index properly when nested empty-path segments', () => {
|
||||
const url = tree('a');
|
||||
recognize(
|
||||
RootComponent, [{
|
||||
path: 'a',
|
||||
children: [
|
||||
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}
|
||||
]
|
||||
}],
|
||||
url, 'a')
|
||||
.forEach((s: RouterStateSnapshot) => {
|
||||
expect(s.root._urlSegment).toBe(url.root);
|
||||
expect(s.root._lastPathIndex).toBe(-1);
|
||||
|
||||
const a = s.firstChild(s.root);
|
||||
expect(a._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(a._lastPathIndex).toBe(0);
|
||||
|
||||
const b = s.firstChild(a);
|
||||
expect(b._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(b._lastPathIndex).toBe(0);
|
||||
|
||||
const c = s.firstChild(b);
|
||||
expect(c._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||
expect(c._lastPathIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set url segment and index properly when nested empty-path segments (2)', () => {
|
||||
const url = tree('');
|
||||
recognize(
|
||||
RootComponent, [{
|
||||
path: '',
|
||||
children: [
|
||||
{path: '', component: ComponentB, children: [{path: '', component: ComponentC}]}
|
||||
]
|
||||
}],
|
||||
url, '')
|
||||
.forEach((s: RouterStateSnapshot) => {
|
||||
expect(s.root._urlSegment).toBe(url.root);
|
||||
expect(s.root._lastPathIndex).toBe(-1);
|
||||
|
||||
const a = s.firstChild(s.root);
|
||||
expect(a._urlSegment).toBe(url.root);
|
||||
expect(a._lastPathIndex).toBe(-1);
|
||||
|
||||
const b = s.firstChild(a);
|
||||
expect(b._urlSegment).toBe(url.root);
|
||||
expect(b._lastPathIndex).toBe(-1);
|
||||
|
||||
const c = s.firstChild(b);
|
||||
expect(c._urlSegment).toBe(url.root);
|
||||
expect(c._lastPathIndex).toBe(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aux split at the end (no right child)', () => {
|
||||
it('should match (non-terminal)', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'a', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
|
||||
const c = s.children(s.firstChild(s.root));
|
||||
checkActivatedRoute(c[0], '', {}, ComponentB);
|
||||
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
|
||||
});
|
||||
});
|
||||
|
||||
it('should match (terminal)', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', pathMatch: 'full', component: ComponentB},
|
||||
{path: '', pathMatch: 'full', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'a', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
|
||||
const c = s.children(s.firstChild(s.root));
|
||||
checkActivatedRoute(c[0], '', {}, ComponentB);
|
||||
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
|
||||
});
|
||||
});
|
||||
|
||||
it('should work only only primary outlet', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'a/(aux:c)', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
|
||||
const c = s.children(s.firstChild(s.root));
|
||||
checkActivatedRoute(c[0], '', {}, ComponentB);
|
||||
checkActivatedRoute(c[1], 'c', {}, ComponentC, 'aux');
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when split is at the root level', () => {
|
||||
checkRecognize(
|
||||
[
|
||||
{path: '', component: ComponentA}, {path: 'b', component: ComponentB},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'}
|
||||
],
|
||||
'(aux:c)', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.root, '', {}, RootComponent);
|
||||
|
||||
const children = s.children(s.root);
|
||||
expect(children.length).toEqual(2);
|
||||
checkActivatedRoute(children[0], '', {}, ComponentA);
|
||||
checkActivatedRoute(children[1], 'c', {}, ComponentC, 'aux');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('split at the end (right child)', () => {
|
||||
it('should match (non-terminal)', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: '', component: ComponentB, children: [{path: 'd', component: ComponentD}]},
|
||||
{
|
||||
path: '',
|
||||
component: ComponentC,
|
||||
outlet: 'aux',
|
||||
children: [{path: 'e', component: ComponentE}]
|
||||
},
|
||||
]
|
||||
}],
|
||||
'a/(d//aux:e)', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a', {}, ComponentA);
|
||||
|
||||
const c = s.children(s.firstChild(s.root));
|
||||
checkActivatedRoute(c[0], '', {}, ComponentB);
|
||||
checkActivatedRoute(s.firstChild(c[0]), 'd', {}, ComponentD);
|
||||
checkActivatedRoute(c[1], '', {}, ComponentC, 'aux');
|
||||
checkActivatedRoute(s.firstChild(c[1]), 'e', {}, ComponentE);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('wildcards', () => {
|
||||
it('should support simple wildcards', () => {
|
||||
checkRecognize(
|
||||
[{path: '**', component: ComponentA}], 'a/b/c/d;a1=11', (s: RouterStateSnapshot) => {
|
||||
checkActivatedRoute(s.firstChild(s.root), 'a/b/c/d', {a1: '11'}, ComponentA);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentless routes', () => {
|
||||
it('should work', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'p/:id',
|
||||
children: [
|
||||
{path: 'a', component: ComponentA},
|
||||
{path: 'b', component: ComponentB, outlet: 'aux'}
|
||||
]
|
||||
}],
|
||||
'p/11;pp=22/(a;pa=33//aux:b;pb=44)', (s: RouterStateSnapshot) => {
|
||||
const p = s.firstChild(s.root);
|
||||
checkActivatedRoute(p, 'p/11', {id: '11', pp: '22'}, undefined);
|
||||
|
||||
const c = s.children(p);
|
||||
checkActivatedRoute(c[0], 'a', {id: '11', pp: '22', pa: '33'}, ComponentA);
|
||||
checkActivatedRoute(c[1], 'b', {id: '11', pp: '22', pb: '44'}, ComponentB, 'aux');
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge params until encounters a normal route', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'p/:id',
|
||||
children: [{
|
||||
path: 'a/:name',
|
||||
children: [{
|
||||
path: 'b',
|
||||
component: ComponentB,
|
||||
children: [{path: 'c', component: ComponentC}]
|
||||
}]
|
||||
}]
|
||||
}],
|
||||
'p/11/a/victor/b/c', (s: RouterStateSnapshot) => {
|
||||
const p = s.firstChild(s.root);
|
||||
checkActivatedRoute(p, 'p/11', {id: '11'}, undefined);
|
||||
|
||||
const a = s.firstChild(p);
|
||||
checkActivatedRoute(a, 'a/victor', {id: '11', name: 'victor'}, undefined);
|
||||
|
||||
const b = s.firstChild(a);
|
||||
checkActivatedRoute(b, 'b', {id: '11', name: 'victor'}, ComponentB);
|
||||
|
||||
const c = s.firstChild(b);
|
||||
checkActivatedRoute(c, 'c', {}, ComponentC);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty URL leftovers', () => {
|
||||
it('should not throw when no children matching', () => {
|
||||
checkRecognize(
|
||||
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
|
||||
'/a', (s: RouterStateSnapshot) => {
|
||||
const a = s.firstChild(s.root);
|
||||
checkActivatedRoute(a, 'a', {}, ComponentA);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw when no children matching (aux routes)', () => {
|
||||
checkRecognize(
|
||||
[{
|
||||
path: 'a',
|
||||
component: ComponentA,
|
||||
children: [
|
||||
{path: 'b', component: ComponentB},
|
||||
{path: '', component: ComponentC, outlet: 'aux'},
|
||||
]
|
||||
}],
|
||||
'/a', (s: RouterStateSnapshot) => {
|
||||
const a = s.firstChild(s.root);
|
||||
checkActivatedRoute(a, 'a', {}, ComponentA);
|
||||
checkActivatedRoute(a.children[0], '', {}, ComponentC, 'aux');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom path matchers', () => {
|
||||
it('should use custom path matcher', () => {
|
||||
const matcher = (s: any, g: any, r: any) => {
|
||||
if (s[0].path === 'a') {
|
||||
return {consumed: s.slice(0, 2), posParams: {id: s[1]}};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
checkRecognize(
|
||||
[{
|
||||
matcher: matcher,
|
||||
component: ComponentA,
|
||||
children: [{path: 'b', component: ComponentB}]
|
||||
}],
|
||||
'/a/1;p=99/b', (s: RouterStateSnapshot) => {
|
||||
const a = s.root.firstChild;
|
||||
checkActivatedRoute(a, 'a/1', {id: '1', p: '99'}, ComponentA);
|
||||
checkActivatedRoute(a.firstChild, 'b', {}, ComponentB);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('query parameters', () => {
|
||||
it('should support query params', () => {
|
||||
const config = [{path: 'a', component: ComponentA}];
|
||||
checkRecognize(config, 'a?q=11', (s: RouterStateSnapshot) => {
|
||||
expect(s.root.queryParams).toEqual({q: '11'});
|
||||
});
|
||||
});
|
||||
|
||||
it('should freeze query params object', () => {
|
||||
checkRecognize([{path: 'a', component: ComponentA}], 'a?q=11', (s: RouterStateSnapshot) => {
|
||||
expect(Object.isFrozen(s.root.queryParams)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fragment', () => {
|
||||
it('should support fragment', () => {
|
||||
const config = [{path: 'a', component: ComponentA}];
|
||||
checkRecognize(
|
||||
config, 'a#f1', (s: RouterStateSnapshot) => { expect(s.root.fragment).toEqual('f1'); });
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should error when two routes with the same outlet name got matched', () => {
|
||||
recognize(
|
||||
RootComponent,
|
||||
[
|
||||
{path: 'a', component: ComponentA}, {path: 'b', component: ComponentB, outlet: 'aux'},
|
||||
{path: 'c', component: ComponentC, outlet: 'aux'}
|
||||
],
|
||||
tree('a(aux:b//aux:c)'), 'a(aux:b//aux:c)')
|
||||
.subscribe((_) => {}, (s: RouterStateSnapshot) => {
|
||||
expect(s.toString())
|
||||
.toContain(
|
||||
'Two segments cannot have the same outlet name: \'aux:b\' and \'aux:c\'.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkRecognize(config: Routes, url: string, callback: any): void {
|
||||
recognize(RootComponent, config, tree(url), url).subscribe(callback, e => { throw e; });
|
||||
}
|
||||
|
||||
function checkActivatedRoute(
|
||||
actual: ActivatedRouteSnapshot, url: string, params: Params, cmp: Function,
|
||||
outlet: string = PRIMARY_OUTLET): void {
|
||||
if (actual === null) {
|
||||
expect(actual).not.toBeNull();
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function tree(url: string): UrlTree {
|
||||
return new DefaultUrlSerializer().parse(url);
|
||||
}
|
||||
|
||||
class RootComponent {}
|
||||
class ComponentA {}
|
||||
class ComponentB {}
|
||||
class ComponentC {}
|
||||
class ComponentD {}
|
||||
class ComponentE {}
|
120
packages/router/test/router.spec.ts
Normal file
120
packages/router/test/router.spec.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Location} from '@angular/common';
|
||||
import {TestBed, inject} from '@angular/core/testing';
|
||||
|
||||
import {ResolveData} from '../src/config';
|
||||
import {PreActivation, Router} from '../src/router';
|
||||
import {RouterOutletMap} from '../src/router_outlet_map';
|
||||
import {ActivatedRouteSnapshot, RouterStateSnapshot, createEmptyStateSnapshot} from '../src/router_state';
|
||||
import {DefaultUrlSerializer} from '../src/url_tree';
|
||||
import {TreeNode} from '../src/utils/tree';
|
||||
import {RouterTestingModule} from '../testing/router_testing_module';
|
||||
|
||||
describe('Router', () => {
|
||||
describe('resetRootComponentType', () => {
|
||||
class NewRootComponent {}
|
||||
|
||||
beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); });
|
||||
|
||||
it('should not change root route when updating the root component', () => {
|
||||
const r: Router = TestBed.get(Router);
|
||||
const root = r.routerState.root;
|
||||
|
||||
r.resetRootComponentType(NewRootComponent);
|
||||
|
||||
expect(r.routerState.root).toBe(root);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setUpLocationChangeListener', () => {
|
||||
beforeEach(() => { TestBed.configureTestingModule({imports: [RouterTestingModule]}); });
|
||||
|
||||
it('should be indempotent', inject([Router, Location], (r: Router, location: Location) => {
|
||||
r.setUpLocationChangeListener();
|
||||
const a = (<any>r).locationSubscription;
|
||||
r.setUpLocationChangeListener();
|
||||
const b = (<any>r).locationSubscription;
|
||||
|
||||
expect(a).toBe(b);
|
||||
|
||||
r.dispose();
|
||||
r.setUpLocationChangeListener();
|
||||
const c = (<any>r).locationSubscription;
|
||||
|
||||
expect(c).not.toBe(b);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('PreActivation', () => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
const inj = {get: (token: any) => () => `${token}_value`};
|
||||
let empty: RouterStateSnapshot;
|
||||
|
||||
beforeEach(() => { empty = createEmptyStateSnapshot(serializer.parse('/'), null); });
|
||||
|
||||
it('should resolve data', () => {
|
||||
const r = {data: 'resolver'};
|
||||
const n = createActivatedRouteSnapshot('a', {resolve: r});
|
||||
const s = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n, [])]));
|
||||
|
||||
checkResolveData(s, empty, inj, () => {
|
||||
expect(s.root.firstChild.data).toEqual({data: 'resolver_value'});
|
||||
});
|
||||
});
|
||||
|
||||
it('should wait for the parent resolve to complete', () => {
|
||||
const parentResolve = {data: 'resolver'};
|
||||
const childResolve = {};
|
||||
|
||||
const parent = createActivatedRouteSnapshot(null, {resolve: parentResolve});
|
||||
const child = createActivatedRouteSnapshot('b', {resolve: childResolve});
|
||||
|
||||
const s = new RouterStateSnapshot(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(parent, [new TreeNode(child, [])])]));
|
||||
|
||||
const inj = {get: (token: any) => () => Promise.resolve(`${token}_value`)};
|
||||
|
||||
checkResolveData(s, empty, inj, () => {
|
||||
expect(s.root.firstChild.firstChild.data).toEqual({data: 'resolver_value'});
|
||||
});
|
||||
});
|
||||
|
||||
it('should copy over data when creating a snapshot', () => {
|
||||
const r1 = {data: 'resolver1'};
|
||||
const r2 = {data: 'resolver2'};
|
||||
|
||||
const n1 = createActivatedRouteSnapshot('a', {resolve: r1});
|
||||
const s1 = new RouterStateSnapshot('url', new TreeNode(empty.root, [new TreeNode(n1, [])]));
|
||||
checkResolveData(s1, empty, inj, () => {});
|
||||
|
||||
const n21 = createActivatedRouteSnapshot('a', {resolve: r1});
|
||||
const n22 = createActivatedRouteSnapshot('b', {resolve: r2});
|
||||
const s2 = new RouterStateSnapshot(
|
||||
'url', new TreeNode(empty.root, [new TreeNode(n21, [new TreeNode(n22, [])])]));
|
||||
checkResolveData(s2, s1, inj, () => {
|
||||
expect(s2.root.firstChild.data).toEqual({data: 'resolver1_value'});
|
||||
expect(s2.root.firstChild.firstChild.data).toEqual({data: 'resolver2_value'});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkResolveData(
|
||||
future: RouterStateSnapshot, curr: RouterStateSnapshot, injector: any, check: any): void {
|
||||
const p = new PreActivation(future, curr, injector);
|
||||
p.traverse(new RouterOutletMap());
|
||||
p.resolveData().subscribe(check, (e) => { throw e; });
|
||||
}
|
||||
|
||||
function createActivatedRouteSnapshot(cmp: string, extra: any = {}): ActivatedRouteSnapshot {
|
||||
return new ActivatedRouteSnapshot(
|
||||
<any>[], {}, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>{}, <any>null, -1,
|
||||
extra.resolve);
|
||||
}
|
143
packages/router/test/router_preloader.spec.ts
Normal file
143
packages/router/test/router_preloader.spec.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core';
|
||||
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
|
||||
|
||||
import {RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index';
|
||||
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '../src/router_preloader';
|
||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||
|
||||
describe('RouterPreloader', () => {
|
||||
@Component({template: ''})
|
||||
class LazyLoadedCmp {
|
||||
}
|
||||
|
||||
describe('should preload configurations', () => {
|
||||
@NgModule({
|
||||
declarations: [LazyLoadedCmp],
|
||||
imports: [RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])]
|
||||
})
|
||||
class LoadedModule2 {
|
||||
}
|
||||
|
||||
@NgModule(
|
||||
{imports: [RouterModule.forChild([{path: 'LoadedModule1', loadChildren: 'expected2'}])]})
|
||||
class LoadedModule1 {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule.withRoutes([{path: 'lazy', loadChildren: 'expected'}])],
|
||||
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should work',
|
||||
fakeAsync(inject(
|
||||
[NgModuleFactoryLoader, RouterPreloader, Router],
|
||||
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
|
||||
const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
|
||||
|
||||
router.events.subscribe(e => {
|
||||
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
|
||||
events.push(e);
|
||||
}
|
||||
});
|
||||
|
||||
loader.stubbedModules = {
|
||||
expected: LoadedModule1,
|
||||
expected2: LoadedModule2,
|
||||
};
|
||||
|
||||
preloader.preload().subscribe(() => {});
|
||||
|
||||
tick();
|
||||
|
||||
const c = router.config;
|
||||
expect(c[0].loadChildren).toEqual('expected');
|
||||
|
||||
const loaded: any = (<any>c[0])._loadedConfig.routes;
|
||||
expect(loaded[0].path).toEqual('LoadedModule1');
|
||||
|
||||
const loaded2: any = (<any>loaded[0])._loadedConfig.routes;
|
||||
expect(loaded2[0].path).toEqual('LoadedModule2');
|
||||
|
||||
expect(events.map(e => e.toString())).toEqual([
|
||||
'RouteConfigLoadStart(path: lazy)',
|
||||
'RouteConfigLoadEnd(path: lazy)',
|
||||
'RouteConfigLoadStart(path: LoadedModule1)',
|
||||
'RouteConfigLoadEnd(path: LoadedModule1)',
|
||||
]);
|
||||
})));
|
||||
});
|
||||
|
||||
describe('should not load configurations with canLoad guard', () => {
|
||||
@NgModule({
|
||||
declarations: [LazyLoadedCmp],
|
||||
imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
|
||||
})
|
||||
class LoadedModule {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule.withRoutes(
|
||||
[{path: 'lazy', loadChildren: 'expected', canLoad: ['someGuard']}])],
|
||||
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should work',
|
||||
fakeAsync(inject(
|
||||
[NgModuleFactoryLoader, RouterPreloader, Router],
|
||||
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
|
||||
loader.stubbedModules = {expected: LoadedModule};
|
||||
|
||||
preloader.preload().subscribe(() => {});
|
||||
|
||||
tick();
|
||||
|
||||
const c = router.config;
|
||||
expect(!!((<any>c[0])._loadedConfig)).toBe(false);
|
||||
})));
|
||||
});
|
||||
|
||||
describe('should ignore errors', () => {
|
||||
@NgModule({
|
||||
declarations: [LazyLoadedCmp],
|
||||
imports: [RouterModule.forChild([{path: 'LoadedModule1', component: LazyLoadedCmp}])]
|
||||
})
|
||||
class LoadedModule {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule.withRoutes([
|
||||
{path: 'lazy1', loadChildren: 'expected1'}, {path: 'lazy2', loadChildren: 'expected2'}
|
||||
])],
|
||||
providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should work',
|
||||
fakeAsync(inject(
|
||||
[NgModuleFactoryLoader, RouterPreloader, Router],
|
||||
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router) => {
|
||||
loader.stubbedModules = {expected2: LoadedModule};
|
||||
|
||||
preloader.preload().subscribe(() => {});
|
||||
|
||||
tick();
|
||||
|
||||
const c = router.config;
|
||||
expect(!!((<any>c[0])._loadedConfig)).toBe(false);
|
||||
expect(!!((<any>c[1])._loadedConfig)).toBe(true);
|
||||
})));
|
||||
});
|
||||
});
|
210
packages/router/test/router_state.spec.ts
Normal file
210
packages/router/test/router_state.spec.ts
Normal file
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, equalParamsAndUrlSegments} from '../src/router_state';
|
||||
import {Params} from '../src/shared';
|
||||
import {UrlSegment} from '../src/url_tree';
|
||||
import {TreeNode} from '../src/utils/tree';
|
||||
|
||||
describe('RouterState & Snapshot', () => {
|
||||
describe('RouterStateSnapshot', () => {
|
||||
let state: RouterStateSnapshot;
|
||||
let a: ActivatedRouteSnapshot;
|
||||
let b: ActivatedRouteSnapshot;
|
||||
let c: ActivatedRouteSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
a = createActivatedRouteSnapshot('a');
|
||||
b = createActivatedRouteSnapshot('b');
|
||||
c = createActivatedRouteSnapshot('c');
|
||||
|
||||
const root = new TreeNode(a, [new TreeNode(b, []), new TreeNode(c, [])]);
|
||||
|
||||
state = new RouterStateSnapshot('url', root);
|
||||
});
|
||||
|
||||
it('should return first child', () => { expect(state.root.firstChild).toBe(b); });
|
||||
|
||||
it('should return children', () => {
|
||||
const cc = state.root.children;
|
||||
expect(cc[0]).toBe(b);
|
||||
expect(cc[1]).toBe(c);
|
||||
});
|
||||
|
||||
it('should return root', () => {
|
||||
const b = state.root.firstChild;
|
||||
expect(b.root).toBe(state.root);
|
||||
});
|
||||
|
||||
it('should return parent', () => {
|
||||
const b = state.root.firstChild;
|
||||
expect(b.parent).toBe(state.root);
|
||||
});
|
||||
|
||||
it('should return path from root', () => {
|
||||
const b = state.root.firstChild;
|
||||
const p = b.pathFromRoot;
|
||||
expect(p[0]).toBe(state.root);
|
||||
expect(p[1]).toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RouterState', () => {
|
||||
let state: RouterState;
|
||||
let a: ActivatedRoute;
|
||||
let b: ActivatedRoute;
|
||||
let c: ActivatedRoute;
|
||||
|
||||
beforeEach(() => {
|
||||
a = createActivatedRoute('a');
|
||||
b = createActivatedRoute('b');
|
||||
c = createActivatedRoute('c');
|
||||
|
||||
const root = new TreeNode(a, [new TreeNode(b, []), new TreeNode(c, [])]);
|
||||
|
||||
state = new RouterState(root, <any>null);
|
||||
});
|
||||
|
||||
it('should return first child', () => { expect(state.root.firstChild).toBe(b); });
|
||||
|
||||
it('should return children', () => {
|
||||
const cc = state.root.children;
|
||||
expect(cc[0]).toBe(b);
|
||||
expect(cc[1]).toBe(c);
|
||||
});
|
||||
|
||||
it('should return root', () => {
|
||||
const b = state.root.firstChild;
|
||||
expect(b.root).toBe(state.root);
|
||||
});
|
||||
|
||||
it('should return parent', () => {
|
||||
const b = state.root.firstChild;
|
||||
expect(b.parent).toBe(state.root);
|
||||
});
|
||||
|
||||
it('should return path from root', () => {
|
||||
const b = state.root.firstChild;
|
||||
const p = b.pathFromRoot;
|
||||
expect(p[0]).toBe(state.root);
|
||||
expect(p[1]).toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equalParamsAndUrlSegments', () => {
|
||||
function createSnapshot(params: Params, url: UrlSegment[]): ActivatedRouteSnapshot {
|
||||
const snapshot = new ActivatedRouteSnapshot(
|
||||
url, params, <any>null, <any>null, <any>null, <any>null, <any>null, <any>null, <any>null,
|
||||
-1, null);
|
||||
snapshot._routerState = new RouterStateSnapshot('', new TreeNode(snapshot, []));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function createSnapshotPairWithParent(
|
||||
params: [Params, Params], parentParams: [Params, Params],
|
||||
urls: [string, string]): [ActivatedRouteSnapshot, ActivatedRouteSnapshot] {
|
||||
const snapshot1 = createSnapshot(params[0], []);
|
||||
const snapshot2 = createSnapshot(params[1], []);
|
||||
|
||||
const snapshot1Parent = createSnapshot(parentParams[0], [new UrlSegment(urls[0], {})]);
|
||||
const snapshot2Parent = createSnapshot(parentParams[1], [new UrlSegment(urls[1], {})]);
|
||||
|
||||
snapshot1._routerState =
|
||||
new RouterStateSnapshot('', new TreeNode(snapshot1Parent, [new TreeNode(snapshot1, [])]));
|
||||
snapshot2._routerState =
|
||||
new RouterStateSnapshot('', new TreeNode(snapshot2Parent, [new TreeNode(snapshot2, [])]));
|
||||
|
||||
return [snapshot1, snapshot2];
|
||||
}
|
||||
|
||||
it('should return false when params are different', () => {
|
||||
expect(equalParamsAndUrlSegments(createSnapshot({a: 1}, []), createSnapshot({a: 2}, [])))
|
||||
.toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when urls are different', () => {
|
||||
expect(equalParamsAndUrlSegments(
|
||||
createSnapshot({a: 1}, [new UrlSegment('a', {})]),
|
||||
createSnapshot({a: 1}, [new UrlSegment('b', {})])))
|
||||
.toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true othewise', () => {
|
||||
expect(equalParamsAndUrlSegments(
|
||||
createSnapshot({a: 1}, [new UrlSegment('a', {})]),
|
||||
createSnapshot({a: 1}, [new UrlSegment('a', {})])))
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false when upstream params are different', () => {
|
||||
const [snapshot1, snapshot2] =
|
||||
createSnapshotPairWithParent([{a: 1}, {a: 1}], [{b: 1}, {c: 1}], ['a', 'a']);
|
||||
|
||||
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return false when upstream urls are different', () => {
|
||||
const [snapshot1, snapshot2] =
|
||||
createSnapshotPairWithParent([{a: 1}, {a: 1}], [{b: 1}, {b: 1}], ['a', 'b']);
|
||||
|
||||
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return true when upstream urls and params are equal', () => {
|
||||
const [snapshot1, snapshot2] =
|
||||
createSnapshotPairWithParent([{a: 1}, {a: 1}], [{b: 1}, {b: 1}], ['a', 'a']);
|
||||
|
||||
expect(equalParamsAndUrlSegments(snapshot1, snapshot2)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('advanceActivatedRoute', () => {
|
||||
|
||||
let route: ActivatedRoute;
|
||||
|
||||
beforeEach(() => { route = createActivatedRoute('a'); });
|
||||
|
||||
function createSnapshot(params: Params, url: UrlSegment[]): ActivatedRouteSnapshot {
|
||||
const queryParams = {};
|
||||
const fragment = '';
|
||||
const data = {};
|
||||
const snapshot = new ActivatedRouteSnapshot(
|
||||
url, params, queryParams, fragment, data, <any>null, <any>null, <any>null, <any>null, -1,
|
||||
null);
|
||||
const state = new RouterStateSnapshot('', new TreeNode(snapshot, []));
|
||||
snapshot._routerState = state;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
it('should call change observers', () => {
|
||||
const firstPlace = createSnapshot({a: 1}, []);
|
||||
const secondPlace = createSnapshot({a: 2}, []);
|
||||
route.snapshot = firstPlace;
|
||||
route._futureSnapshot = secondPlace;
|
||||
|
||||
let hasSeenDataChange = false;
|
||||
route.data.forEach((data) => { hasSeenDataChange = true; });
|
||||
advanceActivatedRoute(route);
|
||||
expect(hasSeenDataChange).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createActivatedRouteSnapshot(cmp: string) {
|
||||
return new ActivatedRouteSnapshot(
|
||||
<any>null, <any>null, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null,
|
||||
<any>null, -1, null);
|
||||
}
|
||||
|
||||
function createActivatedRoute(cmp: string) {
|
||||
return new ActivatedRoute(
|
||||
new BehaviorSubject([new UrlSegment('', {})]), new BehaviorSubject({}), <any>null, <any>null,
|
||||
new BehaviorSubject({}), <any>null, <any>cmp, <any>null);
|
||||
}
|
45
packages/router/test/spy_ng_module_factory_loader.spec.ts
Normal file
45
packages/router/test/spy_ng_module_factory_loader.spec.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {fakeAsync, tick} from '@angular/core/testing';
|
||||
import {SpyNgModuleFactoryLoader} from '../testing/router_testing_module';
|
||||
|
||||
describe('SpyNgModuleFactoryLoader', () => {
|
||||
it('should invoke the compiler when the setter is called', () => {
|
||||
const expected = Promise.resolve('returned');
|
||||
const compiler: any = {compileModuleAsync: () => {}};
|
||||
spyOn(compiler, 'compileModuleAsync').and.returnValue(expected);
|
||||
|
||||
const r = new SpyNgModuleFactoryLoader(<any>compiler);
|
||||
r.stubbedModules = {'one': 'someModule'};
|
||||
|
||||
expect(compiler.compileModuleAsync).toHaveBeenCalledWith('someModule');
|
||||
expect(r.stubbedModules['one']).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return the created promise', () => {
|
||||
const expected = Promise.resolve('returned');
|
||||
const compiler: any = {compileModuleAsync: () => expected};
|
||||
|
||||
const r = new SpyNgModuleFactoryLoader(<any>compiler);
|
||||
r.stubbedModules = {'one': 'someModule'};
|
||||
|
||||
expect(r.load('one')).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return a rejected promise when given an invalid path', fakeAsync(() => {
|
||||
const r = new SpyNgModuleFactoryLoader(<any>null);
|
||||
|
||||
let error: any = null;
|
||||
r.load('two').catch(e => error = e);
|
||||
|
||||
tick();
|
||||
|
||||
expect(error).toEqual(new Error('Cannot find module two'));
|
||||
}));
|
||||
});
|
243
packages/router/test/url_serializer.spec.ts
Normal file
243
packages/router/test/url_serializer.spec.ts
Normal file
@ -0,0 +1,243 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {PRIMARY_OUTLET} from '../src/shared';
|
||||
import {DefaultUrlSerializer, UrlSegmentGroup, encode, serializePath} from '../src/url_tree';
|
||||
|
||||
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('/');
|
||||
});
|
||||
|
||||
it('should parse non-empty urls', () => {
|
||||
const tree = url.parse('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)');
|
||||
|
||||
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one/two');
|
||||
expectSegment(tree.root.children['left'], 'three');
|
||||
expectSegment(tree.root.children['right'], 'four');
|
||||
|
||||
expect(url.serialize(tree)).toEqual('/one/two(left:three//right:four)');
|
||||
});
|
||||
|
||||
it('should parse top-level nodes with only secondary segment', () => {
|
||||
const tree = url.parse('/(left:one)');
|
||||
|
||||
expect(tree.root.numberOfChildren).toEqual(1);
|
||||
expectSegment(tree.root.children['left'], 'one');
|
||||
|
||||
expect(url.serialize(tree)).toEqual('/(left:one)');
|
||||
});
|
||||
|
||||
it('should parse nodes with only secondary segment', () => {
|
||||
const tree = url.parse('/one/(left:two)');
|
||||
|
||||
const one = tree.root.children[PRIMARY_OUTLET];
|
||||
expectSegment(one, 'one', true);
|
||||
expect(one.numberOfChildren).toEqual(1);
|
||||
expectSegment(one.children['left'], 'two');
|
||||
|
||||
expect(url.serialize(tree)).toEqual('/one/(left:two)');
|
||||
});
|
||||
|
||||
it('should not parse empty path segments with params', () => {
|
||||
expect(() => url.parse('/one/two/(;a=1//right:;b=2)'))
|
||||
.toThrowError(/Empty path url segment cannot have parameters/);
|
||||
});
|
||||
|
||||
it('should parse scoped secondary segments', () => {
|
||||
const tree = url.parse('/one/(two//left:three)');
|
||||
|
||||
const primary = tree.root.children[PRIMARY_OUTLET];
|
||||
expectSegment(primary, 'one', true);
|
||||
|
||||
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)');
|
||||
|
||||
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()');
|
||||
|
||||
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)');
|
||||
|
||||
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)');
|
||||
});
|
||||
|
||||
it('should parse key only matrix params', () => {
|
||||
const tree = url.parse('/one;a');
|
||||
|
||||
expectSegment(tree.root.children[PRIMARY_OUTLET], 'one;a=');
|
||||
|
||||
expect(url.serialize(tree)).toEqual('/one;a=');
|
||||
});
|
||||
|
||||
it('should parse query params (root)', () => {
|
||||
const tree = url.parse('/?a=1&b=2');
|
||||
expect(tree.root.children).toEqual({});
|
||||
expect(tree.queryParams).toEqual({a: '1', b: '2'});
|
||||
expect(url.serialize(tree)).toEqual('/?a=1&b=2');
|
||||
});
|
||||
|
||||
it('should parse query params', () => {
|
||||
const tree = url.parse('/one?a=1&b=2');
|
||||
expect(tree.queryParams).toEqual({a: '1', b: '2'});
|
||||
});
|
||||
|
||||
it('should parse query params when with parenthesis', () => {
|
||||
const tree = url.parse('/one?a=(11)&b=(22)');
|
||||
expect(tree.queryParams).toEqual({a: '(11)', b: '(22)'});
|
||||
});
|
||||
|
||||
it('should parse query params when with slashes', () => {
|
||||
const tree = url.parse('/one?a=1/2&b=3/4');
|
||||
expect(tree.queryParams).toEqual({a: '1/2', b: '3/4'});
|
||||
});
|
||||
|
||||
it('should parse key only query params', () => {
|
||||
const tree = url.parse('/one?a');
|
||||
expect(tree.queryParams).toEqual({a: ''});
|
||||
});
|
||||
|
||||
it('should parse a value-empty query param', () => {
|
||||
const tree = url.parse('/one?a=');
|
||||
expect(tree.queryParams).toEqual({a: ''});
|
||||
});
|
||||
|
||||
it('should parse value-empty query params', () => {
|
||||
const tree = url.parse('/one?a=&b=');
|
||||
expect(tree.queryParams).toEqual({a: '', b: ''});
|
||||
});
|
||||
|
||||
it('should serializer query params', () => {
|
||||
const tree = url.parse('/one?a');
|
||||
expect(url.serialize(tree)).toEqual('/one?a=');
|
||||
});
|
||||
|
||||
it('should handle multiple query params of the same name into an array', () => {
|
||||
const tree = url.parse('/one?a=foo&a=bar&a=swaz');
|
||||
expect(tree.queryParams).toEqual({a: ['foo', 'bar', 'swaz']});
|
||||
});
|
||||
|
||||
it('should parse fragment', () => {
|
||||
const tree = url.parse('/one#two');
|
||||
expect(tree.fragment).toEqual('two');
|
||||
expect(url.serialize(tree)).toEqual('/one#two');
|
||||
});
|
||||
|
||||
it('should parse fragment (root)', () => {
|
||||
const tree = url.parse('/#one');
|
||||
expectSegment(tree.root, '');
|
||||
expect(url.serialize(tree)).toEqual('/#one');
|
||||
});
|
||||
|
||||
it('should parse empty fragment', () => {
|
||||
const tree = url.parse('/one#');
|
||||
expect(tree.fragment).toEqual('');
|
||||
expect(url.serialize(tree)).toEqual('/one#');
|
||||
});
|
||||
|
||||
describe('encoding/decoding', () => {
|
||||
it('should encode/decode path segments and parameters', () => {
|
||||
const u =
|
||||
`/${encode("one two")};${encode("p 1")}=${encode("v 1")};${encode("p 2")}=${encode("v 2")}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters)
|
||||
.toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
|
||||
it('should encode/decode "slash" in path segments and parameters', () => {
|
||||
const u = `/${encode("one/two")};${encode("p/1")}=${encode("v/1")}/three`;
|
||||
const tree = url.parse(u);
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one/two');
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({['p/1']: 'v/1'});
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
|
||||
it('should encode/decode query params', () => {
|
||||
const u = `/one?${encode("p 1")}=${encode("v 1")}&${encode("p 2")}=${encode("v 2")}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.queryParams).toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
|
||||
it('should encode/decode fragment', () => {
|
||||
const u = `/one#${encodeURI("one two=three four")}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.fragment).toEqual('one two=three four');
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw when invalid characters inside children', () => {
|
||||
expect(() => url.parse('/one/(left#one)'))
|
||||
.toThrowError('Cannot parse url \'/one/(left#one)\'');
|
||||
});
|
||||
|
||||
it('should throw when missing closing )', () => {
|
||||
expect(() => url.parse('/one/(left')).toThrowError('Cannot parse url \'/one/(left\'');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectSegment(
|
||||
segment: UrlSegmentGroup, expected: string, hasChildren: boolean = false): void {
|
||||
if (segment.segments.filter(s => s.path === '').length > 0) {
|
||||
throw new Error(`UrlSegments cannot be empty ${segment.segments}`);
|
||||
}
|
||||
const p = segment.segments.map(p => serializePath(p)).join('/');
|
||||
expect(p).toEqual(expected);
|
||||
expect(Object.keys(segment.children).length > 0).toEqual(hasChildren);
|
||||
}
|
135
packages/router/test/url_tree.spec.ts
Normal file
135
packages/router/test/url_tree.spec.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {DefaultUrlSerializer, containsTree} from '../src/url_tree';
|
||||
|
||||
describe('UrlTree', () => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
|
||||
describe('containsTree', () => {
|
||||
describe('exact = true', () => {
|
||||
it('should return true when two tree are the same', () => {
|
||||
const url = '/one/(one//left:three)(right:four)';
|
||||
const t1 = serializer.parse(url);
|
||||
const t2 = serializer.parse(url);
|
||||
expect(containsTree(t1, t2, true)).toBe(true);
|
||||
expect(containsTree(t2, t1, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when queryParams are the same', () => {
|
||||
const t1 = serializer.parse('/one/two?test=1&page=5');
|
||||
const t2 = serializer.parse('/one/two?test=1&page=5');
|
||||
expect(containsTree(t1, t2, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when queryParams are not the same', () => {
|
||||
const t1 = serializer.parse('/one/two?test=1&page=5');
|
||||
const t2 = serializer.parse('/one/two?test=1');
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when containee is missing queryParams', () => {
|
||||
const t1 = serializer.parse('/one/two?page=5');
|
||||
const t2 = serializer.parse('/one/two');
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when paths are not the same', () => {
|
||||
const t1 = serializer.parse('/one/two(right:three)');
|
||||
const t2 = serializer.parse('/one/two2(right:three)');
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when container has an extra child', () => {
|
||||
const t1 = serializer.parse('/one/two(right:three)');
|
||||
const t2 = serializer.parse('/one/two');
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when containee has an extra child', () => {
|
||||
const t1 = serializer.parse('/one/two');
|
||||
const t2 = serializer.parse('/one/two(right:three)');
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exact = false', () => {
|
||||
it('should return true when containee is missing a segment', () => {
|
||||
const t1 = serializer.parse('/one/(two//left:three)(right:four)');
|
||||
const t2 = serializer.parse('/one/(two//left:three)');
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when containee is missing some paths', () => {
|
||||
const t1 = serializer.parse('/one/two/three');
|
||||
const t2 = serializer.parse('/one/two');
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true container has its paths splitted into multiple segments', () => {
|
||||
const t1 = serializer.parse('/one/(two//left:three)');
|
||||
const t2 = serializer.parse('/one/two');
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when containee has extra segments', () => {
|
||||
const t1 = serializer.parse('/one/two');
|
||||
const t2 = serializer.parse('/one/(two//left:three)');
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false containee has segments that the container does not have', () => {
|
||||
const t1 = serializer.parse('/one/(two//left:three)');
|
||||
const t2 = serializer.parse('/one/(two//right:four)');
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when containee has extra paths', () => {
|
||||
const t1 = serializer.parse('/one');
|
||||
const t2 = serializer.parse('/one/two');
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when queryParams are the same', () => {
|
||||
const t1 = serializer.parse('/one/two?test=1&page=5');
|
||||
const t2 = serializer.parse('/one/two?test=1&page=5');
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when container contains containees queryParams', () => {
|
||||
const t1 = serializer.parse('/one/two?test=1&u=5');
|
||||
const t2 = serializer.parse('/one/two?u=5');
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when containee does not have queryParams', () => {
|
||||
const t1 = serializer.parse('/one/two?page=5');
|
||||
const t2 = serializer.parse('/one/two');
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when containee has but container does not have queryParams', () => {
|
||||
const t1 = serializer.parse('/one/two');
|
||||
const t2 = serializer.parse('/one/two?page=1');
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when containee has different queryParams', () => {
|
||||
const t1 = serializer.parse('/one/two?page=5');
|
||||
const t2 = serializer.parse('/one/two?test=1');
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when containee has more queryParams than container', () => {
|
||||
const t1 = serializer.parse('/one/two?page=5');
|
||||
const t2 = serializer.parse('/one/two?page=5&test=1');
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
53
packages/router/test/utils/tree.spec.ts
Normal file
53
packages/router/test/utils/tree.spec.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Tree, TreeNode} from '../../src/utils/tree';
|
||||
|
||||
describe('tree', () => {
|
||||
it('should return the root of the tree', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(1, []));
|
||||
expect(t.root).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return the parent of a node', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
|
||||
expect(t.parent(1)).toEqual(null);
|
||||
expect(t.parent(2)).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return the parent of a node (second child)', () => {
|
||||
const t = new Tree<any>(
|
||||
new TreeNode<number>(1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])]));
|
||||
expect(t.parent(1)).toEqual(null);
|
||||
expect(t.parent(3)).toEqual(1);
|
||||
});
|
||||
|
||||
it('should return the children of a node', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
|
||||
expect(t.children(1)).toEqual([2]);
|
||||
expect(t.children(2)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return the first child of a node', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
|
||||
expect(t.firstChild(1)).toEqual(2);
|
||||
expect(t.firstChild(2)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return the siblings of a node', () => {
|
||||
const t = new Tree<any>(
|
||||
new TreeNode<number>(1, [new TreeNode<number>(2, []), new TreeNode<number>(3, [])]));
|
||||
expect(t.siblings(2)).toEqual([3]);
|
||||
expect(t.siblings(1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return the path to the root', () => {
|
||||
const t = new Tree<any>(new TreeNode<number>(1, [new TreeNode<number>(2, [])]));
|
||||
expect(t.pathFromRoot(2)).toEqual([1, 2]);
|
||||
});
|
||||
});
|
14
packages/router/testing/src/index.ts
Normal file
14
packages/router/testing/src/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
* @description
|
||||
* Entry point for all public APIs of the router/testing package.
|
||||
*/
|
||||
export * from './router_testing_module';
|
141
packages/router/testing/src/router_testing_module.ts
Normal file
141
packages/router/testing/src/router_testing_module.ts
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {Location, LocationStrategy} from '@angular/common';
|
||||
import {MockLocationStrategy, SpyLocation} from '@angular/common/testing';
|
||||
import {Compiler, Injectable, Injector, ModuleWithProviders, NgModule, NgModuleFactory, NgModuleFactoryLoader, Optional} from '@angular/core';
|
||||
import {NoPreloading, PreloadingStrategy, ROUTES, Route, Router, RouterModule, RouterOutletMap, Routes, UrlHandlingStrategy, UrlSerializer, provideRoutes, ɵROUTER_PROVIDERS as ROUTER_PROVIDERS, ɵflatten as flatten} from '@angular/router';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Allows to simulate the loading of ng modules in tests.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* const loader = TestBed.get(NgModuleFactoryLoader);
|
||||
*
|
||||
* @Component({template: 'lazy-loaded'})
|
||||
* class LazyLoadedComponent {}
|
||||
* @NgModule({
|
||||
* declarations: [LazyLoadedComponent],
|
||||
* imports: [RouterModule.forChild([{path: 'loaded', component: LazyLoadedComponent}])]
|
||||
* })
|
||||
*
|
||||
* class LoadedModule {}
|
||||
*
|
||||
* // sets up stubbedModules
|
||||
* loader.stubbedModules = {lazyModule: LoadedModule};
|
||||
*
|
||||
* router.resetConfig([
|
||||
* {path: 'lazy', loadChildren: 'lazyModule'},
|
||||
* ]);
|
||||
*
|
||||
* router.navigateByUrl('/lazy/loaded');
|
||||
* ```
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@Injectable()
|
||||
export class SpyNgModuleFactoryLoader implements NgModuleFactoryLoader {
|
||||
/**
|
||||
* @docsNotRequired
|
||||
*/
|
||||
private _stubbedModules: {[path: string]: Promise<NgModuleFactory<any>>} = {};
|
||||
|
||||
/**
|
||||
* @docsNotRequired
|
||||
*/
|
||||
set stubbedModules(modules: {[path: string]: any}) {
|
||||
const res: {[path: string]: any} = {};
|
||||
for (const t of Object.keys(modules)) {
|
||||
res[t] = this.compiler.compileModuleAsync(modules[t]);
|
||||
}
|
||||
this._stubbedModules = res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @docsNotRequired
|
||||
*/
|
||||
get stubbedModules(): {[path: string]: any} { return this._stubbedModules; }
|
||||
|
||||
constructor(private compiler: Compiler) {}
|
||||
|
||||
load(path: string): Promise<NgModuleFactory<any>> {
|
||||
if (this._stubbedModules[path]) {
|
||||
return this._stubbedModules[path];
|
||||
} else {
|
||||
return <any>Promise.reject(new Error(`Cannot find module ${path}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Router setup factory function used for testing.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export function setupTestingRouter(
|
||||
urlSerializer: UrlSerializer, outletMap: RouterOutletMap, location: Location,
|
||||
loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][],
|
||||
urlHandlingStrategy?: UrlHandlingStrategy) {
|
||||
const router = new Router(
|
||||
null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(routes));
|
||||
if (urlHandlingStrategy) {
|
||||
router.urlHandlingStrategy = urlHandlingStrategy;
|
||||
}
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Sets up the router to be used for testing.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* beforeEach(() => {
|
||||
* TestBed.configureTestModule({
|
||||
* imports: [
|
||||
* RouterTestingModule.withRoutes(
|
||||
* [{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}])]
|
||||
* )
|
||||
* ]
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* The modules sets up the router to be used for testing.
|
||||
* It provides spy implementations of {@link Location}, {@link LocationStrategy}, and {@link
|
||||
* NgModuleFactoryLoader}.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
@NgModule({
|
||||
exports: [RouterModule],
|
||||
providers: [
|
||||
ROUTER_PROVIDERS, {provide: Location, useClass: SpyLocation},
|
||||
{provide: LocationStrategy, useClass: MockLocationStrategy},
|
||||
{provide: NgModuleFactoryLoader, useClass: SpyNgModuleFactoryLoader}, {
|
||||
provide: Router,
|
||||
useFactory: setupTestingRouter,
|
||||
deps: [
|
||||
UrlSerializer, RouterOutletMap, Location, NgModuleFactoryLoader, Compiler, Injector, ROUTES,
|
||||
[UrlHandlingStrategy, new Optional()]
|
||||
]
|
||||
},
|
||||
{provide: PreloadingStrategy, useExisting: NoPreloading}, provideRoutes([])
|
||||
]
|
||||
})
|
||||
export class RouterTestingModule {
|
||||
static withRoutes(routes: Routes): ModuleWithProviders {
|
||||
return {ngModule: RouterTestingModule, providers: [provideRoutes(routes)]};
|
||||
}
|
||||
}
|
18
packages/router/testing/tsconfig-build.json
Normal file
18
packages/router/testing/tsconfig-build.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages/core"],
|
||||
"@angular/common": ["../../../dist/packages/common"],
|
||||
"@angular/common/testing": ["../../../dist/packages/common/testing"],
|
||||
"@angular/platform-browser": ["../../../dist/packages/platform-browser"],
|
||||
"@angular/router": ["../../../dist/packages/router"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"index.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
35
packages/router/tsconfig-build.json
Normal file
35
packages/router/tsconfig-build.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"stripInternal": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
"noEmitOnError": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"outDir": "../../../dist/packages-dist/router",
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"],
|
||||
"@angular/common": ["../../../dist/packages-dist/common"],
|
||||
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"]
|
||||
},
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"lib": ["es2015", "dom"],
|
||||
"target": "es2015",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"files": [
|
||||
"public_api.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/router"
|
||||
}
|
||||
}
|
76
packages/router/upgrade.ts
Normal file
76
packages/router/upgrade.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
import {APP_BOOTSTRAP_LISTENER, ComponentRef, InjectionToken} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {UpgradeModule} from '@angular/upgrade/static';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Creates an initializer that in addition to setting up the Angular
|
||||
* router sets up the ngRoute integration.
|
||||
*
|
||||
* @howToUse
|
||||
*
|
||||
* ```
|
||||
* @NgModule({
|
||||
* imports: [
|
||||
* RouterModule.forRoot(SOME_ROUTES),
|
||||
* UpgradeModule
|
||||
* ],
|
||||
* providers: [
|
||||
* RouterUpgradeInitializer
|
||||
* ]
|
||||
* })
|
||||
* export class AppModule {
|
||||
* ngDoBootstrap() {}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export const RouterUpgradeInitializer = {
|
||||
provide: APP_BOOTSTRAP_LISTENER,
|
||||
multi: true,
|
||||
useFactory: locationSyncBootstrapListener,
|
||||
deps: [UpgradeModule]
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function locationSyncBootstrapListener(ngUpgrade: UpgradeModule) {
|
||||
return () => { setUpLocationSync(ngUpgrade); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @whatItDoes Sets up a location synchronization.
|
||||
*
|
||||
* History.pushState does not fire onPopState, so the angular2 location
|
||||
* doesn't detect it. The workaround is to attach a location change listener
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function setUpLocationSync(ngUpgrade: UpgradeModule) {
|
||||
if (!ngUpgrade.$injector) {
|
||||
throw new Error(`
|
||||
RouterUpgradeInitializer can be used only after UpgradeModule.bootstrap has been called.
|
||||
Remove RouterUpgradeInitializer and call setUpLocationSync after UpgradeModule.bootstrap.
|
||||
`);
|
||||
}
|
||||
|
||||
const router: Router = ngUpgrade.injector.get(Router);
|
||||
const url = document.createElement('a');
|
||||
|
||||
ngUpgrade.$injector.get('$rootScope')
|
||||
.$on('$locationChangeStart', (_: any, next: string, __: string) => {
|
||||
url.href = next;
|
||||
router.navigateByUrl(url.pathname);
|
||||
});
|
||||
}
|
18
packages/router/upgrade/tsconfig-build.json
Normal file
18
packages/router/upgrade/tsconfig-build.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "./tsconfig-build",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/packages-dist/router/upgrade",
|
||||
"paths": {
|
||||
"@angular/core": ["../../../dist/packages-dist/core"],
|
||||
"@angular/platform-browser": ["../../../dist/packages-dist/platform-browser"],
|
||||
"@angular/router": ["../../../dist/packages-dist/router"],
|
||||
"@angular/upgrade/static": ["../../../dist/packages-dist/upgrade/static"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"upgrade.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"strictMetadataEmit": true
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user