├── .editorconfig
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ └── main.yml
├── .gitignore
├── .npmignore
├── .vscode
└── settings.json
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
├── bower.json
├── build
├── args.js
├── babel-options.js
├── paths.js
├── scripts
│ ├── build.js
│ ├── changelog.js
│ └── dev.js
├── tasks
│ ├── args.js
│ ├── build.js
│ ├── clean.js
│ ├── dev.js
│ ├── doc.js
│ ├── lint.js
│ ├── prepare-release.js
│ └── test.js
└── typescript-options.js
├── dist
├── amd
│ ├── aurelia-router.js
│ └── aurelia-router.js.map
├── aurelia-router.d.ts
├── commonjs
│ ├── aurelia-router.js
│ └── aurelia-router.js.map
├── es2015
│ ├── aurelia-router.js
│ └── aurelia-router.js.map
└── native-modules
│ ├── aurelia-router.js
│ └── aurelia-router.js.map
├── doc
├── CHANGELOG.md
├── MAINTAINER.md
├── api.json
└── cleanup.js
├── karma.conf.js
├── package-lock.json
├── package.json
├── src
├── activation-strategy.ts
├── app-router.ts
├── aurelia-router.ts
├── interfaces.ts
├── nav-model.ts
├── navigation-commands.ts
├── navigation-instruction.ts
├── navigation-plan.ts
├── next.ts
├── pipeline-provider.ts
├── pipeline-slot-name.ts
├── pipeline-status.ts
├── pipeline.ts
├── route-loader.ts
├── router-configuration.ts
├── router-event.ts
├── router.ts
├── step-activation.ts
├── step-build-navigation-plan.ts
├── step-commit-changes.ts
├── step-load-route.ts
├── util.ts
├── utilities-activation.ts
└── utilities-route-loading.ts
├── test
├── activation.spec.ts
├── app-router.spec.ts
├── navigation-commands.spec.ts
├── navigation-instruction.spec.ts
├── navigation-plan.spec.ts
├── pipeline.spec.ts
├── route-config-validation.spec.ts
├── route-loading
│ ├── load-component.spec.ts
│ └── load-route-step.spec.ts
├── router.spec.ts
├── setup.ts
├── shared.ts
└── utils.spec.ts
├── tsconfig.json
├── tslint.json
└── typings.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # 2 space indentation
12 | [**.*]
13 | indent_style = space
14 | indent_size = 2
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "extends": [
5 | "plugin:@typescript-eslint/recommended"
6 | ],
7 | "ignorePatterns": [
8 | "node_modules",
9 | "dist",
10 | "build",
11 | ".vscode",
12 | "*.config.js",
13 | ".webpack",
14 | "_warmup",
15 | "**/*.js"
16 | ],
17 | "plugins": [],
18 | "parserOptions": {
19 | "ecmaVersion": 2019,
20 | "sourceType": "module"
21 | },
22 | "rules": {
23 | "@typescript-eslint/no-namespace": "off",
24 | "@typescript-eslint/camelcase": "off",
25 | "@typescript-eslint/no-explicit-any": "off",
26 | "@typescript-eslint/explicit-function-return-type": "off",
27 | "@typescript-eslint/no-empty-function": "off",
28 | "@typescript-eslint/consistent-type-assertions": "off",
29 | "@typescript-eslint/ban-ts-ignore": "off",
30 | "@typescript-eslint/no-var-requires": "off",
31 | "@typescript-eslint/explicit-module-boundary-types": "off",
32 | "prefer-const": "off",
33 | "@typescript-eslint/ban-types": "off"
34 | }
35 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
19 | **I'm submitting a bug report**
20 | **I'm submitting a feature request**
21 |
22 | * **Library Version:**
23 | major.minor.patch-pre
24 |
25 |
26 | **Please tell us about your environment:**
27 | * **Operating System:**
28 | OSX 10.x|Linux (distro)|Windows [7|8|8.1|10]
29 |
30 | * **Node Version:**
31 | 6.2.0
32 |
36 |
37 | * **NPM Version:**
38 | 3.8.9
39 |
43 |
44 | * **JSPM OR Webpack AND Version**
45 | JSPM 0.16.32 | webpack 2.1.0-beta.17
46 |
52 |
53 | * **Browser:**
54 | all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView
55 |
56 | * **Language:**
57 | all | TypeScript X.X | ESNext
58 |
59 |
60 | **Current behavior:**
61 |
62 |
63 | **Expected/desired behavior:**
64 |
71 |
72 |
73 | * **What is the expected behavior?**
74 |
75 |
76 | * **What is the motivation / use case for changing the behavior?**
77 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: main
2 | on: [push]
3 |
4 | jobs:
5 |
6 | ci:
7 | timeout-minutes: 10
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-node@v1
12 | with:
13 | node-version: 14
14 | - run: npm ci
15 | - run: npm run cut-release
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | jspm_packages
3 | bower_components
4 | .idea
5 | .DS_STORE
6 | .rollupcache
7 | build/reports
8 | .npmrc
9 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | jspm_packages
2 | bower_components
3 | .idea
4 | build/reports
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | We'd love for you to contribute and to make this project even better than it is today! If this interests you, please begin by reading [our contributing guidelines](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). The contributing document will provide you with all the information you need to get started. Once you have read that, you will need to also [sign our CLA](http://goo.gl/forms/dI8QDDSyKR) before we can accept a Pull Request from you. More information on the process is included in the [contributor's guide](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md).
4 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
19 | **I'm submitting a bug report**
20 | **I'm submitting a feature request**
21 |
22 | * **Library Version:**
23 | major.minor.patch-pre
24 |
25 |
26 | **Please tell us about your environment:**
27 | * **Operating System:**
28 | OSX 10.x|Linux (distro)|Windows [7|8|8.1|10]
29 |
30 | * **Node Version:**
31 | 6.2.0
32 |
36 |
37 | * **NPM Version:**
38 | 3.8.9
39 |
43 |
44 | * **JSPM OR Webpack AND Version**
45 | JSPM 0.16.32 | webpack 2.1.0-beta.17
46 |
52 |
53 | * **Browser:**
54 | all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView
55 |
56 | * **Language:**
57 | all | TypeScript X.X | ESNext
58 |
59 |
60 | **Current behavior:**
61 |
62 |
63 | **Expected/desired behavior:**
64 |
71 |
72 |
73 | * **What is the expected behavior?**
74 |
75 |
76 | * **What is the motivation / use case for changing the behavior?**
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2010 - 2018 Blue Spire Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [](https://opensource.org/licenses/MIT)
8 | [](https://www.npmjs.com/package/aurelia-router)
9 | 
10 | [](https://discourse.aurelia.io)
11 | [](https://twitter.com/intent/follow?screen_name=aureliaeffect)
12 | [](https://discord.gg/RBtyM6u)
13 |
14 | # aurelia-router
15 |
16 | This library is part of the [Aurelia](http://www.aurelia.io/) platform and contains a powerful client-side router.
17 |
18 | > To keep up to date on [Aurelia](http://www.aurelia.io/), please visit and subscribe to [the official blog](http://blog.aurelia.io/) and [our email list](http://eepurl.com/ces50j). We also invite you to [follow us on twitter](https://twitter.com/aureliaeffect). If you have questions look around our [Discourse forums](https://discourse.aurelia.io/), chat in our [community on Discord](https://discord.gg/RBtyM6u) or use [stack overflow](http://stackoverflow.com/search?q=aurelia). Documentation can be found [in our developer hub](http://aurelia.io/docs/routing).
19 |
20 | ## Platform Support
21 |
22 | This library can be used in the **browser** only.
23 |
24 | ## Building The Code
25 |
26 | To build the code, follow these steps.
27 |
28 | 1. Ensure that [NodeJS](http://nodejs.org/) is installed. This provides the platform on which the build tooling runs.
29 | 2. From the project folder, execute the following command:
30 |
31 | ```shell
32 | npm install
33 | ```
34 | 3. To build the code, you can now run:
35 |
36 | ```shell
37 | npm run build
38 | ```
39 | 4. You will find the compiled code in the `dist` folder, available in three module formats: AMD, CommonJS and ES6.
40 |
41 | ## Development
42 |
43 | 1. To run the project in development mode, you can run:
44 |
45 | ```shell
46 | npm start
47 | ```
48 |
49 | 2. If you want to copy over the newly built bundle, you can specified `--target`:
50 |
51 | ```
52 | npm start -- --target ..\my-test-project
53 | ```
54 |
55 | ## Running The Tests
56 |
57 | To run the unit tests, first ensure that you have followed the steps above in order to install all dependencies and successfully build the library. Once you have done that, proceed with these additional steps:
58 |
59 | 1. Run the test script:
60 |
61 | ```
62 | npm run test
63 | ```
64 |
65 | 2. With watch options to rerun the test (headless):
66 |
67 | ```
68 | npm run test:watch
69 | ```
70 |
71 | 3. With watch options to rerun the test (with browser):
72 |
73 | ```
74 | npm run test:debugger
75 | ```
76 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aurelia-router",
3 | "version": "1.7.2",
4 | "description": "A powerful client-side router.",
5 | "keywords": [
6 | "aurelia",
7 | "router"
8 | ],
9 | "main": "dist/commonjs/aurelia-router.js",
10 | "moduleType": "node",
11 | "homepage": "http://aurelia.io",
12 | "license": "MIT",
13 | "authors": [
14 | "Rob Eisenberg (http://robeisenberg.com/)"
15 | ],
16 | "repository": {
17 | "type": "git",
18 | "url": "http://github.com/aurelia/router"
19 | },
20 | "dependencies": {
21 | "aurelia-dependency-injection": "^1.0.0",
22 | "aurelia-event-aggregator": "^1.0.0",
23 | "aurelia-history": "^1.0.0",
24 | "aurelia-logging": "^1.0.0",
25 | "aurelia-path": "^1.0.0",
26 | "aurelia-route-recognizer": "^1.0.0"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/build/args.js:
--------------------------------------------------------------------------------
1 | var yargs = require('yargs');
2 |
3 | var argv = yargs.argv,
4 | validBumpTypes = "major|minor|patch|prerelease".split("|"),
5 | bump = (argv.bump || 'patch').toLowerCase();
6 |
7 | if(validBumpTypes.indexOf(bump) === -1) {
8 | throw new Error('Unrecognized bump "' + bump + '".');
9 | }
10 |
11 | module.exports = {
12 | bump: bump
13 | };
14 |
--------------------------------------------------------------------------------
/build/babel-options.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var paths = require('./paths');
3 |
4 | exports.base = function() {
5 | var config = {
6 | filename: '',
7 | filenameRelative: '',
8 | sourceMap: true,
9 | sourceRoot: '',
10 | moduleRoot: path.resolve('src').replace(/\\/g, '/'),
11 | moduleIds: false,
12 | comments: false,
13 | compact: false,
14 | code: true,
15 | presets: [ 'es2015-loose', 'stage-1' ],
16 | plugins: [
17 | 'syntax-flow',
18 | 'transform-decorators-legacy',
19 | ]
20 | };
21 | if (!paths.useTypeScriptForDTS) {
22 | config.plugins.push(
23 | ['babel-dts-generator', {
24 | packageName: paths.packageName,
25 | typings: '',
26 | suppressModulePath: true,
27 | suppressComments: false,
28 | memberOutputFilter: /^_.*/,
29 | suppressAmbientDeclaration: true
30 | }]
31 | );
32 | };
33 | config.plugins.push('transform-flow-strip-types');
34 | return config;
35 | }
36 |
37 | exports.commonjs = function() {
38 | var options = exports.base();
39 | options.plugins.push('transform-es2015-modules-commonjs');
40 | return options;
41 | };
42 |
43 | exports.amd = function() {
44 | var options = exports.base();
45 | options.plugins.push('transform-es2015-modules-amd');
46 | return options;
47 | };
48 |
49 | exports.system = function() {
50 | var options = exports.base();
51 | options.plugins.push('transform-es2015-modules-systemjs');
52 | return options;
53 | };
54 |
55 | exports.es2015 = function() {
56 | var options = exports.base();
57 | options.presets = ['stage-1']
58 | return options;
59 | };
60 |
61 | exports['native-modules'] = function() {
62 | var options = exports.base();
63 | options.presets[0] = 'es2015-loose-native-modules';
64 | return options;
65 | }
66 |
--------------------------------------------------------------------------------
/build/paths.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var fs = require('fs');
3 |
4 | // hide warning //
5 | var emitter = require('events');
6 | emitter.defaultMaxListeners = 20;
7 |
8 | var appRoot = 'src/';
9 | var pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
10 |
11 | var paths = {
12 | root: appRoot,
13 | source: appRoot + '**/*.js',
14 | html: appRoot + '**/*.html',
15 | style: 'styles/**/*.css',
16 | output: 'dist/',
17 | doc:'./doc',
18 | e2eSpecsSrc: 'test/e2e/src/*.js',
19 | e2eSpecsDist: 'test/e2e/dist/',
20 | packageName: pkg.name,
21 | ignore: [],
22 | useTypeScriptForDTS: false,
23 | importsToAdd: [],
24 | sort: true
25 | };
26 |
27 | paths.files = [
28 | paths.source
29 | ];
30 |
31 | module.exports = paths;
32 |
--------------------------------------------------------------------------------
/build/scripts/build.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const rollup = require('rollup');
3 | /** @type {(options: import('@rollup/plugin-typescript').RollupTypescriptOptions) => import('rollup').Plugin} */
4 | // @ts-ignore
5 | const typescript = require('@rollup/plugin-typescript');
6 | const rimraf = require('rimraf');
7 |
8 | const LIB_NAME = 'aurelia-router';
9 | const cacheRoot = '.rollupcache';
10 | const externalLibs = [
11 | 'aurelia-binding',
12 | 'aurelia-templating',
13 | 'aurelia-path',
14 | 'aurelia-dependency-injection',
15 | 'aurelia-event-aggregator',
16 | 'aurelia-logging',
17 | 'aurelia-history',
18 | 'aurelia-route-recognizer'
19 | ];
20 |
21 | clean().then(build).then(generateDts);
22 |
23 | /**
24 | * @type {() => Promise}
25 | */
26 | function clean() {
27 | console.log('\n==============\nCleaning dist folder...\n==============');
28 | return new Promise(resolve => {
29 | rimraf('dist', (error) => {
30 | if (error) {
31 | throw error;
32 | }
33 | resolve(void 0);
34 | });
35 | });
36 | }
37 |
38 | function generateDts() {
39 | console.log('\n==============\nGenerating dts bundle...\n==============');
40 | return new Promise(resolve => {
41 | const ChildProcess = require('child_process');
42 | ChildProcess.exec('npm run build:dts', (err, stdout, stderr) => {
43 | if (err || stderr) {
44 | console.log('Generating dts error:');
45 | console.log(stderr);
46 | } else {
47 | console.log('Generated dts bundle successfully');
48 | console.log(stdout);
49 | }
50 | resolve();
51 | });
52 | });
53 | };
54 |
55 | function build() {
56 | console.log('\n==============\nBuidling...\n==============');
57 | const inputFileName = `src/${LIB_NAME}.ts`;
58 |
59 | return Promise.all([
60 | {
61 | input: inputFileName,
62 | output: [
63 | { file: `dist/es2015/${LIB_NAME}.js`, format: 'es', sourcemap: true }
64 | ],
65 | external: externalLibs,
66 | plugins: [
67 | typescript({
68 | target: 'es2015',
69 | removeComments: true,
70 | }),
71 | ]
72 | },
73 | {
74 | input: inputFileName,
75 | output: [
76 | { file: `dist/es2017/${LIB_NAME}.js`, format: 'es', sourcemap: true }
77 | ],
78 | external: externalLibs,
79 | plugins: [
80 | typescript({
81 | target: 'es2017',
82 | removeComments: true,
83 | }),
84 | ]
85 | },
86 | {
87 | input: inputFileName,
88 | output: [
89 | { file: `dist/commonjs/${LIB_NAME}.js`, format: 'cjs', sourcemap: true },
90 | { file: `dist/amd/${LIB_NAME}.js`, format: 'amd', amd: { id: LIB_NAME }, sourcemap: true },
91 | { file: `dist/native-modules/${LIB_NAME}.js`, format: 'es', sourcemap: true }
92 | ],
93 | external: externalLibs,
94 | plugins: [
95 | typescript({
96 | target: 'es5',
97 | removeComments: true,
98 | }),
99 | ]
100 | }
101 | ].map(cfg => {
102 | return rollup
103 | .rollup(cfg)
104 | .then(bundle => Promise.all(cfg.output.map(o => bundle.write(o))));
105 | }));
106 | };
107 |
--------------------------------------------------------------------------------
/build/scripts/changelog.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const paths = require('../paths');
3 | const path = require('path');
4 | const conventionalChangelog = require('conventional-changelog');
5 | const dest = path.resolve(process.cwd(), paths.doc, 'CHANGELOG.md');
6 |
7 | let changelogChunk = '';
8 | const changelogStream = conventionalChangelog({ preset: 'angular' })
9 | .on('data', chunk => changelogChunk += chunk.toString('utf8'))
10 | .on('end', () => {
11 | changelogStream.removeAllListeners();
12 | const data = fs.readFileSync(dest, 'utf-8');
13 | const fd = fs.openSync(dest, 'w+');
14 | fs.writeSync(fd, Buffer.from(changelogChunk, 'utf8'), 0, changelogChunk.length, 0);
15 | fs.writeSync(fd, Buffer.from(data, 'utf8'), 0, data.length, changelogChunk.length);
16 | });
17 |
--------------------------------------------------------------------------------
/build/scripts/dev.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const args = require('../tasks/args');
3 | const rollup = require('rollup');
4 | /** @type {(options: import('@rollup/plugin-typescript').RollupTypescriptOptions) => import('rollup').Plugin} */
5 | // @ts-ignore
6 | const typescript = require('@rollup/plugin-typescript');
7 | const ChildProcess = require('child_process');
8 | const pkg = require('../../package.json');
9 |
10 | const targetFormats = args.format || ['commonjs']; // by default only run devs for commonjs
11 | const targetDir = args.target;
12 |
13 | const LIB_NAME = pkg.name;
14 |
15 | const buildConfigs = {
16 | es2015: {
17 | output: {
18 | file: `dist/es2015/${LIB_NAME}.js`,
19 | format: 'es'
20 | },
21 | tsConfig: {
22 | target: 'es2015'
23 | }
24 | },
25 | es2017: {
26 | output: {
27 | file: `dist/es2017/${LIB_NAME}.js`,
28 | format: 'es'
29 | },
30 | tsConfig: {
31 | target: 'es2015'
32 | }
33 | },
34 | amd: {
35 | output: {
36 | file: `dist/amd/${LIB_NAME}.js`,
37 | format: 'amd',
38 | amd: { id: LIB_NAME }
39 | },
40 | tsConfig: {
41 | target: 'es5'
42 | }
43 | },
44 | commonjs: {
45 | output: {
46 | file: `dist/commonjs/${LIB_NAME}.js`,
47 | format: 'cjs'
48 | },
49 | tsConfig: {
50 | target: 'es5'
51 | }
52 | },
53 | 'native-modules': {
54 | output: {
55 | file: `dist/commonjs/${LIB_NAME}.js`,
56 | format: 'es'
57 | },
58 | tsConfig: {
59 | target: 'es5'
60 | }
61 | }
62 | };
63 |
64 | console.log('Running dev with targets:', targetFormats);
65 |
66 | /**
67 | * @param {string} format
68 | */
69 | async function roll(format) {
70 | const inputOptions = {
71 | input: 'src/aurelia-router.ts',
72 | external: [
73 | 'aurelia-binding',
74 | 'aurelia-path',
75 | 'aurelia-templating',
76 | 'aurelia-dependency-injection',
77 | 'aurelia-event-aggregator',
78 | 'aurelia-logging',
79 | 'aurelia-history',
80 | 'aurelia-route-recognizer'
81 | ],
82 | plugins: [
83 | typescript({
84 | target: buildConfigs[format].tsConfig.target,
85 | removeComments: true
86 | })
87 | ]
88 | };
89 | console.log('Starting watcher');
90 | const watcher = rollup
91 | .watch({
92 | ...inputOptions,
93 | output: buildConfigs[format].output
94 | });
95 |
96 | watcher.on('event', (e) => {
97 | if (e.code === 'BUNDLE_END') {
98 | console.log('Finished compilation. Running post task bundling dts.');
99 | generateDtsBundle();
100 | }
101 | });
102 | }
103 |
104 | function generateDtsBundle() {
105 | return new Promise(resolve => {
106 | ChildProcess.exec('npm run bundle-dts', (err, stdout, stderr) => {
107 | if (err || stderr) {
108 | console.log('Bundling dts error');
109 | console.log(err);
110 | console.log('========');
111 | console.log('stderr');
112 | console.log(stderr);
113 | } else {
114 | console.log('Generated dts bundle successfully');
115 | }
116 | resolve(err ? [null, err] : [null, null]);
117 | });
118 | });
119 | }
120 |
121 | targetFormats.forEach(roll);
122 |
123 | console.log('Target directory for copy: "' + targetDir + '"');
124 | if (targetDir) {
125 | console.log('Watching dist folder');
126 | const gulpWatch = require('gulp-watch');
127 | const path = require('path');
128 | const cwd = process.cwd();
129 | const destPath = path.join(cwd, targetDir, 'node_modules', 'aurelia-router');
130 | const fs = require('fs');
131 | gulpWatch('dist/**/*.*', { ignoreInitial: true }, (vinyl) => {
132 | if (vinyl.event !== 'unlink') {
133 | console.log(`change occurred at "${vinyl.path}". Copying over to specified project`);
134 | const subPath = vinyl.path.replace(cwd, '');
135 | try {
136 | fs.createReadStream(vinyl.path)
137 | .pipe(fs.createWriteStream(path.join(destPath, subPath)));
138 | } catch (ex) {
139 | console.log(`Error trying to copy file from "${vinyl.path}" to "${destPath}"`);
140 | console.log(ex);
141 | }
142 | }
143 | });
144 | }
145 |
--------------------------------------------------------------------------------
/build/tasks/args.js:
--------------------------------------------------------------------------------
1 | module.exports = require('yargs')
2 | .options('target', {
3 | alias: 't',
4 | description: 'target module dir to copy build results into (eg. "--target ../other-module" to copy build results into "../other-module/node_modules/this-module/dist/…" whenever they change)'
5 | })
6 | .options('format', {
7 | alias: 'f',
8 | array: true,
9 | description: 'format to compile to (eg. "es2015", "commonjs", …). Can be set muliple times to compile to multiple formats. Default is all formats.'
10 | })
11 | .argv;
12 |
--------------------------------------------------------------------------------
/build/tasks/build.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var runSequence = require('run-sequence');
3 | var to5 = require('gulp-babel');
4 | var paths = require('../paths');
5 | var compilerOptions = require('../babel-options');
6 | var compilerTsOptions = require('../typescript-options');
7 | var assign = Object.assign || require('object.assign');
8 | var through2 = require('through2');
9 | var concat = require('gulp-concat');
10 | var insert = require('gulp-insert');
11 | var rename = require('gulp-rename');
12 | var tools = require('aurelia-tools');
13 | var ts = require('gulp-typescript');
14 | var gutil = require('gulp-util');
15 | var gulpIgnore = require('gulp-ignore');
16 | var merge = require('merge2');
17 | var jsName = paths.packageName + '.js';
18 | var compileToModules = ['es2015', 'commonjs', 'amd', 'system', 'native-modules'];
19 |
20 | function cleanGeneratedCode() {
21 | return through2.obj(function(file, enc, callback) {
22 | file.contents = new Buffer(tools.cleanGeneratedCode(file.contents.toString('utf8')));
23 | this.push(file);
24 | return callback();
25 | });
26 | }
27 |
28 | gulp.task('build-index', function() {
29 | var importsToAdd = paths.importsToAdd.slice();
30 |
31 | var src = gulp.src(paths.files);
32 |
33 | if (paths.sort) {
34 | src = src.pipe(tools.sortFiles());
35 | }
36 |
37 | if (paths.ignore) {
38 | paths.ignore.forEach(function(filename){
39 | src = src.pipe(gulpIgnore.exclude(filename));
40 | });
41 | }
42 |
43 | return src.pipe(through2.obj(function(file, enc, callback) {
44 | file.contents = new Buffer(tools.extractImports(file.contents.toString('utf8'), importsToAdd));
45 | this.push(file);
46 | return callback();
47 | }))
48 | .pipe(concat(jsName))
49 | .pipe(insert.transform(function(contents) {
50 | return tools.createImportBlock(importsToAdd) + contents;
51 | }))
52 | .pipe(gulp.dest(paths.output));
53 | });
54 |
55 | function gulpFileFromString(filename, string) {
56 | var src = require('stream').Readable({ objectMode: true });
57 | src._read = function() {
58 | this.push(new gutil.File({ cwd: paths.appRoot, base: paths.output, path: filename, contents: new Buffer(string) }))
59 | this.push(null)
60 | }
61 | return src;
62 | }
63 |
64 | function srcForBabel() {
65 | return merge(
66 | gulp.src(paths.output + jsName),
67 | gulpFileFromString(paths.output + 'index.js', "export * from './" + paths.packageName + "';")
68 | );
69 | }
70 |
71 | function srcForTypeScript() {
72 | return gulp
73 | .src(paths.output + paths.packageName + '.js')
74 | .pipe(rename(function (path) {
75 | if (path.extname == '.js') {
76 | path.extname = '.ts';
77 | }
78 | }));
79 | }
80 |
81 | compileToModules.forEach(function(moduleType){
82 | gulp.task('build-babel-' + moduleType, function () {
83 | return srcForBabel()
84 | .pipe(to5(assign({}, compilerOptions[moduleType]())))
85 | .pipe(cleanGeneratedCode())
86 | .pipe(gulp.dest(paths.output + moduleType));
87 | });
88 |
89 | if (moduleType === 'native-modules') return; // typescript doesn't support the combination of: es5 + native modules
90 |
91 | gulp.task('build-ts-' + moduleType, function () {
92 | var tsProject = ts.createProject(
93 | compilerTsOptions({ module: moduleType, target: moduleType == 'es2015' ? 'es2015' : 'es5' }), ts.reporter.defaultReporter());
94 | var tsResult = srcForTypeScript().pipe(ts(tsProject));
95 | return tsResult.js
96 | .pipe(gulp.dest(paths.output + moduleType));
97 | });
98 | });
99 |
100 | gulp.task('build-dts', function() {
101 | var tsProject = ts.createProject(
102 | compilerTsOptions({ removeComments: false, target: "es2015", module: "es2015" }), ts.reporter.defaultReporter());
103 | var tsResult = srcForTypeScript().pipe(ts(tsProject));
104 | return tsResult.dts
105 | .pipe(gulp.dest(paths.output));
106 | });
107 |
108 | gulp.task('build', function(callback) {
109 | return runSequence(
110 | 'clean',
111 | 'build-index',
112 | compileToModules
113 | .map(function(moduleType) { return 'build-babel-' + moduleType })
114 | .concat(paths.useTypeScriptForDTS ? ['build-dts'] : []),
115 | callback
116 | );
117 | });
118 |
119 | gulp.task('build-ts', function(callback) {
120 | return runSequence(
121 | 'clean',
122 | 'build-index',
123 | 'build-babel-native-modules',
124 | compileToModules
125 | .filter(function(moduleType) { return moduleType !== 'native-modules' })
126 | .map(function(moduleType) { return 'build-ts-' + moduleType })
127 | .concat(paths.useTypeScriptForDTS ? ['build-dts'] : []),
128 | callback
129 | );
130 | });
131 |
--------------------------------------------------------------------------------
/build/tasks/clean.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var paths = require('../paths');
3 | var del = require('del');
4 | var vinylPaths = require('vinyl-paths');
5 |
6 | gulp.task('clean', function() {
7 | return gulp.src([paths.output])
8 | .pipe(vinylPaths(del));
9 | });
10 |
--------------------------------------------------------------------------------
/build/tasks/dev.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var tools = require('aurelia-tools');
3 |
4 | gulp.task('update-own-deps', function(){
5 | tools.updateOwnDependenciesFromLocalRepositories();
6 | });
7 |
8 | gulp.task('build-dev-env', function () {
9 | tools.buildDevEnv();
10 | });
11 |
--------------------------------------------------------------------------------
/build/tasks/doc.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var paths = require('../paths');
3 | var typedoc = require('gulp-typedoc');
4 | var runSequence = require('run-sequence');
5 | var through2 = require('through2');
6 |
7 | gulp.task('doc-generate', function(){
8 | return gulp.src([paths.output + paths.packageName + '.d.ts'])
9 | .pipe(typedoc({
10 | target: 'es6',
11 | includeDeclarations: true,
12 | moduleResolution: 'node',
13 | json: paths.doc + '/api.json',
14 | name: paths.packageName + '-docs',
15 | mode: 'modules',
16 | excludeExternals: true,
17 | ignoreCompilerErrors: false,
18 | version: true
19 | }));
20 | });
21 |
22 | gulp.task('doc-shape', function(){
23 | return gulp.src([paths.doc + '/api.json'])
24 | .pipe(through2.obj(function(file, enc, callback) {
25 | var json = JSON.parse(file.contents.toString('utf8')).children[0];
26 |
27 | json = {
28 | name: paths.packageName,
29 | children: json.children,
30 | groups: json.groups
31 | };
32 |
33 | file.contents = new Buffer(JSON.stringify(json));
34 | this.push(file);
35 | return callback();
36 | }))
37 | .pipe(gulp.dest(paths.doc));
38 | });
39 |
40 | gulp.task('doc', function(callback){
41 | return runSequence(
42 | 'doc-generate',
43 | 'doc-shape',
44 | callback
45 | );
46 | });
47 |
--------------------------------------------------------------------------------
/build/tasks/lint.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var paths = require('../paths');
3 | var eslint = require('gulp-eslint');
4 |
5 | gulp.task('lint', function() {
6 | return gulp.src(paths.source)
7 | .pipe(eslint())
8 | .pipe(eslint.format())
9 | .pipe(eslint.failAfterError());
10 | });
11 |
--------------------------------------------------------------------------------
/build/tasks/prepare-release.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var runSequence = require('run-sequence');
3 | var paths = require('../paths');
4 | var fs = require('fs');
5 | var bump = require('gulp-bump');
6 | var args = require('../args');
7 | var conventionalChangelog = require('gulp-conventional-changelog');
8 |
9 | gulp.task('changelog', function () {
10 | return gulp.src(paths.doc + '/CHANGELOG.md', {
11 | buffer: false
12 | }).pipe(conventionalChangelog({
13 | preset: 'angular'
14 | }))
15 | .pipe(gulp.dest(paths.doc));
16 | });
17 |
18 | gulp.task('bump-version', function(){
19 | return gulp.src(['./package.json', './bower.json'])
20 | .pipe(bump({type:args.bump })) //major|minor|patch|prerelease
21 | .pipe(gulp.dest('./'));
22 | });
23 |
24 | gulp.task('prepare-release', function(callback){
25 | return runSequence(
26 | 'build',
27 | 'lint',
28 | 'bump-version',
29 | 'doc',
30 | 'changelog',
31 | callback
32 | );
33 | });
34 |
--------------------------------------------------------------------------------
/build/tasks/test.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var karma = require('karma');
3 | var coveralls = require('gulp-coveralls');
4 |
5 | /**
6 | * Run test once and exit
7 | */
8 | gulp.task('test', function (done) {
9 | new karma.Server({
10 | configFile: __dirname + '/../../karma.conf.js',
11 | singleRun: true
12 | }, function(e) {
13 | done(e === 0 ? null : 'karma exited with status ' + e);
14 | }).start();
15 | });
16 |
17 | /**
18 | * Watch for file changes and re-run tests on each change
19 | */
20 | gulp.task('tdd', function (done) {
21 | new karma.Server({
22 | configFile: __dirname + '/../../karma.conf.js'
23 | }, function(e) {
24 | done();
25 | }).start();
26 | });
27 |
28 | /**
29 | * Report coverage to coveralls
30 | */
31 | gulp.task('coveralls', ['test'], function (done) {
32 | gulp.src('build/reports/coverage/lcov/report-lcovonly.txt')
33 | .pipe(coveralls());
34 | });
35 |
36 |
37 | /**
38 | * Run test once with code coverage and exit
39 | */
40 | gulp.task('cover', function (done) {
41 | new karma.Server({
42 | configFile: __dirname + '/../../karma.conf.js',
43 | singleRun: true,
44 | reporters: ['progress', 'coverage'],
45 | preprocessors: {
46 | 'test/**/*.js': ['babel'],
47 | 'src/**/*.js': ['babel', 'coverage']
48 | },
49 | coverageReporter: {
50 | dir: 'build/reports/coverage',
51 | reporters: [
52 | { type: 'html', subdir: 'report-html' },
53 | { type: 'json', subdir: '.', file: 'coverage-final.json' }
54 | ]
55 | }
56 | }, done).start();
57 | });
58 |
--------------------------------------------------------------------------------
/build/typescript-options.js:
--------------------------------------------------------------------------------
1 | var tsconfig = require('../tsconfig.json');
2 | var assign = Object.assign || require('object.assign');
3 |
4 | module.exports = function(override) {
5 | return assign(tsconfig.compilerOptions, {
6 | "target": override && override.target || "es5",
7 | "typescript": require('typescript')
8 | }, override || {});
9 | }
10 |
--------------------------------------------------------------------------------
/doc/MAINTAINER.md:
--------------------------------------------------------------------------------
1 | ## Workflow releasing a new version
2 |
3 | 1. Update: pull latest master with `git pull`
4 | 2. Cut release: Run `npm run cut-release`. Example:
5 |
6 | ```shell
7 | npm run cut-release
8 | # intentionally a minor release
9 | npm run cut-release -- -- --release-as minor
10 | ```
11 | 3. Commit: `git add .` and then `git commit chore(release): prepare release vXXX` where `XXX` is the new version
12 | 4. Tag: `git tag -a vXXX -m 'prepare release XXX` where `XXX` is the version
13 | 5. Push to remote repo: `git push --follow-tags`
14 | 6. Publish: Run `npm publish` to release the new version
15 |
--------------------------------------------------------------------------------
/doc/cleanup.js:
--------------------------------------------------------------------------------
1 | const path = require('path').resolve(__dirname, 'api.json');
2 | const content = JSON.stringify(require('./api.json'));
3 |
4 | require('fs').writeFileSync(path, content, { encoding: 'utf-8' });
5 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = function(config) {
4 | const browsers = config.browsers;
5 | config.set({
6 |
7 | basePath: '',
8 | frameworks: ["jasmine"],
9 | files: ["test/**/*.spec.ts"],
10 | preprocessors: {
11 | "test/**/*.spec.ts": ["webpack", 'sourcemap']
12 | },
13 | webpack: {
14 | mode: "development",
15 | entry: 'test/setup.ts',
16 | resolve: {
17 | extensions: [".ts", ".js"],
18 | modules: ["node_modules"],
19 | alias: {
20 | src: path.resolve(__dirname, 'src'),
21 | test: path.resolve(__dirname, 'test')
22 | }
23 | },
24 | devtool: browsers.includes('ChromeDebugging') ? 'eval-source-map' : 'inline-source-map',
25 | module: {
26 | rules: [
27 | {
28 | test: /\.ts$/,
29 | loader: "ts-loader",
30 | exclude: /node_modules/
31 | }
32 | ]
33 | }
34 | },
35 | mime: {
36 | "text/x-typescript": ["ts"]
37 | },
38 | reporters: ["mocha"],
39 | webpackServer: { noInfo: config.noInfo },
40 | browsers: Array.isArray(browsers) && browsers.length > 0 ? browsers : ['ChromeHeadless'],
41 | customLaunchers: {
42 | ChromeDebugging: {
43 | base: 'Chrome',
44 | flags: [
45 | '--remote-debugging-port=9333'
46 | ],
47 | debug: true
48 | }
49 | },
50 | mochaReporter: {
51 | ignoreSkipped: true
52 | },
53 | singleRun: false
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aurelia-router",
3 | "version": "1.7.2",
4 | "description": "A powerful client-side router.",
5 | "keywords": [
6 | "aurelia",
7 | "router"
8 | ],
9 | "homepage": "http://aurelia.io",
10 | "bugs": {
11 | "url": "https://github.com/aurelia/router/issues"
12 | },
13 | "license": "MIT",
14 | "author": "Rob Eisenberg (http://robeisenberg.com/)",
15 | "main": "dist/commonjs/aurelia-router.js",
16 | "module": "dist/native-modules/aurelia-router.js",
17 | "typings": "dist/aurelia-router.d.ts",
18 | "repository": {
19 | "type": "git",
20 | "url": "http://github.com/aurelia/router"
21 | },
22 | "files": [
23 | "dist",
24 | "src",
25 | "doc/CHANGELOG.md",
26 | "typings.json"
27 | ],
28 | "scripts": {
29 | "start": "npm run dev -- --format es2015",
30 | "dev": "node build/scripts/dev",
31 | "build": "node build/scripts/build",
32 | "build:dts": "dts-bundle-generator src/aurelia-router.ts -o dist/aurelia-router.d.ts",
33 | "test": "karma start --single-run",
34 | "test:watch": "karma start",
35 | "test:debugger": "karma start --browsers ChromeDebugging",
36 | "lint": "eslint .",
37 | "typedoc": "typedoc src/aurelia-router.ts --json doc/api.json",
38 | "posttypedoc": "node doc/cleanup.js",
39 | "changelog": "standard-version -t \"\" -i doc/CHANGELOG.md --skip.commit --skip.tag",
40 | "precut-release": "npm run lint && npm run test && npm run build",
41 | "cut-release": "npm run changelog",
42 | "postcut-release": "npm run typedoc"
43 | },
44 | "dependencies": {
45 | "aurelia-dependency-injection": "^1.0.0",
46 | "aurelia-event-aggregator": "^1.0.0",
47 | "aurelia-history": "^1.1.0",
48 | "aurelia-logging": "^1.0.0",
49 | "aurelia-path": "^1.1.7",
50 | "aurelia-route-recognizer": "^1.3.2"
51 | },
52 | "devDependencies": {
53 | "@rollup/plugin-typescript": "^8.3.1",
54 | "@types/estree": "^0.0.51",
55 | "@types/jasmine": "^4.0.2",
56 | "@typescript-eslint/eslint-plugin": "^5.19.0",
57 | "@typescript-eslint/parser": "^5.19.0",
58 | "aurelia-framework": "^1.4.1",
59 | "aurelia-pal-browser": "^1.8.1",
60 | "aurelia-polyfills": "^1.3.4",
61 | "aurelia-tools": "0.2.4",
62 | "dts-bundle-generator": "^6.7.0",
63 | "eslint": "^8.13.0",
64 | "gulp-watch": "^5.0.1",
65 | "jasmine-core": "^3.99.1",
66 | "karma": "^6.3.17",
67 | "karma-chrome-launcher": "^3.1.1",
68 | "karma-jasmine": "^4.0.2",
69 | "karma-mocha-reporter": "^2.2.5",
70 | "karma-sourcemap-loader": "^0.3.8",
71 | "karma-webpack": "^5.0.0",
72 | "rimraf": "^3.0.2",
73 | "rollup": "^2.70.1",
74 | "standard-version": "^9.3.2",
75 | "ts-loader": "^9.2.8",
76 | "tslib": "^2.3.1",
77 | "typedoc": "^0.22.15",
78 | "typescript": "^4.6.3",
79 | "webpack": "^5.72.0",
80 | "yargs": "^17.4.1"
81 | },
82 | "peerDependencies": {
83 | "aurelia-history": "^1.1.0"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/activation-strategy.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * An optional interface describing the available activation strategies.
3 | * @internal Used internally.
4 | */
5 | export const enum InternalActivationStrategy {
6 | /**
7 | * Reuse the existing view model, without invoking Router lifecycle hooks.
8 | */
9 | NoChange = 'no-change',
10 | /**
11 | * Reuse the existing view model, invoking Router lifecycle hooks.
12 | */
13 | InvokeLifecycle = 'invoke-lifecycle',
14 | /**
15 | * Replace the existing view model, invoking Router lifecycle hooks.
16 | */
17 | Replace = 'replace'
18 | }
19 |
20 | /**
21 | * The strategy to use when activating modules during navigation.
22 | */
23 | // kept for compat reason
24 | export const activationStrategy: ActivationStrategy = {
25 | noChange: InternalActivationStrategy.NoChange,
26 | invokeLifecycle: InternalActivationStrategy.InvokeLifecycle,
27 | replace: InternalActivationStrategy.Replace
28 | };
29 |
30 | /**
31 | * An optional interface describing the available activation strategies.
32 | */
33 | export interface ActivationStrategy {
34 | /**
35 | * Reuse the existing view model, without invoking Router lifecycle hooks.
36 | */
37 | noChange: 'no-change';
38 | /**
39 | * Reuse the existing view model, invoking Router lifecycle hooks.
40 | */
41 | invokeLifecycle: 'invoke-lifecycle';
42 | /**
43 | * Replace the existing view model, invoking Router lifecycle hooks.
44 | */
45 | replace: 'replace';
46 | }
47 |
48 | /**
49 | * Enum like type for activation strategy built-in values
50 | */
51 | export type ActivationStrategyType = ActivationStrategy[keyof ActivationStrategy];
52 |
--------------------------------------------------------------------------------
/src/app-router.ts:
--------------------------------------------------------------------------------
1 | import * as LogManager from 'aurelia-logging';
2 | import { Container } from 'aurelia-dependency-injection';
3 | import { History, NavigationOptions } from 'aurelia-history';
4 | import { Router } from './router';
5 | import { PipelineProvider } from './pipeline-provider';
6 | import { isNavigationCommand } from './navigation-commands';
7 | import { EventAggregator } from 'aurelia-event-aggregator';
8 | import { NavigationInstruction } from './navigation-instruction';
9 | import { ViewPort, ConfiguresRouter, PipelineResult } from './interfaces';
10 | import { RouterEvent } from './router-event';
11 |
12 | /**@internal */
13 | declare module 'aurelia-dependency-injection' {
14 | interface Container {
15 | viewModel?: any;
16 | }
17 | }
18 |
19 | const logger = LogManager.getLogger('app-router');
20 |
21 | /**
22 | * The main application router.
23 | */
24 | export class AppRouter extends Router {
25 |
26 | /**@internal */
27 | static inject() { return [Container, History, PipelineProvider, EventAggregator]; }
28 |
29 | events: EventAggregator;
30 | /**@internal */
31 | maxInstructionCount: number;
32 | /**@internal */
33 | _queue: NavigationInstruction[];
34 | /**@internal */
35 | isActive: boolean;
36 |
37 | constructor(container: Container, history: History, pipelineProvider: PipelineProvider, events: EventAggregator) {
38 | super(container, history); // Note the super will call reset internally.
39 | this.pipelineProvider = pipelineProvider;
40 | this.events = events;
41 | }
42 |
43 | /**
44 | * Fully resets the router's internal state. Primarily used internally by the framework when multiple calls to setRoot are made.
45 | * Use with caution (actually, avoid using this). Do not use this to simply change your navigation model.
46 | */
47 | reset(): void {
48 | super.reset();
49 | this.maxInstructionCount = 10;
50 | if (!this._queue) {
51 | this._queue = [];
52 | } else {
53 | this._queue.length = 0;
54 | }
55 | }
56 |
57 | /**
58 | * Loads the specified URL.
59 | *
60 | * @param url The URL fragment to load.
61 | */
62 | loadUrl(url: string): Promise {
63 | return this
64 | ._createNavigationInstruction(url)
65 | .then(instruction => this._queueInstruction(instruction))
66 | .catch(error => {
67 | logger.error(error);
68 | restorePreviousLocation(this);
69 | });
70 | }
71 |
72 | /**
73 | * Registers a viewPort to be used as a rendering target for activated routes.
74 | *
75 | * @param viewPort The viewPort. This is typically a element in Aurelia default impl
76 | * @param name The name of the viewPort. 'default' if unspecified.
77 | */
78 | registerViewPort(viewPort: /*ViewPort*/ any, name?: string): Promise {
79 | // having strong typing without changing public API
80 | const $viewPort: ViewPort = viewPort;
81 | super.registerViewPort($viewPort, name);
82 |
83 | // beside adding viewport to the registry of this instance
84 | // AppRouter also configure routing/history to start routing functionality
85 | // There are situation where there are more than 1 element at root view
86 | // in that case, still only activate once via the following guard
87 | if (!this.isActive) {
88 | const viewModel = this._findViewModel($viewPort);
89 | if ('configureRouter' in viewModel) {
90 | // If there are more than one element at root view
91 | // use this flag to guard against configure method being invoked multiple times
92 | // this flag is set inside method configure
93 | if (!this.isConfigured) {
94 | // replace the real resolve with a noop to guarantee that any action in base class Router
95 | // won't resolve the configurePromise prematurely
96 | const resolveConfiguredPromise = this._resolveConfiguredPromise;
97 | this._resolveConfiguredPromise = () => {/**/};
98 | return this
99 | .configure(config =>
100 | Promise
101 | .resolve(viewModel.configureRouter(config, this))
102 | // an issue with configure interface. Should be fixed there
103 | // todo: fix this via configure interface in router
104 | .then(() => config) as any
105 | )
106 | .then(() => {
107 | this.activate();
108 | resolveConfiguredPromise();
109 | });
110 | }
111 | } else {
112 | this.activate();
113 | }
114 | }
115 | // when a viewport is added dynamically to a root view that is already activated
116 | // just process the navigation instruction
117 | else {
118 | this._dequeueInstruction();
119 | }
120 |
121 | return Promise.resolve();
122 | }
123 |
124 | /**
125 | * Activates the router. This instructs the router to begin listening for history changes and processing instructions.
126 | *
127 | * @params options The set of options to activate the router with.
128 | */
129 | activate(options?: NavigationOptions): void {
130 | if (this.isActive) {
131 | return;
132 | }
133 |
134 | this.isActive = true;
135 | // route handler property is responsible for handling url change
136 | // the interface of aurelia-history isn't clear on this perspective
137 | this.options = Object.assign({ routeHandler: this.loadUrl.bind(this) }, this.options, options);
138 | this.history.activate(this.options);
139 | this._dequeueInstruction();
140 | }
141 |
142 | /**
143 | * Deactivates the router.
144 | */
145 | deactivate(): void {
146 | this.isActive = false;
147 | this.history.deactivate();
148 | }
149 |
150 | /**@internal */
151 | _queueInstruction(instruction: NavigationInstruction): Promise {
152 | return new Promise((resolve) => {
153 | instruction.resolve = resolve;
154 | this._queue.unshift(instruction);
155 | this._dequeueInstruction();
156 | });
157 | }
158 |
159 | /**@internal */
160 | _dequeueInstruction(instructionCount = 0): Promise {
161 | return Promise.resolve().then(() => {
162 | if (this.isNavigating && !instructionCount) {
163 | // ts complains about inconsistent returns without void 0
164 | return void 0;
165 | }
166 |
167 | let instruction = this._queue.shift();
168 | this._queue.length = 0;
169 |
170 | if (!instruction) {
171 | // ts complains about inconsistent returns without void 0
172 | return void 0;
173 | }
174 |
175 | this.isNavigating = true;
176 |
177 | let navtracker: number = this.history.getState('NavigationTracker');
178 | let currentNavTracker = this.currentNavigationTracker;
179 |
180 | if (!navtracker && !currentNavTracker) {
181 | this.isNavigatingFirst = true;
182 | this.isNavigatingNew = true;
183 | } else if (!navtracker) {
184 | this.isNavigatingNew = true;
185 | } else if (!currentNavTracker) {
186 | this.isNavigatingRefresh = true;
187 | } else if (currentNavTracker < navtracker) {
188 | this.isNavigatingForward = true;
189 | } else if (currentNavTracker > navtracker) {
190 | this.isNavigatingBack = true;
191 | } if (!navtracker) {
192 | navtracker = Date.now();
193 | this.history.setState('NavigationTracker', navtracker);
194 | }
195 | this.currentNavigationTracker = navtracker;
196 |
197 | instruction.previousInstruction = this.currentInstruction;
198 |
199 | let maxInstructionCount = this.maxInstructionCount;
200 |
201 | if (!instructionCount) {
202 | this.events.publish(RouterEvent.Processing, { instruction });
203 | } else if (instructionCount === maxInstructionCount - 1) {
204 | logger.error(`${instructionCount + 1} navigation instructions have been attempted without success. Restoring last known good location.`);
205 | restorePreviousLocation(this);
206 | return this._dequeueInstruction(instructionCount + 1);
207 | } else if (instructionCount > maxInstructionCount) {
208 | throw new Error('Maximum navigation attempts exceeded. Giving up.');
209 | }
210 |
211 | let pipeline = this.pipelineProvider.createPipeline(!this.couldDeactivate);
212 |
213 | return pipeline
214 | .run(instruction)
215 | .then(result => processResult(instruction, result, instructionCount, this))
216 | .catch(error => {
217 | return { output: error instanceof Error ? error : new Error(error) } as PipelineResult;
218 | })
219 | .then(result => resolveInstruction(instruction, result, !!instructionCount, this));
220 | });
221 | }
222 |
223 | /**@internal */
224 | _findViewModel(viewPort: ViewPort): ConfiguresRouter | undefined {
225 | if (this.container.viewModel) {
226 | return this.container.viewModel;
227 | }
228 |
229 | if (viewPort.container) {
230 | let container = viewPort.container;
231 |
232 | while (container) {
233 | if (container.viewModel) {
234 | this.container.viewModel = container.viewModel;
235 | return container.viewModel;
236 | }
237 |
238 | container = container.parent;
239 | }
240 | }
241 |
242 | return undefined;
243 | }
244 | }
245 |
246 | const processResult = (
247 | instruction: NavigationInstruction,
248 | result: PipelineResult,
249 | instructionCount: number,
250 | router: AppRouter
251 | ): Promise => {
252 | if (!(result && 'completed' in result && 'output' in result)) {
253 | result = result || {} as PipelineResult;
254 | result.output = new Error(`Expected router pipeline to return a navigation result, but got [${JSON.stringify(result)}] instead.`);
255 | }
256 |
257 | let finalResult: PipelineResult = null;
258 | let navigationCommandResult = null;
259 | if (isNavigationCommand(result.output)) {
260 | navigationCommandResult = result.output.navigate(router);
261 | } else {
262 | finalResult = result;
263 |
264 | if (!result.completed) {
265 | if (result.output instanceof Error) {
266 | logger.error(result.output.toString());
267 | }
268 |
269 | restorePreviousLocation(router);
270 | }
271 | }
272 |
273 | return Promise.resolve(navigationCommandResult)
274 | .then(() => router._dequeueInstruction(instructionCount + 1))
275 | .then(innerResult => finalResult || innerResult || result);
276 | };
277 |
278 | const resolveInstruction = (
279 | instruction: NavigationInstruction,
280 | result: PipelineResult,
281 | isInnerInstruction: boolean,
282 | router: AppRouter
283 | ): PipelineResult => {
284 | instruction.resolve(result);
285 |
286 | let eventAggregator = router.events;
287 | let eventArgs = { instruction, result };
288 | if (!isInnerInstruction) {
289 | router.isNavigating = false;
290 | router.isExplicitNavigation = false;
291 | router.isExplicitNavigationBack = false;
292 | router.isNavigatingFirst = false;
293 | router.isNavigatingNew = false;
294 | router.isNavigatingRefresh = false;
295 | router.isNavigatingForward = false;
296 | router.isNavigatingBack = false;
297 | router.couldDeactivate = false;
298 |
299 | let eventName: string;
300 |
301 | if (result.output instanceof Error) {
302 | eventName = RouterEvent.Error;
303 | } else if (!result.completed) {
304 | eventName = RouterEvent.Canceled;
305 | } else {
306 | let queryString = instruction.queryString ? ('?' + instruction.queryString) : '';
307 | router.history.previousLocation = instruction.fragment + queryString;
308 | eventName = RouterEvent.Success;
309 | }
310 |
311 | eventAggregator.publish(eventName, eventArgs);
312 | eventAggregator.publish(RouterEvent.Complete, eventArgs);
313 | } else {
314 | eventAggregator.publish(RouterEvent.ChildComplete, eventArgs);
315 | }
316 |
317 | return result;
318 | };
319 |
320 | const restorePreviousLocation = (router: AppRouter): void => {
321 | let previousLocation = router.history.previousLocation;
322 | if (previousLocation) {
323 | router.navigate(previousLocation, { trigger: false, replace: true });
324 | } else if (router.fallbackRoute) {
325 | router.navigate(router.fallbackRoute, { trigger: true, replace: true });
326 | } else {
327 | logger.error('Router navigation failed, and no previous location or fallbackRoute could be restored.');
328 | }
329 | };
330 |
--------------------------------------------------------------------------------
/src/aurelia-router.ts:
--------------------------------------------------------------------------------
1 | export {
2 | RoutableComponentCanActivate,
3 | RoutableComponentActivate,
4 | RoutableComponentCanDeactivate,
5 | RoutableComponentDeactivate,
6 | RoutableComponentDetermineActivationStrategy,
7 | ConfiguresRouter,
8 | RouteConfig,
9 | NavigationResult,
10 | Next,
11 | PipelineResult,
12 | PipelineStep
13 | // following are excluded and wait for more proper chance to be introduced for stronger typings story
14 | // this is to avoid any typings issue for a long delayed release
15 | /**
16 | * ViewPort
17 | * ViewPortPlan
18 | * ViewPortInstruction
19 | * ViewPortComponent
20 | */
21 | } from './interfaces';
22 | export {
23 | IObservable,
24 | IObservableConfig
25 | } from './utilities-activation';
26 | export { AppRouter } from './app-router';
27 | export { NavModel } from './nav-model';
28 | export { Redirect, RedirectToRoute, NavigationCommand, isNavigationCommand } from './navigation-commands';
29 |
30 | export {
31 | NavigationInstruction,
32 | NavigationInstructionInit
33 | } from './navigation-instruction';
34 |
35 | export {
36 | ActivateNextStep,
37 | CanActivateNextStep,
38 | CanDeactivatePreviousStep,
39 | DeactivatePreviousStep
40 | } from './step-activation';
41 |
42 | export { CommitChangesStep } from './step-commit-changes';
43 | export { BuildNavigationPlanStep } from './step-build-navigation-plan';
44 | export { LoadRouteStep } from './step-load-route';
45 |
46 | export {
47 | ActivationStrategy,
48 | activationStrategy
49 | } from './activation-strategy';
50 |
51 | export {
52 | PipelineStatus
53 | } from './pipeline-status';
54 |
55 | export {
56 | RouterEvent
57 | } from './router-event';
58 |
59 | export {
60 | PipelineSlotName
61 | } from './pipeline-slot-name';
62 |
63 | export { PipelineProvider } from './pipeline-provider';
64 | export { Pipeline } from './pipeline';
65 | export { RouteLoader } from './route-loader';
66 | export { RouterConfiguration } from './router-configuration';
67 | export { Router } from './router';
68 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { Container } from 'aurelia-dependency-injection';
2 | import { NavigationInstruction } from './navigation-instruction';
3 | import { Router } from './router';
4 | import { NavModel } from './nav-model';
5 | import { RouterConfiguration } from './router-configuration';
6 | import { NavigationCommand } from './navigation-commands';
7 | import { IObservable } from './utilities-activation';
8 | import { PipelineStatus } from './pipeline-status';
9 | import { ActivationStrategyType } from './activation-strategy';
10 |
11 | /**@internal */
12 | declare module 'aurelia-dependency-injection' {
13 | interface Container {
14 | getChildRouter?: () => Router;
15 | }
16 | }
17 |
18 | /**
19 | * A configuration object that describes a route for redirection
20 | */
21 | export interface RedirectConfig {
22 | /**
23 | * path that will be redirected to. This is relative to currently in process router
24 | */
25 | redirect: string;
26 | /**
27 | * A backward compat interface. Should be ignored in new code
28 | */
29 | [key: string]: any;
30 | }
31 |
32 | /**
33 | * A more generic RouteConfig for unknown route. Either a redirect config or a `RouteConfig`
34 | * Redirect config is generally used in `mapUnknownRoutes` of `RouterConfiguration`
35 | */
36 | export type RouteOrRedirectConfig = RouteConfig | RedirectConfig;
37 |
38 | /**
39 | * A RouteConfig specifier. Could be a string, or an object with `RouteConfig` interface shape,
40 | * or could be an object with redirect interface shape
41 | */
42 | export type RouteConfigSpecifier =
43 | string
44 | | RouteOrRedirectConfig
45 | | ((instruction: NavigationInstruction) => string | RouteOrRedirectConfig | Promise);
46 |
47 | /**
48 | * A configuration object that describes a route.
49 | */
50 | export interface RouteConfig {
51 | /**
52 | * The route pattern to match against incoming URL fragments, or an array of patterns.
53 | */
54 | route: string | string[];
55 |
56 | /**
57 | * A unique name for the route that may be used to identify the route when generating URL fragments.
58 | * Required when this route should support URL generation, such as with [[Router.generate]] or
59 | * the route-href custom attribute.
60 | */
61 | name?: string;
62 |
63 | /**
64 | * The moduleId of the view model that should be activated for this route.
65 | */
66 | moduleId?: string;
67 |
68 | /**
69 | * A URL fragment to redirect to when this route is matched.
70 | */
71 | redirect?: string;
72 |
73 | /**
74 | * A function that can be used to dynamically select the module or modules to activate.
75 | * The function is passed the current [[NavigationInstruction]], and should configure
76 | * instruction.config with the desired moduleId, viewPorts, or redirect.
77 | */
78 | navigationStrategy?: (instruction: NavigationInstruction) => Promise | void;
79 |
80 | /**
81 | * The view ports to target when activating this route. If unspecified, the target moduleId is loaded
82 | * into the default viewPort (the viewPort with name 'default'). The viewPorts object should have keys
83 | * whose property names correspond to names used by elements. The values should be objects
84 | * specifying the moduleId to load into that viewPort. The values may optionally include properties related to layout:
85 | * `layoutView`, `layoutViewModel` and `layoutModel`.
86 | */
87 | viewPorts?: any;
88 |
89 | /**
90 | * When specified, this route will be included in the [[Router.navigation]] nav model. Useful for
91 | * dynamically generating menus or other navigation elements. When a number is specified, that value
92 | * will be used as a sort order.
93 | */
94 | nav?: boolean | number;
95 |
96 | /**
97 | * The URL fragment to use in nav models. If unspecified, the [[RouteConfig.route]] will be used.
98 | * However, if the [[RouteConfig.route]] contains dynamic segments, this property must be specified.
99 | */
100 | href?: string;
101 |
102 | /**
103 | * Indicates that when route generation is done for this route, it should just take the literal value of the href property.
104 | */
105 | generationUsesHref?: boolean;
106 |
107 | /**
108 | * The document title to set when this route is active.
109 | */
110 | title?: string;
111 |
112 | /**
113 | * Arbitrary data to attach to the route. This can be used to attached custom data needed by components
114 | * like pipeline steps and activated modules.
115 | */
116 | settings?: any;
117 |
118 | /**
119 | * The navigation model for storing and interacting with the route's navigation settings.
120 | */
121 | navModel?: NavModel;
122 |
123 | /**
124 | * When true is specified, this route will be case sensitive.
125 | */
126 | caseSensitive?: boolean;
127 |
128 | /**
129 | * Add to specify an activation strategy if it is always the same and you do not want that
130 | * to be in your view-model code. Available values are 'replace' and 'invoke-lifecycle'.
131 | */
132 | activationStrategy?: ActivationStrategyType;
133 |
134 | /**
135 | * specifies the file name of a layout view to use.
136 | */
137 | layoutView?: string;
138 |
139 | /**
140 | * specifies the moduleId of the view model to use with the layout view.
141 | */
142 | layoutViewModel?: string;
143 |
144 | /**
145 | * specifies the model parameter to pass to the layout view model's `activate` function.
146 | */
147 | layoutModel?: any;
148 |
149 | /**
150 | * @internal
151 | */
152 | hasChildRouter?: boolean;
153 |
154 | [x: string]: any;
155 | }
156 |
157 | /**
158 | * An optional interface describing the canActivate convention.
159 | */
160 | export interface RoutableComponentCanActivate {
161 | /**
162 | * Implement this hook if you want to control whether or not your view-model can be navigated to.
163 | * Return a boolean value, a promise for a boolean value, or a navigation command.
164 | */
165 | canActivate(
166 | params: any,
167 | routeConfig: RouteConfig,
168 | navigationInstruction: NavigationInstruction
169 | ): boolean | Promise | PromiseLike | NavigationCommand | Promise | PromiseLike;
170 | }
171 |
172 | /**
173 | * An optional interface describing the activate convention.
174 | */
175 | export interface RoutableComponentActivate {
176 | /**
177 | * Implement this hook if you want to perform custom logic just before your view-model is displayed.
178 | * You can optionally return a promise to tell the router to wait to bind and attach the view until
179 | * after you finish your work.
180 | */
181 | activate(params: any, routeConfig: RouteConfig, navigationInstruction: NavigationInstruction): Promise | PromiseLike | IObservable | void;
182 | }
183 |
184 | /**
185 | * An optional interface describing the canDeactivate convention.
186 | */
187 | export interface RoutableComponentCanDeactivate {
188 | /**
189 | * Implement this hook if you want to control whether or not the router can navigate away from your
190 | * view-model when moving to a new route. Return a boolean value, a promise for a boolean value,
191 | * or a navigation command.
192 | */
193 | canDeactivate: () => boolean | Promise | PromiseLike | NavigationCommand;
194 | }
195 |
196 | /**
197 | * An optional interface describing the deactivate convention.
198 | */
199 | export interface RoutableComponentDeactivate {
200 | /**
201 | * Implement this hook if you want to perform custom logic when your view-model is being
202 | * navigated away from. You can optionally return a promise to tell the router to wait until
203 | * after you finish your work.
204 | */
205 | deactivate: () => Promise | PromiseLike | IObservable | void;
206 | }
207 |
208 | /**
209 | * An optional interface describing the determineActivationStrategy convention.
210 | */
211 | export interface RoutableComponentDetermineActivationStrategy {
212 | /**
213 | * Implement this hook if you want to give hints to the router about the activation strategy, when reusing
214 | * a view model for different routes. Available values are 'replace' and 'invoke-lifecycle'.
215 | */
216 | determineActivationStrategy(params: any, routeConfig: RouteConfig, navigationInstruction: NavigationInstruction): ActivationStrategyType;
217 | }
218 |
219 | /**
220 | * An optional interface describing the router configuration convention.
221 | */
222 | export interface ConfiguresRouter {
223 | /**
224 | * Implement this hook if you want to configure a router.
225 | */
226 | configureRouter(config: RouterConfiguration, router: Router): Promise | PromiseLike | void;
227 | }
228 |
229 | /**
230 | * A step to be run during processing of the pipeline.
231 | */
232 | export interface PipelineStep {
233 | /**
234 | * Execute the pipeline step. The step should invoke next(), next.complete(),
235 | * next.cancel(), or next.reject() to allow the pipeline to continue.
236 | *
237 | * @param instruction The navigation instruction.
238 | * @param next The next step in the pipeline.
239 | */
240 | run(instruction: NavigationInstruction, next: Next): Promise;
241 |
242 | /**
243 | * @internal
244 | */
245 | getSteps?(): any[];
246 | }
247 |
248 | /**
249 | * A multi-step pipeline step that helps enable multiple hooks to the pipeline
250 | */
251 | export interface IPipelineSlot {
252 | /**@internal */
253 | getSteps(): (StepRunnerFunction | IPipelineSlot | PipelineStep)[];
254 | }
255 |
256 | /**
257 | * The result of a pipeline run.
258 | */
259 | export interface PipelineResult {
260 | status: string;
261 | instruction: NavigationInstruction;
262 | output: any;
263 | completed: boolean;
264 | }
265 |
266 | /**
267 | * The component responsible for routing
268 | */
269 | export interface ViewPortComponent {
270 | viewModel: any;
271 | childContainer?: Container;
272 | router: Router;
273 | config?: RouteConfig;
274 | childRouter?: Router;
275 | /**
276 | * This is for backward compat, when moving from any to a more strongly typed interface
277 | */
278 | [key: string]: any;
279 | }
280 |
281 | /**
282 | * A viewport used by a Router to render a route config
283 | */
284 | export interface ViewPort {
285 | /**@internal */
286 | container: Container;
287 | swap(viewportInstruction: ViewPortInstruction): void;
288 | process(viewportInstruction: ViewPortInstruction, waitToSwap?: boolean): Promise;
289 | }
290 |
291 | /**
292 | * A viewport plan to create/update a viewport.
293 | */
294 | export interface ViewPortPlan {
295 | name: string;
296 | config: RouteConfig;
297 | strategy: ActivationStrategyType;
298 |
299 | prevComponent?: ViewPortComponent;
300 | prevModuleId?: string;
301 | childNavigationInstruction?: NavigationInstruction;
302 | }
303 |
304 | export interface ViewPortInstruction {
305 |
306 | name?: string;
307 |
308 | strategy: ActivationStrategyType;
309 |
310 | childNavigationInstruction?: NavigationInstruction;
311 |
312 | moduleId: string;
313 |
314 | component: ViewPortComponent;
315 |
316 | childRouter?: Router;
317 |
318 | lifecycleArgs: LifecycleArguments;
319 |
320 | prevComponent?: ViewPortComponent;
321 | }
322 |
323 | export type NavigationResult = boolean | Promise;
324 |
325 | export type LifecycleArguments = [Record, RouteConfig, NavigationInstruction];
326 |
327 | /**
328 | * A callback to indicate when pipeline processing should advance to the next step
329 | * or be aborted.
330 | */
331 | export interface Next {
332 | /**
333 | * Indicates the successful completion of the pipeline step.
334 | */
335 | (): Promise;
336 | /**
337 | * Indicates the successful completion of the entire pipeline.
338 | */
339 | complete: NextCompletionHandler;
340 |
341 | /**
342 | * Indicates that the pipeline should cancel processing.
343 | */
344 | cancel: NextCompletionHandler;
345 |
346 | /**
347 | * Indicates that pipeline processing has failed and should be stopped.
348 | */
349 | reject: NextCompletionHandler;
350 | }
351 |
352 | /**
353 | * Next Completion result. Comprises of final status, output (could be value/error) and flag `completed`
354 | */
355 | export interface NextCompletionResult {
356 | status: PipelineStatus;
357 | output: T;
358 | completed: boolean;
359 | }
360 |
361 | /**
362 | * Handler for resolving `NextCompletionResult`
363 | */
364 | export type NextCompletionHandler = (output?: T) => Promise>;
365 |
366 | export type StepRunnerFunction = (this: TThis, instruction: NavigationInstruction, next: Next) => any;
367 |
--------------------------------------------------------------------------------
/src/nav-model.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-inferrable-types */
2 | import { Router } from './router';
3 | import { RouteConfig } from './interfaces';
4 |
5 | /**
6 | * Class for storing and interacting with a route's navigation settings.
7 | */
8 | export class NavModel {
9 |
10 | /**
11 | * True if this nav item is currently active.
12 | */
13 | isActive: boolean = false;
14 |
15 | /**
16 | * The title.
17 | */
18 | title: string = null;
19 |
20 | /**
21 | * This nav item's absolute href.
22 | */
23 | href: string = null;
24 |
25 | /**
26 | * This nav item's relative href.
27 | */
28 | relativeHref: string = null;
29 |
30 | /**
31 | * Data attached to the route at configuration time.
32 | */
33 | settings: any = {};
34 |
35 | /**
36 | * The route config.
37 | */
38 | config: RouteConfig = null;
39 |
40 | /**
41 | * The router associated with this navigation model.
42 | */
43 | router: Router;
44 |
45 | order: number | boolean;
46 |
47 | constructor(router: Router, relativeHref: string) {
48 | this.router = router;
49 | this.relativeHref = relativeHref;
50 | }
51 |
52 | /**
53 | * Sets the route's title and updates document.title.
54 | * If the a navigation is in progress, the change will be applied
55 | * to document.title when the navigation completes.
56 | *
57 | * @param title The new title.
58 | */
59 | setTitle(title: string): void {
60 | this.title = title;
61 |
62 | if (this.isActive) {
63 | this.router.updateTitle();
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/navigation-commands.ts:
--------------------------------------------------------------------------------
1 | import { NavigationOptions } from 'aurelia-history';
2 | import { Router } from './router';
3 |
4 | /**@internal */
5 | declare module 'aurelia-history' {
6 | interface NavigationOptions {
7 | useAppRouter?: boolean;
8 | }
9 | }
10 |
11 | /**
12 | * When a navigation command is encountered, the current navigation
13 | * will be cancelled and control will be passed to the navigation
14 | * command so it can determine the correct action.
15 | */
16 | export interface NavigationCommand {
17 | navigate: (router: Router) => void;
18 | /**@internal */
19 | shouldContinueProcessing?: boolean;
20 | /**@internal */
21 | setRouter?: (router: Router) => void;
22 | }
23 |
24 | /**
25 | * Determines if the provided object is a navigation command.
26 | * A navigation command is anything with a navigate method.
27 | *
28 | * @param obj The object to check.
29 | */
30 | export function isNavigationCommand(obj: any): obj is NavigationCommand {
31 | return obj && typeof obj.navigate === 'function';
32 | }
33 |
34 | /**
35 | * Used during the activation lifecycle to cause a redirect.
36 | */
37 | export class Redirect implements NavigationCommand {
38 |
39 | url: string;
40 | /**@internal */
41 | options: NavigationOptions;
42 | /**@internal */
43 | shouldContinueProcessing: boolean;
44 |
45 | private router: Router;
46 |
47 | /**
48 | * @param url The URL fragment to use as the navigation destination.
49 | * @param options The navigation options.
50 | */
51 | constructor(url: string, options: NavigationOptions = {}) {
52 | this.url = url;
53 | this.options = Object.assign({ trigger: true, replace: true }, options);
54 | this.shouldContinueProcessing = false;
55 | }
56 |
57 | /**
58 | * Called by the activation system to set the child router.
59 | *
60 | * @param router The router.
61 | */
62 | setRouter(router: Router): void {
63 | this.router = router;
64 | }
65 |
66 | /**
67 | * Called by the navigation pipeline to navigate.
68 | *
69 | * @param appRouter The router to be redirected.
70 | */
71 | navigate(appRouter: Router): void {
72 | let navigatingRouter = this.options.useAppRouter ? appRouter : (this.router || appRouter);
73 | navigatingRouter.navigate(this.url, this.options);
74 | }
75 | }
76 |
77 | /**
78 | * Used during the activation lifecycle to cause a redirect to a named route.
79 | */
80 | export class RedirectToRoute implements NavigationCommand {
81 |
82 | route: string;
83 | params: any;
84 | /**@internal */
85 | options: NavigationOptions;
86 |
87 | /**@internal */
88 | shouldContinueProcessing: boolean;
89 |
90 | /**@internal */
91 | router: Router;
92 |
93 | /**
94 | * @param route The name of the route.
95 | * @param params The parameters to be sent to the activation method.
96 | * @param options The options to use for navigation.
97 | */
98 | constructor(route: string, params: any = {}, options: NavigationOptions = {}) {
99 | this.route = route;
100 | this.params = params;
101 | this.options = Object.assign({ trigger: true, replace: true }, options);
102 | this.shouldContinueProcessing = false;
103 | }
104 |
105 | /**
106 | * Called by the activation system to set the child router.
107 | *
108 | * @param router The router.
109 | */
110 | setRouter(router: Router): void {
111 | this.router = router;
112 | }
113 |
114 | /**
115 | * Called by the navigation pipeline to navigate.
116 | *
117 | * @param appRouter The router to be redirected.
118 | */
119 | navigate(appRouter: Router): void {
120 | let navigatingRouter = this.options.useAppRouter ? appRouter : (this.router || appRouter);
121 | navigatingRouter.navigateToRoute(this.route, this.params, this.options);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/navigation-instruction.ts:
--------------------------------------------------------------------------------
1 | import { ViewPortInstruction, RouteConfig, ViewPort, LifecycleArguments, ViewPortComponent } from './interfaces';
2 | import { Router } from './router';
3 | import { ActivationStrategyType, InternalActivationStrategy } from './activation-strategy';
4 |
5 | /**
6 | * Initialization options for a navigation instruction
7 | */
8 | export interface NavigationInstructionInit {
9 | fragment: string;
10 | queryString?: string;
11 | params?: Record;
12 | queryParams?: Record;
13 | config: RouteConfig;
14 | parentInstruction?: NavigationInstruction;
15 | previousInstruction?: NavigationInstruction;
16 | router: Router;
17 | options?: Object;
18 | plan?: Record;
19 | }
20 |
21 | export interface ViewPortInstructionInit {
22 | name: string;
23 | strategy: ActivationStrategyType;
24 | moduleId: string;
25 | component: ViewPortComponent;
26 | }
27 |
28 | /**
29 | * Class used to represent an instruction during a navigation.
30 | */
31 | export class NavigationInstruction {
32 | /**
33 | * The URL fragment.
34 | */
35 | fragment: string;
36 |
37 | /**
38 | * The query string.
39 | */
40 | queryString: string;
41 |
42 | /**
43 | * Parameters extracted from the route pattern.
44 | */
45 | params: any;
46 |
47 | /**
48 | * Parameters extracted from the query string.
49 | */
50 | queryParams: any;
51 |
52 | /**
53 | * The route config for the route matching this instruction.
54 | */
55 | config: RouteConfig;
56 |
57 | /**
58 | * The parent instruction, if this instruction was created by a child router.
59 | */
60 | parentInstruction: NavigationInstruction;
61 |
62 | parentCatchHandler: any;
63 |
64 | /**
65 | * The instruction being replaced by this instruction in the current router.
66 | */
67 | previousInstruction: NavigationInstruction;
68 |
69 | /**
70 | * viewPort instructions to used activation.
71 | */
72 | viewPortInstructions: Record;
73 |
74 | /**
75 | * The router instance.
76 | */
77 | router: Router;
78 |
79 | /**
80 | * Current built viewport plan of this nav instruction
81 | */
82 | plan: Record = null;
83 |
84 | options: Record = {};
85 |
86 | /**@internal */
87 | lifecycleArgs: LifecycleArguments;
88 | /**@internal */
89 | resolve?: (val?: any) => void;
90 |
91 | constructor(init: NavigationInstructionInit) {
92 | Object.assign(this, init);
93 |
94 | this.params = this.params || {};
95 | this.viewPortInstructions = {};
96 |
97 | let ancestorParams = [];
98 | // eslint-disable-next-line @typescript-eslint/no-this-alias
99 | let current: NavigationInstruction = this;
100 | do {
101 | let currentParams = Object.assign({}, current.params);
102 | if (current.config && current.config.hasChildRouter) {
103 | // remove the param for the injected child route segment
104 | delete currentParams[current.getWildCardName()];
105 | }
106 |
107 | ancestorParams.unshift(currentParams);
108 | current = current.parentInstruction;
109 | } while (current);
110 |
111 | let allParams = Object.assign({}, this.queryParams, ...ancestorParams);
112 | this.lifecycleArgs = [allParams, this.config, this];
113 | }
114 |
115 | /**
116 | * Gets an array containing this instruction and all child instructions for the current navigation.
117 | */
118 | getAllInstructions(): Array {
119 | let instructions: NavigationInstruction[] = [this];
120 | let viewPortInstructions: Record = this.viewPortInstructions;
121 |
122 | for (let key in viewPortInstructions) {
123 | let childInstruction = viewPortInstructions[key].childNavigationInstruction;
124 | if (childInstruction) {
125 | instructions.push(...childInstruction.getAllInstructions());
126 | }
127 | }
128 |
129 | return instructions;
130 | }
131 |
132 | /**
133 | * Gets an array containing the instruction and all child instructions for the previous navigation.
134 | * Previous instructions are no longer available after navigation completes.
135 | */
136 | getAllPreviousInstructions(): Array {
137 | return this.getAllInstructions().map(c => c.previousInstruction).filter(c => c);
138 | }
139 |
140 | /**
141 | * Adds a viewPort instruction. Returns the newly created instruction based on parameters
142 | */
143 | addViewPortInstruction(initOptions: ViewPortInstructionInit): /*ViewPortInstruction*/ any;
144 | addViewPortInstruction(viewPortName: string, strategy: ActivationStrategyType, moduleId: string, component: any): /*ViewPortInstruction*/ any;
145 | addViewPortInstruction(
146 | nameOrInitOptions: string | ViewPortInstructionInit,
147 | strategy?: ActivationStrategyType,
148 | moduleId?: string,
149 | component?: any
150 | ): /*ViewPortInstruction*/ any {
151 |
152 | let viewPortInstruction: ViewPortInstruction;
153 | let viewPortName = typeof nameOrInitOptions === 'string' ? nameOrInitOptions : nameOrInitOptions.name;
154 | const lifecycleArgs = this.lifecycleArgs;
155 | const config: RouteConfig = Object.assign({}, lifecycleArgs[1], { currentViewPort: viewPortName });
156 |
157 | if (typeof nameOrInitOptions === 'string') {
158 | viewPortInstruction = {
159 | name: nameOrInitOptions,
160 | strategy: strategy,
161 | moduleId: moduleId,
162 | component: component,
163 | childRouter: component.childRouter,
164 | lifecycleArgs: [lifecycleArgs[0], config, lifecycleArgs[2]] as LifecycleArguments
165 | };
166 | } else {
167 | viewPortInstruction = {
168 | name: viewPortName,
169 | strategy: nameOrInitOptions.strategy,
170 | component: nameOrInitOptions.component,
171 | moduleId: nameOrInitOptions.moduleId,
172 | childRouter: nameOrInitOptions.component.childRouter,
173 | lifecycleArgs: [lifecycleArgs[0], config, lifecycleArgs[2]] as LifecycleArguments
174 | };
175 | }
176 |
177 | return this.viewPortInstructions[viewPortName] = viewPortInstruction;
178 | }
179 |
180 | /**
181 | * Gets the name of the route pattern's wildcard parameter, if applicable.
182 | */
183 | getWildCardName(): string {
184 | // todo: potential issue, or at least unsafe typings
185 | let configRoute = this.config.route as string;
186 | let wildcardIndex = configRoute.lastIndexOf('*');
187 | return configRoute.substr(wildcardIndex + 1);
188 | }
189 |
190 | /**
191 | * Gets the path and query string created by filling the route
192 | * pattern's wildcard parameter with the matching param.
193 | */
194 | getWildcardPath(): string {
195 | let wildcardName = this.getWildCardName();
196 | let path = this.params[wildcardName] || '';
197 | let queryString = this.queryString;
198 |
199 | if (queryString) {
200 | path += '?' + queryString;
201 | }
202 |
203 | return path;
204 | }
205 |
206 | /**
207 | * Gets the instruction's base URL, accounting for wildcard route parameters.
208 | */
209 | getBaseUrl(): string {
210 | let $encodeURI = encodeURI;
211 | let fragment = decodeURI(this.fragment);
212 |
213 | if (fragment === '') {
214 | let nonEmptyRoute = this.router.routes.find(route => {
215 | return route.name === this.config.name &&
216 | route.route !== '';
217 | });
218 | if (nonEmptyRoute) {
219 | fragment = nonEmptyRoute.route as any;
220 | }
221 | }
222 |
223 | if (!this.params) {
224 | return $encodeURI(fragment);
225 | }
226 |
227 | let wildcardName = this.getWildCardName();
228 | let path = this.params[wildcardName] || '';
229 |
230 | if (!path) {
231 | return $encodeURI(fragment);
232 | }
233 |
234 | return $encodeURI(fragment.substr(0, fragment.lastIndexOf(path)));
235 | }
236 |
237 | /**
238 | * Finalize a viewport instruction
239 | * @internal
240 | */
241 | _commitChanges(waitToSwap: boolean): Promise {
242 | let router = this.router;
243 | router.currentInstruction = this;
244 |
245 | const previousInstruction = this.previousInstruction;
246 | if (previousInstruction) {
247 | previousInstruction.config.navModel.isActive = false;
248 | }
249 |
250 | this.config.navModel.isActive = true;
251 |
252 | router.refreshNavigation();
253 |
254 | let loads: Promise[] = [];
255 | let delaySwaps: ISwapPlan[] = [];
256 | let viewPortInstructions: Record = this.viewPortInstructions;
257 |
258 | for (let viewPortName in viewPortInstructions) {
259 | let viewPortInstruction = viewPortInstructions[viewPortName];
260 | let viewPort = router.viewPorts[viewPortName];
261 |
262 | if (!viewPort) {
263 | throw new Error(`There was no router-view found in the view for ${viewPortInstruction.moduleId}.`);
264 | }
265 |
266 | let childNavInstruction = viewPortInstruction.childNavigationInstruction;
267 | if (viewPortInstruction.strategy === InternalActivationStrategy.Replace) {
268 | if (childNavInstruction && childNavInstruction.parentCatchHandler) {
269 | loads.push(childNavInstruction._commitChanges(waitToSwap));
270 | } else {
271 | if (waitToSwap) {
272 | delaySwaps.push({ viewPort, viewPortInstruction });
273 | }
274 | loads.push(
275 | viewPort
276 | .process(viewPortInstruction, waitToSwap)
277 | .then(() => childNavInstruction
278 | ? childNavInstruction._commitChanges(waitToSwap)
279 | : Promise.resolve()
280 | )
281 | );
282 | }
283 | } else {
284 | if (childNavInstruction) {
285 | loads.push(childNavInstruction._commitChanges(waitToSwap));
286 | }
287 | }
288 | }
289 |
290 | return Promise
291 | .all(loads)
292 | .then(() => {
293 | delaySwaps.forEach(x => x.viewPort.swap(x.viewPortInstruction));
294 | return null;
295 | })
296 | .then(() => prune(this));
297 | }
298 |
299 | /**@internal */
300 | _updateTitle(): void {
301 | let router = this.router;
302 | let title = this._buildTitle(router.titleSeparator);
303 | if (title) {
304 | router.history.setTitle(title);
305 | }
306 | }
307 |
308 | /**@internal */
309 | _buildTitle(separator = ' | '): string {
310 | let title = '';
311 | let childTitles = [];
312 | let navModelTitle = this.config.navModel.title;
313 | let instructionRouter = this.router;
314 | let viewPortInstructions: Record = this.viewPortInstructions;
315 |
316 | if (navModelTitle) {
317 | title = instructionRouter.transformTitle(navModelTitle);
318 | }
319 |
320 | for (let viewPortName in viewPortInstructions) {
321 | let viewPortInstruction = viewPortInstructions[viewPortName];
322 | let child_nav_instruction = viewPortInstruction.childNavigationInstruction;
323 |
324 | if (child_nav_instruction) {
325 | let childTitle = child_nav_instruction._buildTitle(separator);
326 | if (childTitle) {
327 | childTitles.push(childTitle);
328 | }
329 | }
330 | }
331 |
332 | if (childTitles.length) {
333 | title = childTitles.join(separator) + (title ? separator : '') + title;
334 | }
335 |
336 | if (instructionRouter.title) {
337 | title += (title ? separator : '') + instructionRouter.transformTitle(instructionRouter.title);
338 | }
339 |
340 | return title;
341 | }
342 | }
343 |
344 | const prune = (instruction: NavigationInstruction): void => {
345 | instruction.previousInstruction = null;
346 | instruction.plan = null;
347 | };
348 |
349 | interface ISwapPlan {
350 | viewPort: ViewPort;
351 | viewPortInstruction: ViewPortInstruction;
352 | }
353 |
--------------------------------------------------------------------------------
/src/navigation-plan.ts:
--------------------------------------------------------------------------------
1 | import { ViewPortPlan, ViewPortInstruction, RouteConfig } from './interfaces';
2 | import { Redirect } from './navigation-commands';
3 | import { NavigationInstruction } from './navigation-instruction';
4 | import { InternalActivationStrategy, ActivationStrategyType } from './activation-strategy';
5 |
6 | type ViewPortPlansRecord = Record;
7 |
8 | /**
9 | * @internal exported for unit testing
10 | */
11 | export function _buildNavigationPlan(
12 | instruction: NavigationInstruction,
13 | forceLifecycleMinimum?: boolean
14 | ): Promise {
15 | let config = instruction.config;
16 |
17 | if ('redirect' in config) {
18 | return buildRedirectPlan(instruction);
19 | }
20 |
21 | const prevInstruction = instruction.previousInstruction;
22 | const defaultViewPortConfigs = instruction.router.viewPortDefaults;
23 |
24 | if (prevInstruction) {
25 | return buildTransitionPlans(instruction, prevInstruction, defaultViewPortConfigs, forceLifecycleMinimum);
26 | }
27 |
28 | // first navigation, only need to prepare a few information for each viewport plan
29 | const viewPortPlans: ViewPortPlansRecord = {};
30 | let viewPortConfigs = config.viewPorts;
31 | for (let viewPortName in viewPortConfigs) {
32 | let viewPortConfig = viewPortConfigs[viewPortName];
33 | if (viewPortConfig.moduleId === null && viewPortName in defaultViewPortConfigs) {
34 | viewPortConfig = defaultViewPortConfigs[viewPortName];
35 | }
36 | viewPortPlans[viewPortName] = {
37 | name: viewPortName,
38 | strategy: InternalActivationStrategy.Replace,
39 | config: viewPortConfig
40 | };
41 | }
42 |
43 | return Promise.resolve(viewPortPlans);
44 | }
45 |
46 | /**
47 | * Build redirect plan based on config of a navigation instruction
48 | * @internal exported for unit testing
49 | */
50 | export const buildRedirectPlan = (instruction: NavigationInstruction) => {
51 | const config = instruction.config;
52 | const router = instruction.router;
53 | return router
54 | ._createNavigationInstruction(config.redirect)
55 | .then(redirectInstruction => {
56 |
57 | const params: Record = {};
58 | const originalInstructionParams = instruction.params;
59 | const redirectInstructionParams = redirectInstruction.params;
60 |
61 | for (let key in redirectInstructionParams) {
62 | // If the param on the redirect points to another param, e.g. { route: first/:this, redirect: second/:this }
63 | let val = redirectInstructionParams[key];
64 | if (typeof val === 'string' && val[0] === ':') {
65 | val = val.slice(1);
66 | // And if that param is found on the original instruction then use it
67 | if (val in originalInstructionParams) {
68 | params[key] = originalInstructionParams[val];
69 | }
70 | } else {
71 | params[key] = redirectInstructionParams[key];
72 | }
73 | }
74 | let redirectLocation = router.generate(redirectInstruction.config, params, instruction.options);
75 |
76 | // Special handling for child routes
77 | for (let key in originalInstructionParams) {
78 | redirectLocation = redirectLocation.replace(`:${key}`, originalInstructionParams[key]);
79 | }
80 |
81 | let queryString = instruction.queryString;
82 | if (queryString) {
83 | redirectLocation += '?' + queryString;
84 | }
85 |
86 | return Promise.resolve(new Redirect(redirectLocation));
87 | });
88 | };
89 |
90 | /**
91 | * @param viewPortPlans the Plan record that holds information about built plans
92 | * @internal exported for unit testing
93 | */
94 | export const buildTransitionPlans = (
95 | currentInstruction: NavigationInstruction,
96 | previousInstruction: NavigationInstruction,
97 | defaultViewPortConfigs: Record,
98 | forceLifecycleMinimum?: boolean
99 | ): Promise => {
100 |
101 | let viewPortPlans: ViewPortPlansRecord = {};
102 | let newInstructionConfig = currentInstruction.config;
103 | let hasNewParams = hasDifferentParameterValues(previousInstruction, currentInstruction);
104 | let pending: Promise[] = [];
105 | let previousViewPortInstructions = previousInstruction.viewPortInstructions as Record;
106 |
107 | for (let viewPortName in previousViewPortInstructions) {
108 |
109 | const prevViewPortInstruction = previousViewPortInstructions[viewPortName];
110 | const prevViewPortComponent = prevViewPortInstruction.component;
111 | const newInstructionViewPortConfigs = newInstructionConfig.viewPorts as Record;
112 |
113 | // if this is invoked on a viewport without any changes, based on new url,
114 | // newViewPortConfig will be the existing viewport instruction
115 | let nextViewPortConfig = viewPortName in newInstructionViewPortConfigs
116 | ? newInstructionViewPortConfigs[viewPortName]
117 | : prevViewPortInstruction;
118 |
119 | if (nextViewPortConfig.moduleId === null && viewPortName in defaultViewPortConfigs) {
120 | nextViewPortConfig = defaultViewPortConfigs[viewPortName];
121 | }
122 |
123 | const viewPortActivationStrategy = determineActivationStrategy(
124 | currentInstruction,
125 | prevViewPortInstruction,
126 | nextViewPortConfig,
127 | hasNewParams,
128 | forceLifecycleMinimum
129 | );
130 | const viewPortPlan = viewPortPlans[viewPortName] = {
131 | name: viewPortName,
132 | // ViewPortInstruction can quack like a RouteConfig
133 | config: nextViewPortConfig as RouteConfig,
134 | prevComponent: prevViewPortComponent,
135 | prevModuleId: prevViewPortInstruction.moduleId,
136 | strategy: viewPortActivationStrategy
137 | } as ViewPortPlan;
138 |
139 | // recursively build nav plans for all existing child routers/viewports of this viewport
140 | // this is possible because existing child viewports and routers already have necessary information
141 | // to process the wildcard path from parent instruction
142 | if (viewPortActivationStrategy !== InternalActivationStrategy.Replace && prevViewPortInstruction.childRouter) {
143 | const path = currentInstruction.getWildcardPath();
144 | const task: Promise = prevViewPortInstruction
145 | .childRouter
146 | ._createNavigationInstruction(path, currentInstruction)
147 | .then((childInstruction: NavigationInstruction) => {
148 | viewPortPlan.childNavigationInstruction = childInstruction;
149 |
150 | return _buildNavigationPlan(
151 | childInstruction,
152 | // is it safe to assume viewPortPlan has not been changed from previous assignment?
153 | // if so, can just use local variable viewPortPlanStrategy
154 | // there could be user code modifying viewport plan during _createNavigationInstruction?
155 | viewPortPlan.strategy === InternalActivationStrategy.InvokeLifecycle
156 | )
157 | .then(childPlan => {
158 | if (childPlan instanceof Redirect) {
159 | return Promise.reject(childPlan);
160 | }
161 | childInstruction.plan = childPlan;
162 | // for bluebird ?
163 | return null;
164 | });
165 | });
166 |
167 | pending.push(task);
168 | }
169 | }
170 |
171 | return Promise.all(pending).then(() => viewPortPlans);
172 | };
173 |
174 | /**
175 | * @param newViewPortConfig if this is invoked on a viewport without any changes, based on new url, newViewPortConfig will be the existing viewport instruction
176 | * @internal exported for unit testing
177 | */
178 | export const determineActivationStrategy = (
179 | currentNavInstruction: NavigationInstruction,
180 | prevViewPortInstruction: ViewPortInstruction,
181 | newViewPortConfig: RouteConfig | ViewPortInstruction,
182 | // indicates whether there is difference between old and new url params
183 | hasNewParams: boolean,
184 | forceLifecycleMinimum?: boolean
185 | ): ActivationStrategyType => {
186 |
187 | let newInstructionConfig = currentNavInstruction.config;
188 | let prevViewPortViewModel = prevViewPortInstruction.component.viewModel;
189 | let viewPortPlanStrategy: ActivationStrategyType;
190 |
191 | if (prevViewPortInstruction.moduleId !== newViewPortConfig.moduleId) {
192 | viewPortPlanStrategy = InternalActivationStrategy.Replace;
193 | } else if ('determineActivationStrategy' in prevViewPortViewModel) {
194 | viewPortPlanStrategy = prevViewPortViewModel.determineActivationStrategy(...currentNavInstruction.lifecycleArgs);
195 | } else if (newInstructionConfig.activationStrategy) {
196 | viewPortPlanStrategy = newInstructionConfig.activationStrategy;
197 | } else if (hasNewParams || forceLifecycleMinimum) {
198 | viewPortPlanStrategy = InternalActivationStrategy.InvokeLifecycle;
199 | } else {
200 | viewPortPlanStrategy = InternalActivationStrategy.NoChange;
201 | }
202 | return viewPortPlanStrategy;
203 | };
204 |
205 | /**@internal exported for unit testing */
206 | export const hasDifferentParameterValues = (prev: NavigationInstruction, next: NavigationInstruction): boolean => {
207 | let prevParams = prev.params;
208 | let nextParams = next.params;
209 | let nextWildCardName = next.config.hasChildRouter ? next.getWildCardName() : null;
210 |
211 | for (let key in nextParams) {
212 | if (key === nextWildCardName) {
213 | continue;
214 | }
215 |
216 | if (prevParams[key] !== nextParams[key]) {
217 | return true;
218 | }
219 | }
220 |
221 | for (let key in prevParams) {
222 | if (key === nextWildCardName) {
223 | continue;
224 | }
225 |
226 | if (prevParams[key] !== nextParams[key]) {
227 | return true;
228 | }
229 | }
230 |
231 | if (!next.options.compareQueryParams) {
232 | return false;
233 | }
234 |
235 | let prevQueryParams = prev.queryParams;
236 | let nextQueryParams = next.queryParams;
237 | for (let key in nextQueryParams) {
238 | if (prevQueryParams[key] !== nextQueryParams[key]) {
239 | return true;
240 | }
241 | }
242 |
243 | for (let key in prevQueryParams) {
244 | if (prevQueryParams[key] !== nextQueryParams[key]) {
245 | return true;
246 | }
247 | }
248 |
249 | return false;
250 | };
251 |
--------------------------------------------------------------------------------
/src/next.ts:
--------------------------------------------------------------------------------
1 | import { PipelineStatus } from './pipeline-status';
2 | import { NavigationInstruction } from './navigation-instruction';
3 | import { Next, StepRunnerFunction, NextCompletionHandler } from './interfaces';
4 |
5 | /**@internal exported for unit testing */
6 | export const createNextFn = (instruction: NavigationInstruction, steps: StepRunnerFunction[]): Next => {
7 | let index = -1;
8 | const next: Next = function() {
9 | index++;
10 |
11 | if (index < steps.length) {
12 | let currentStep = steps[index];
13 |
14 | try {
15 | return currentStep(instruction, next);
16 | } catch (e) {
17 | return next.reject(e);
18 | }
19 | } else {
20 | return next.complete();
21 | }
22 | } as Next;
23 |
24 | next.complete = createCompletionHandler(next, PipelineStatus.Completed);
25 | next.cancel = createCompletionHandler(next, PipelineStatus.Canceled);
26 | next.reject = createCompletionHandler(next, PipelineStatus.Rejected);
27 |
28 | return next;
29 | };
30 |
31 | /**@internal exported for unit testing */
32 | export const createCompletionHandler = (next: Next, status: PipelineStatus): NextCompletionHandler => {
33 | return (output: any) => Promise
34 | .resolve({
35 | status,
36 | output,
37 | completed: status === PipelineStatus.Completed
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/src/pipeline-provider.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-inferrable-types */
2 | import { Container } from 'aurelia-dependency-injection';
3 | import { Pipeline } from './pipeline';
4 | import { BuildNavigationPlanStep } from './step-build-navigation-plan';
5 | import { LoadRouteStep } from './step-load-route';
6 | import { CommitChangesStep } from './step-commit-changes';
7 | import { CanDeactivatePreviousStep, CanActivateNextStep, DeactivatePreviousStep, ActivateNextStep } from './step-activation';
8 | import { PipelineStep, StepRunnerFunction, IPipelineSlot } from './interfaces';
9 | import { PipelineSlotName } from './pipeline-slot-name';
10 |
11 | /**
12 | * A multi-slots Pipeline Placeholder Step for hooking into a pipeline execution
13 | */
14 | class PipelineSlot implements IPipelineSlot {
15 |
16 | /**@internal */
17 | container: Container;
18 | /**@internal */
19 | slotName: string;
20 | /**@internal */
21 | slotAlias?: string;
22 |
23 | steps: (Function | PipelineStep)[] = [];
24 |
25 | constructor(container: Container, name: string, alias?: string) {
26 | this.container = container;
27 | this.slotName = name;
28 | this.slotAlias = alias;
29 | }
30 |
31 | getSteps(): (StepRunnerFunction | IPipelineSlot | PipelineStep)[] {
32 | return this.steps.map(x => this.container.get(x));
33 | }
34 | }
35 |
36 | /**
37 | * Class responsible for creating the navigation pipeline.
38 | */
39 | export class PipelineProvider {
40 |
41 | /**@internal */
42 | static inject() { return [Container]; }
43 | /**@internal */
44 | container: Container;
45 | /**@internal */
46 | steps: (Function | PipelineSlot)[];
47 |
48 | constructor(container: Container) {
49 | this.container = container;
50 | this.steps = [
51 | BuildNavigationPlanStep,
52 | CanDeactivatePreviousStep, // optional
53 | LoadRouteStep,
54 | createPipelineSlot(container, PipelineSlotName.Authorize),
55 | CanActivateNextStep, // optional
56 | createPipelineSlot(container, PipelineSlotName.PreActivate, 'modelbind'),
57 | // NOTE: app state changes start below - point of no return
58 | DeactivatePreviousStep, // optional
59 | ActivateNextStep, // optional
60 | createPipelineSlot(container, PipelineSlotName.PreRender, 'precommit'),
61 | CommitChangesStep,
62 | createPipelineSlot(container, PipelineSlotName.PostRender, 'postcomplete')
63 | ];
64 | }
65 |
66 | /**
67 | * Create the navigation pipeline.
68 | */
69 | createPipeline(useCanDeactivateStep: boolean = true): Pipeline {
70 | let pipeline = new Pipeline();
71 | this.steps.forEach(step => {
72 | if (useCanDeactivateStep || step !== CanDeactivatePreviousStep) {
73 | pipeline.addStep(this.container.get(step));
74 | }
75 | });
76 | return pipeline;
77 | }
78 |
79 | /**@internal */
80 | _findStep(name: string): PipelineSlot {
81 | // Steps that are not PipelineSlots are constructor functions, and they will automatically fail. Probably.
82 | return this.steps.find(x => (x as PipelineSlot).slotName === name || (x as PipelineSlot).slotAlias === name) as PipelineSlot;
83 | }
84 |
85 | /**
86 | * Adds a step into the pipeline at a known slot location.
87 | */
88 | addStep(name: string, step: PipelineStep | Function): void {
89 | let found = this._findStep(name);
90 | if (found) {
91 | let slotSteps = found.steps;
92 | // prevent duplicates
93 | if (!slotSteps.includes(step)) {
94 | slotSteps.push(step);
95 | }
96 | } else {
97 | throw new Error(`Invalid pipeline slot name: ${name}.`);
98 | }
99 | }
100 |
101 | /**
102 | * Removes a step from a slot in the pipeline
103 | */
104 | removeStep(name: string, step: PipelineStep): void {
105 | let slot = this._findStep(name);
106 | if (slot) {
107 | let slotSteps = slot.steps;
108 | slotSteps.splice(slotSteps.indexOf(step), 1);
109 | }
110 | }
111 |
112 | /**
113 | * Clears all steps from a slot in the pipeline
114 | * @internal
115 | */
116 | _clearSteps(name: string = ''): void {
117 | let slot = this._findStep(name);
118 | if (slot) {
119 | slot.steps = [];
120 | }
121 | }
122 |
123 | /**
124 | * Resets all pipeline slots
125 | */
126 | reset(): void {
127 | this._clearSteps(PipelineSlotName.Authorize);
128 | this._clearSteps(PipelineSlotName.PreActivate);
129 | this._clearSteps(PipelineSlotName.PreRender);
130 | this._clearSteps(PipelineSlotName.PostRender);
131 | }
132 | }
133 |
134 | /**@internal */
135 | const createPipelineSlot = (container: Container, name: PipelineSlotName, alias?: string): PipelineSlot => {
136 | return new PipelineSlot(container, name, alias);
137 | };
138 |
--------------------------------------------------------------------------------
/src/pipeline-slot-name.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Available pipeline slot names to insert interceptor into router pipeline
3 | */
4 | // const enum is preserved in tsconfig
5 | export const enum PipelineSlotName {
6 | /**
7 | * Authorization slot. Invoked early in the pipeline,
8 | * before `canActivate` hook of incoming route
9 | */
10 | Authorize = 'authorize',
11 | /**
12 | * Pre-activation slot. Invoked early in the pipeline,
13 | * Invoked timing:
14 | * - after Authorization slot
15 | * - after canActivate hook on new view model
16 | * - before deactivate hook on old view model
17 | * - before activate hook on new view model
18 | */
19 | PreActivate = 'preActivate',
20 | /**
21 | * Pre-render slot. Invoked later in the pipeline
22 | * Invokcation timing:
23 | * - after activate hook on new view model
24 | * - before commit step on new navigation instruction
25 | */
26 | PreRender = 'preRender',
27 | /**
28 | * Post-render slot. Invoked last in the pipeline
29 | */
30 | PostRender = 'postRender'
31 | }
32 |
--------------------------------------------------------------------------------
/src/pipeline-status.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The status of a Pipeline.
3 | */
4 | export const enum PipelineStatus {
5 | Completed = 'completed',
6 | Canceled = 'canceled',
7 | Rejected = 'rejected',
8 | Running = 'running'
9 | }
10 |
--------------------------------------------------------------------------------
/src/pipeline.ts:
--------------------------------------------------------------------------------
1 | import { PipelineStep, PipelineResult, StepRunnerFunction, IPipelineSlot } from './interfaces';
2 | import { NavigationInstruction } from './navigation-instruction';
3 | import { createNextFn } from './next';
4 |
5 | /**
6 | * The class responsible for managing and processing the navigation pipeline.
7 | */
8 | export class Pipeline {
9 | /**
10 | * The pipeline steps. And steps added via addStep will be converted to a function
11 | * The actualy running functions with correct step contexts of this pipeline
12 | */
13 | steps: StepRunnerFunction[] = [];
14 |
15 | /**
16 | * Adds a step to the pipeline.
17 | *
18 | * @param step The pipeline step.
19 | */
20 | addStep(step: StepRunnerFunction | PipelineStep | IPipelineSlot): Pipeline {
21 | let run;
22 |
23 | if (typeof step === 'function') {
24 | run = step;
25 | } else if (typeof step.getSteps === 'function') {
26 | // getSteps is to enable support open slots
27 | // where devs can add multiple steps into the same slot name
28 | let steps = step.getSteps();
29 | for (let i = 0, l = steps.length; i < l; i++) {
30 | this.addStep(steps[i]);
31 | }
32 |
33 | return this;
34 | } else {
35 | run = (step as PipelineStep).run.bind(step);
36 | }
37 |
38 | this.steps.push(run);
39 |
40 | return this;
41 | }
42 |
43 | /**
44 | * Runs the pipeline.
45 | *
46 | * @param instruction The navigation instruction to process.
47 | */
48 | run(instruction: NavigationInstruction): Promise {
49 | const nextFn = createNextFn(instruction, this.steps);
50 | return nextFn();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/route-loader.ts:
--------------------------------------------------------------------------------
1 | import { RouteConfig } from './interfaces';
2 | import { NavigationInstruction } from './navigation-instruction';
3 | import { Router } from './router';
4 |
5 | /**
6 | * Abstract class that is responsible for loading view / view model from a route config
7 | * The default implementation can be found in `aurelia-templating-router`
8 | */
9 | export class RouteLoader {
10 | /**
11 | * Load a route config based on its viewmodel / view configuration
12 | */
13 | // return typing: return typings used to be never
14 | // as it was a throw. Changing it to Promise should not cause any issues
15 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
16 | loadRoute(router: Router, config: RouteConfig, navigationInstruction: NavigationInstruction): Promise*ViewPortInstruction*/any> {
17 | throw new Error('Route loaders must implement "loadRoute(router, config, navigationInstruction)".');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/router-configuration.ts:
--------------------------------------------------------------------------------
1 | import { RouteConfig, PipelineStep, RouteConfigSpecifier } from './interfaces';
2 | import { _ensureArrayWithSingleRoutePerConfig } from './util';
3 | import { Router } from './router';
4 | import { PipelineSlotName } from './pipeline-slot-name';
5 |
6 | /**
7 | * Class used to configure a [[Router]] instance.
8 | *
9 | * @constructor
10 | */
11 | export class RouterConfiguration {
12 | instructions: Array<(router: Router) => void> = [];
13 | options: {
14 | [key: string]: any;
15 | compareQueryParams?: boolean;
16 | root?: string;
17 | pushState?: boolean;
18 | hashChange?: boolean;
19 | silent?: boolean;
20 | } = {};
21 | pipelineSteps: Array<{ name: string, step: Function | PipelineStep }> = [];
22 | title: string;
23 | titleSeparator: string;
24 | unknownRouteConfig: RouteConfigSpecifier;
25 | viewPortDefaults: Record;
26 |
27 | /**@internal */
28 | _fallbackRoute: string;
29 |
30 | /**
31 | * Adds a step to be run during the [[Router]]'s navigation pipeline.
32 | *
33 | * @param name The name of the pipeline slot to insert the step into.
34 | * @param step The pipeline step.
35 | * @chainable
36 | */
37 | addPipelineStep(name: string, step: Function | PipelineStep): RouterConfiguration {
38 | if (step === null || step === undefined) {
39 | throw new Error('Pipeline step cannot be null or undefined.');
40 | }
41 | this.pipelineSteps.push({ name, step });
42 | return this;
43 | }
44 |
45 | /**
46 | * Adds a step to be run during the [[Router]]'s authorize pipeline slot.
47 | *
48 | * @param step The pipeline step.
49 | * @chainable
50 | */
51 | addAuthorizeStep(step: Function | PipelineStep): RouterConfiguration {
52 | return this.addPipelineStep(PipelineSlotName.Authorize, step);
53 | }
54 |
55 | /**
56 | * Adds a step to be run during the [[Router]]'s preActivate pipeline slot.
57 | *
58 | * @param step The pipeline step.
59 | * @chainable
60 | */
61 | addPreActivateStep(step: Function | PipelineStep): RouterConfiguration {
62 | return this.addPipelineStep(PipelineSlotName.PreActivate, step);
63 | }
64 |
65 | /**
66 | * Adds a step to be run during the [[Router]]'s preRender pipeline slot.
67 | *
68 | * @param step The pipeline step.
69 | * @chainable
70 | */
71 | addPreRenderStep(step: Function | PipelineStep): RouterConfiguration {
72 | return this.addPipelineStep(PipelineSlotName.PreRender, step);
73 | }
74 |
75 | /**
76 | * Adds a step to be run during the [[Router]]'s postRender pipeline slot.
77 | *
78 | * @param step The pipeline step.
79 | * @chainable
80 | */
81 | addPostRenderStep(step: Function | PipelineStep): RouterConfiguration {
82 | return this.addPipelineStep(PipelineSlotName.PostRender, step);
83 | }
84 |
85 | /**
86 | * Configures a route that will be used if there is no previous location available on navigation cancellation.
87 | *
88 | * @param fragment The URL fragment to use as the navigation destination.
89 | * @chainable
90 | */
91 | fallbackRoute(fragment: string): RouterConfiguration {
92 | this._fallbackRoute = fragment;
93 | return this;
94 | }
95 |
96 | /**
97 | * Maps one or more routes to be registered with the router.
98 | *
99 | * @param route The [[RouteConfig]] to map, or an array of [[RouteConfig]] to map.
100 | * @chainable
101 | */
102 | map(route: RouteConfig | RouteConfig[]): RouterConfiguration {
103 | if (Array.isArray(route)) {
104 | route.forEach(r => this.map(r));
105 | return this;
106 | }
107 |
108 | return this.mapRoute(route);
109 | }
110 |
111 | /**
112 | * Configures defaults to use for any view ports.
113 | *
114 | * @param viewPortConfig a view port configuration object to use as a
115 | * default, of the form { viewPortName: { moduleId } }.
116 | * @chainable
117 | */
118 | useViewPortDefaults(viewPortConfig: Record): RouterConfiguration {
119 | this.viewPortDefaults = viewPortConfig;
120 | return this;
121 | }
122 |
123 | /**
124 | * Maps a single route to be registered with the router.
125 | *
126 | * @param route The [[RouteConfig]] to map.
127 | * @chainable
128 | */
129 | mapRoute(config: RouteConfig): RouterConfiguration {
130 | this.instructions.push(router => {
131 | let routeConfigs = _ensureArrayWithSingleRoutePerConfig(config);
132 |
133 | let navModel;
134 | for (let i = 0, ii = routeConfigs.length; i < ii; ++i) {
135 | let routeConfig = routeConfigs[i];
136 | routeConfig.settings = routeConfig.settings || {};
137 | if (!navModel) {
138 | navModel = router.createNavModel(routeConfig);
139 | }
140 |
141 | router.addRoute(routeConfig, navModel);
142 | }
143 | });
144 |
145 | return this;
146 | }
147 |
148 | /**
149 | * Registers an unknown route handler to be run when the URL fragment doesn't match any registered routes.
150 | *
151 | * @param config A string containing a moduleId to load, or a [[RouteConfig]], or a function that takes the
152 | * [[NavigationInstruction]] and selects a moduleId to load.
153 | * @chainable
154 | */
155 | mapUnknownRoutes(config: RouteConfigSpecifier): RouterConfiguration {
156 | this.unknownRouteConfig = config;
157 | return this;
158 | }
159 |
160 | /**
161 | * Applies the current configuration to the specified [[Router]].
162 | *
163 | * @param router The [[Router]] to apply the configuration to.
164 | */
165 | exportToRouter(router: Router): void {
166 | let instructions = this.instructions;
167 | for (let i = 0, ii = instructions.length; i < ii; ++i) {
168 | instructions[i](router);
169 | }
170 |
171 | let { title, titleSeparator, unknownRouteConfig, _fallbackRoute, viewPortDefaults } = this;
172 |
173 | if (title) {
174 | router.title = title;
175 | }
176 |
177 | if (titleSeparator) {
178 | router.titleSeparator = titleSeparator;
179 | }
180 |
181 | if (unknownRouteConfig) {
182 | router.handleUnknownRoutes(unknownRouteConfig);
183 | }
184 |
185 | if (_fallbackRoute) {
186 | router.fallbackRoute = _fallbackRoute;
187 | }
188 |
189 | if (viewPortDefaults) {
190 | router.useViewPortDefaults(viewPortDefaults);
191 | }
192 |
193 | Object.assign(router.options, this.options);
194 |
195 | let pipelineSteps = this.pipelineSteps;
196 | let pipelineStepCount = pipelineSteps.length;
197 | if (pipelineStepCount) {
198 | if (!router.isRoot) {
199 | throw new Error('Pipeline steps can only be added to the root router');
200 | }
201 |
202 | let pipelineProvider = router.pipelineProvider;
203 | for (let i = 0, ii = pipelineStepCount; i < ii; ++i) {
204 | let { name, step } = pipelineSteps[i];
205 | pipelineProvider.addStep(name, step);
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/router-event.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A list of known router events used by the Aurelia router
3 | * to signal the pipeline has come to a certain state
4 | */
5 | // const enum is preserved in tsconfig
6 | export const enum RouterEvent {
7 | Processing = 'router:navigation:processing',
8 | Error = 'router:navigation:error',
9 | Canceled = 'router:navigation:canceled',
10 | Complete = 'router:navigation:complete',
11 | Success = 'router:navigation:success',
12 | ChildComplete = 'router:navigation:child:complete'
13 | }
14 |
--------------------------------------------------------------------------------
/src/step-activation.ts:
--------------------------------------------------------------------------------
1 | import { Next } from './interfaces';
2 | import { NavigationInstruction } from './navigation-instruction';
3 | import { processDeactivatable, processActivatable } from './utilities-activation';
4 |
5 | /**
6 | * A pipeline step responsible for finding and activating method `canDeactivate` on a view model of a route
7 | */
8 | export class CanDeactivatePreviousStep {
9 | run(navigationInstruction: NavigationInstruction, next: Next): Promise {
10 | return processDeactivatable(navigationInstruction, 'canDeactivate', next);
11 | }
12 | }
13 |
14 | /**
15 | * A pipeline step responsible for finding and activating method `canActivate` on a view model of a route
16 | */
17 | export class CanActivateNextStep {
18 | run(navigationInstruction: NavigationInstruction, next: Next): Promise {
19 | return processActivatable(navigationInstruction, 'canActivate', next);
20 | }
21 | }
22 |
23 | /**
24 | * A pipeline step responsible for finding and activating method `deactivate` on a view model of a route
25 | */
26 | export class DeactivatePreviousStep {
27 | run(navigationInstruction: NavigationInstruction, next: Next): Promise {
28 | return processDeactivatable(navigationInstruction, 'deactivate', next, true);
29 | }
30 | }
31 |
32 | /**
33 | * A pipeline step responsible for finding and activating method `activate` on a view model of a route
34 | */
35 | export class ActivateNextStep {
36 | run(navigationInstruction: NavigationInstruction, next: Next): Promise {
37 | return processActivatable(navigationInstruction, 'activate', next, true);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/step-build-navigation-plan.ts:
--------------------------------------------------------------------------------
1 | import { Next } from './interfaces';
2 | import { Redirect } from './navigation-commands';
3 | import { NavigationInstruction } from './navigation-instruction';
4 | import { _buildNavigationPlan } from './navigation-plan';
5 |
6 | /**
7 | * Transform a navigation instruction into viewport plan record object,
8 | * or a redirect request if user viewmodel demands
9 | */
10 | export class BuildNavigationPlanStep {
11 | run(navigationInstruction: NavigationInstruction, next: Next): Promise {
12 | return _buildNavigationPlan(navigationInstruction)
13 | .then(plan => {
14 | if (plan instanceof Redirect) {
15 | return next.cancel(plan);
16 | }
17 | navigationInstruction.plan = plan;
18 | return next();
19 | })
20 | .catch(next.cancel);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/step-commit-changes.ts:
--------------------------------------------------------------------------------
1 | import { NavigationInstruction } from './navigation-instruction';
2 |
3 | /**
4 | * A pipeline step for instructing a piepline to commit changes on a navigation instruction
5 | */
6 | export class CommitChangesStep {
7 | run(navigationInstruction: NavigationInstruction, next: Function): Promise {
8 | return navigationInstruction
9 | ._commitChanges(/*wait to swap?*/ true)
10 | .then(() => {
11 | navigationInstruction._updateTitle();
12 | return next();
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/step-load-route.ts:
--------------------------------------------------------------------------------
1 | import { Next } from './interfaces';
2 | import { NavigationInstruction } from './navigation-instruction';
3 | import { loadNewRoute } from './utilities-route-loading';
4 | import { RouteLoader } from './route-loader';
5 | /**
6 | * A pipeline step responsible for loading a route config of a navigation instruction
7 | */
8 | export class LoadRouteStep {
9 | /**@internal */
10 | static inject() { return [RouteLoader]; }
11 | /**
12 | * Route loader isntance that will handle loading route config
13 | * @internal
14 | */
15 | routeLoader: RouteLoader;
16 | constructor(routeLoader: RouteLoader) {
17 | this.routeLoader = routeLoader;
18 | }
19 | /**
20 | * Run the internal to load route config of a navigation instruction to prepare for next steps in the pipeline
21 | */
22 | run(navigationInstruction: NavigationInstruction, next: Next): Promise {
23 | return loadNewRoute(this.routeLoader, navigationInstruction)
24 | .then(next, next.cancel);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import { RouteConfig } from './interfaces';
2 |
3 | export function _normalizeAbsolutePath(path: string, hasPushState: boolean, absolute = false) {
4 | if (!hasPushState && path[0] !== '#') {
5 | path = '#' + path;
6 | }
7 |
8 | if (hasPushState && absolute) {
9 | path = path.substring(1, path.length);
10 | }
11 |
12 | return path;
13 | }
14 |
15 | export function _createRootedPath(fragment: string, baseUrl: string, hasPushState: boolean, absolute?: boolean) {
16 | if (isAbsoluteUrl.test(fragment)) {
17 | return fragment;
18 | }
19 |
20 | let path = '';
21 |
22 | if (baseUrl.length && baseUrl[0] !== '/') {
23 | path += '/';
24 | }
25 |
26 | path += baseUrl;
27 |
28 | if ((!path.length || path[path.length - 1] !== '/') && fragment[0] !== '/') {
29 | path += '/';
30 | }
31 |
32 | if (path.length && path[path.length - 1] === '/' && fragment[0] === '/') {
33 | path = path.substring(0, path.length - 1);
34 | }
35 |
36 | return _normalizeAbsolutePath(path + fragment, hasPushState, absolute);
37 | }
38 |
39 | export function _resolveUrl(fragment: string, baseUrl: string, hasPushState?: boolean) {
40 | if (isRootedPath.test(fragment)) {
41 | return _normalizeAbsolutePath(fragment, hasPushState);
42 | }
43 |
44 | return _createRootedPath(fragment, baseUrl, hasPushState);
45 | }
46 |
47 | export function _ensureArrayWithSingleRoutePerConfig(config: RouteConfig) {
48 | let routeConfigs = [];
49 |
50 | if (Array.isArray(config.route)) {
51 | for (let i = 0, ii = config.route.length; i < ii; ++i) {
52 | let current = Object.assign({}, config);
53 | current.route = config.route[i];
54 | routeConfigs.push(current);
55 | }
56 | } else {
57 | routeConfigs.push(Object.assign({}, config));
58 | }
59 |
60 | return routeConfigs;
61 | }
62 |
63 | const isRootedPath = /^#?\//;
64 | const isAbsoluteUrl = /^([a-z][a-z0-9+\-.]*:)?\/\//i;
65 |
--------------------------------------------------------------------------------
/src/utilities-activation.ts:
--------------------------------------------------------------------------------
1 | import { Next, ViewPortComponent, ViewPortPlan, ViewPortInstruction, LifecycleArguments } from './interfaces';
2 | import { isNavigationCommand } from './navigation-commands';
3 | import { NavigationInstruction } from './navigation-instruction';
4 | import { activationStrategy } from './activation-strategy';
5 | import { Router } from './router';
6 |
7 | /**
8 | * Recursively find list of deactivate-able view models
9 | * and invoke the either 'canDeactivate' or 'deactivate' on each
10 | * @internal exported for unit testing
11 | */
12 | export const processDeactivatable = (
13 | navigationInstruction: NavigationInstruction,
14 | callbackName: 'canDeactivate' | 'deactivate',
15 | next: Next,
16 | ignoreResult?: boolean
17 | ): Promise => {
18 | let plan: Record = navigationInstruction.plan;
19 | let infos = findDeactivatable(plan, callbackName);
20 | let i = infos.length; // query from inside out
21 |
22 | function inspect(val: any): Promise {
23 | if (ignoreResult || shouldContinue(val)) {
24 | return iterate();
25 | }
26 |
27 | return next.cancel(val);
28 | }
29 |
30 | function iterate(): Promise {
31 | if (i--) {
32 | try {
33 | let viewModel = infos[i];
34 | let result = viewModel[callbackName](navigationInstruction);
35 | return processPotential(result, inspect, next.cancel);
36 | } catch (error) {
37 | return next.cancel(error);
38 | }
39 | }
40 |
41 | navigationInstruction.router.couldDeactivate = true;
42 |
43 | return next();
44 | }
45 |
46 | return iterate();
47 | };
48 |
49 | /**
50 | * Recursively find and returns a list of deactivate-able view models
51 | * @internal exported for unit testing
52 | */
53 | export const findDeactivatable = (
54 | plan: Record,
55 | callbackName: string,
56 | list: IActivatableInfo[] = []
57 | ): any[] => {
58 | for (let viewPortName in plan) {
59 | let viewPortPlan = plan[viewPortName];
60 | let prevComponent = viewPortPlan.prevComponent;
61 |
62 | if ((viewPortPlan.strategy === activationStrategy.invokeLifecycle || viewPortPlan.strategy === activationStrategy.replace)
63 | && prevComponent
64 | ) {
65 | let viewModel = prevComponent.viewModel;
66 |
67 | if (callbackName in viewModel) {
68 | list.push(viewModel);
69 | }
70 | }
71 |
72 | if (viewPortPlan.strategy === activationStrategy.replace && prevComponent) {
73 | addPreviousDeactivatable(prevComponent, callbackName, list);
74 | } else if (viewPortPlan.childNavigationInstruction) {
75 | findDeactivatable(viewPortPlan.childNavigationInstruction.plan, callbackName, list);
76 | }
77 | }
78 |
79 | return list;
80 | };
81 |
82 | /**
83 | * @internal exported for unit testing
84 | */
85 | export const addPreviousDeactivatable = (
86 | component: ViewPortComponent,
87 | callbackName: string,
88 | list: IActivatableInfo[]
89 | ): void => {
90 | let childRouter = component.childRouter;
91 |
92 | if (childRouter && childRouter.currentInstruction) {
93 | let viewPortInstructions = childRouter.currentInstruction.viewPortInstructions;
94 |
95 | for (let viewPortName in viewPortInstructions) {
96 | let viewPortInstruction = viewPortInstructions[viewPortName];
97 | let prevComponent = viewPortInstruction.component;
98 | let prevViewModel = prevComponent.viewModel;
99 |
100 | if (callbackName in prevViewModel) {
101 | list.push(prevViewModel);
102 | }
103 |
104 | addPreviousDeactivatable(prevComponent, callbackName, list);
105 | }
106 | }
107 | };
108 |
109 | /**
110 | * @internal exported for unit testing
111 | */
112 | export const processActivatable = (
113 | navigationInstruction: NavigationInstruction,
114 | callbackName: 'canActivate' | 'activate',
115 | next: Next,
116 | ignoreResult?: boolean
117 | ): Promise => {
118 | let infos = findActivatable(navigationInstruction, callbackName);
119 | let length = infos.length;
120 | let i = -1; // query from top down
121 |
122 | function inspect(val: any, router: Router): Promise {
123 | if (ignoreResult || shouldContinue(val, router)) {
124 | return iterate();
125 | }
126 |
127 | return next.cancel(val);
128 | }
129 |
130 | function iterate(): Promise {
131 | i++;
132 |
133 | if (i < length) {
134 | try {
135 | let current = infos[i];
136 | let result = current.viewModel[callbackName](...current.lifecycleArgs);
137 | return processPotential(result, (val: any) => inspect(val, current.router), next.cancel);
138 | } catch (error) {
139 | return next.cancel(error);
140 | }
141 | }
142 |
143 | return next();
144 | }
145 |
146 | return iterate();
147 | };
148 |
149 | interface IActivatableInfo {
150 | viewModel: any;
151 | lifecycleArgs: LifecycleArguments;
152 | router: Router;
153 | }
154 |
155 | /**
156 | * Find list of activatable view model and add to list (3rd parameter)
157 | * @internal exported for unit testing
158 | */
159 | export const findActivatable = (
160 | navigationInstruction: NavigationInstruction,
161 | callbackName: 'canActivate' | 'activate',
162 | list: IActivatableInfo[] = [],
163 | router?: Router
164 | ): IActivatableInfo[] => {
165 | let plan: Record = navigationInstruction.plan;
166 |
167 | Object
168 | .keys(plan)
169 | .forEach((viewPortName) => {
170 | let viewPortPlan = plan[viewPortName];
171 | let viewPortInstruction = navigationInstruction.viewPortInstructions[viewPortName] as ViewPortInstruction;
172 | let viewPortComponent = viewPortInstruction.component;
173 | let viewModel = viewPortComponent.viewModel;
174 |
175 | if (
176 | (viewPortPlan.strategy === activationStrategy.invokeLifecycle
177 | || viewPortPlan.strategy === activationStrategy.replace
178 | )
179 | && callbackName in viewModel
180 | ) {
181 | list.push({
182 | viewModel,
183 | lifecycleArgs: viewPortInstruction.lifecycleArgs,
184 | router
185 | });
186 | }
187 |
188 | let childNavInstruction = viewPortPlan.childNavigationInstruction;
189 |
190 | if (childNavInstruction) {
191 | findActivatable(
192 | childNavInstruction,
193 | callbackName,
194 | list,
195 | viewPortComponent.childRouter || router
196 | );
197 | }
198 | });
199 |
200 | return list;
201 | };
202 |
203 | const shouldContinue = (output: T, router?: Router): boolean | T => {
204 | if (output instanceof Error) {
205 | return false;
206 | }
207 |
208 | if (isNavigationCommand(output)) {
209 | if (typeof output.setRouter === 'function') {
210 | output.setRouter(router);
211 | }
212 |
213 | return !!output.shouldContinueProcessing;
214 | }
215 |
216 | if (output === undefined) {
217 | return true;
218 | }
219 |
220 | return output;
221 | };
222 |
223 | /**
224 | * A basic interface for an Observable type
225 | */
226 | export interface IObservable {
227 | subscribe(sub?: IObservableConfig): ISubscription;
228 | }
229 |
230 | export interface IObservableConfig {
231 | next(): void;
232 | error(err?: any): void;
233 | complete(): void;
234 | }
235 |
236 | /**
237 | * A basic interface for a Subscription to an Observable
238 | */
239 | interface ISubscription {
240 | unsubscribe(): void;
241 | }
242 |
243 | type SafeSubscriptionFunc = (sub: SafeSubscription) => ISubscription;
244 |
245 | /**
246 | * wraps a subscription, allowing unsubscribe calls even if
247 | * the first value comes synchronously
248 | */
249 | class SafeSubscription {
250 |
251 | private _subscribed: boolean;
252 | private _subscription: ISubscription;
253 |
254 | constructor(subscriptionFunc: SafeSubscriptionFunc) {
255 | this._subscribed = true;
256 | this._subscription = subscriptionFunc(this);
257 |
258 | if (!this._subscribed) {
259 | this.unsubscribe();
260 | }
261 | }
262 |
263 | get subscribed(): boolean {
264 | return this._subscribed;
265 | }
266 |
267 | unsubscribe(): void {
268 | if (this._subscribed && this._subscription) {
269 | this._subscription.unsubscribe();
270 | }
271 |
272 | this._subscribed = false;
273 | }
274 | }
275 |
276 | /**
277 | * A function to process return value from `activate`/`canActivate` steps
278 | * Supports observable/promise
279 | *
280 | * For observable, resolve at first next() or on complete()
281 | */
282 | const processPotential = (obj: any, resolve: (val?: any) => any, reject: (err?: any) => any): any => {
283 | // if promise like
284 | if (obj && typeof obj.then === 'function') {
285 | return Promise.resolve(obj).then(resolve).catch(reject);
286 | }
287 |
288 | // if observable
289 | if (obj && typeof obj.subscribe === 'function') {
290 | let obs: IObservable = obj;
291 | return new SafeSubscription(sub => obs.subscribe({
292 | next() {
293 | if (sub.subscribed) {
294 | sub.unsubscribe();
295 | resolve(obj);
296 | }
297 | },
298 | error(error) {
299 | if (sub.subscribed) {
300 | sub.unsubscribe();
301 | reject(error);
302 | }
303 | },
304 | complete() {
305 | if (sub.subscribed) {
306 | sub.unsubscribe();
307 | resolve(obj);
308 | }
309 | }
310 | }));
311 | }
312 |
313 | // else just resolve
314 | try {
315 | return resolve(obj);
316 | } catch (error) {
317 | return reject(error);
318 | }
319 | };
320 |
--------------------------------------------------------------------------------
/src/utilities-route-loading.ts:
--------------------------------------------------------------------------------
1 | import { RouteConfig, ViewPortComponent, ViewPortPlan, ViewPortInstruction } from './interfaces';
2 | import { Redirect } from './navigation-commands';
3 | import { NavigationInstruction } from './navigation-instruction';
4 | import { _buildNavigationPlan } from './navigation-plan';
5 | import { InternalActivationStrategy } from './activation-strategy';
6 | import { RouteLoader } from './route-loader';
7 |
8 | /**
9 | * Loading plan calculated based on a navigration-instruction and a viewport plan
10 | */
11 | interface ILoadingPlan {
12 | viewPortPlan: ViewPortPlan;
13 | navigationInstruction: NavigationInstruction;
14 | }
15 |
16 | /**
17 | * @internal Exported for unit testing
18 | */
19 | export const loadNewRoute = (
20 | routeLoader: RouteLoader,
21 | navigationInstruction: NavigationInstruction
22 | ): Promise => {
23 | let loadingPlans = determineLoadingPlans(navigationInstruction);
24 | let loadPromises = loadingPlans.map((loadingPlan: ILoadingPlan) => loadRoute(
25 | routeLoader,
26 | loadingPlan.navigationInstruction,
27 | loadingPlan.viewPortPlan
28 | ));
29 |
30 | return Promise.all(loadPromises);
31 | };
32 |
33 | /**
34 | * @internal Exported for unit testing
35 | */
36 | export const determineLoadingPlans = (
37 | navigationInstruction: NavigationInstruction,
38 | loadingPlans: ILoadingPlan[] = []
39 | ): ILoadingPlan[] => {
40 | let viewPortPlans: Record = navigationInstruction.plan;
41 |
42 | for (let viewPortName in viewPortPlans) {
43 | let viewPortPlan = viewPortPlans[viewPortName];
44 | let childNavInstruction = viewPortPlan.childNavigationInstruction;
45 |
46 | if (viewPortPlan.strategy === InternalActivationStrategy.Replace) {
47 | loadingPlans.push({ viewPortPlan, navigationInstruction } as ILoadingPlan);
48 |
49 | if (childNavInstruction) {
50 | determineLoadingPlans(childNavInstruction, loadingPlans);
51 | }
52 | } else {
53 | let viewPortInstruction = navigationInstruction.addViewPortInstruction({
54 | name: viewPortName,
55 | strategy: viewPortPlan.strategy,
56 | moduleId: viewPortPlan.prevModuleId,
57 | component: viewPortPlan.prevComponent
58 | }) as ViewPortInstruction;
59 |
60 | if (childNavInstruction) {
61 | viewPortInstruction.childNavigationInstruction = childNavInstruction;
62 | determineLoadingPlans(childNavInstruction, loadingPlans);
63 | }
64 | }
65 | }
66 |
67 | return loadingPlans;
68 | };
69 |
70 | /**
71 | * @internal Exported for unit testing
72 | */
73 | export const loadRoute = (
74 | routeLoader: RouteLoader,
75 | navigationInstruction: NavigationInstruction,
76 | viewPortPlan: ViewPortPlan
77 | ): Promise => {
78 | let planConfig = viewPortPlan.config;
79 | let moduleId = planConfig ? planConfig.moduleId : null;
80 |
81 | return loadComponent(routeLoader, navigationInstruction, planConfig)
82 | .then((component) => {
83 | let viewPortInstruction = navigationInstruction.addViewPortInstruction({
84 | name: viewPortPlan.name,
85 | strategy: viewPortPlan.strategy,
86 | moduleId: moduleId,
87 | component: component
88 | }) as ViewPortInstruction;
89 |
90 | let childRouter = component.childRouter;
91 | if (childRouter) {
92 | let path = navigationInstruction.getWildcardPath();
93 |
94 | return childRouter
95 | ._createNavigationInstruction(path, navigationInstruction)
96 | .then((childInstruction) => {
97 | viewPortPlan.childNavigationInstruction = childInstruction;
98 |
99 | return _buildNavigationPlan(childInstruction)
100 | .then((childPlan) => {
101 | if (childPlan instanceof Redirect) {
102 | return Promise.reject(childPlan);
103 | }
104 | childInstruction.plan = childPlan;
105 | viewPortInstruction.childNavigationInstruction = childInstruction;
106 |
107 | return loadNewRoute(routeLoader, childInstruction);
108 | });
109 | });
110 | }
111 | // ts complains without this, though they are same
112 | return void 0;
113 | });
114 | };
115 |
116 | /**
117 | * Load a routed-component based on navigation instruction and route config
118 | * @internal exported for unit testing only
119 | */
120 | export const loadComponent = (
121 | routeLoader: RouteLoader,
122 | navigationInstruction: NavigationInstruction,
123 | config: RouteConfig
124 | ): Promise => {
125 | let router = navigationInstruction.router;
126 | let lifecycleArgs = navigationInstruction.lifecycleArgs;
127 |
128 | return Promise.resolve()
129 | .then(() => routeLoader.loadRoute(router, config, navigationInstruction))
130 | .then(
131 | /**
132 | * @param component an object carrying information about loaded route
133 | * typically contains information about view model, childContainer, view and router
134 | */
135 | (component: ViewPortComponent) => {
136 | let { viewModel, childContainer } = component;
137 | component.router = router;
138 | component.config = config;
139 |
140 | if ('configureRouter' in viewModel) {
141 | let childRouter = childContainer.getChildRouter();
142 | component.childRouter = childRouter;
143 |
144 | return childRouter
145 | .configure(c => viewModel.configureRouter(c, childRouter, lifecycleArgs[0], lifecycleArgs[1], lifecycleArgs[2]))
146 | .then(() => component);
147 | }
148 |
149 | return component;
150 | }
151 | );
152 | };
153 |
--------------------------------------------------------------------------------
/test/activation.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Router,
3 | activationStrategy,
4 | ActivationStrategy,
5 | NavigationInstruction,
6 | RouteConfig,
7 | CanDeactivatePreviousStep,
8 | CanActivateNextStep,
9 | ActivateNextStep,
10 | } from '../src/aurelia-router';
11 | import {
12 | ViewPortInstruction,
13 | ViewPortPlan
14 | } from '../src/interfaces';
15 | import { ValueOf, createPipelineState, MockPipelineState } from './shared';
16 |
17 | describe('activation', () => {
18 | describe('CanDeactivatePreviousStep', () => {
19 | let step: CanDeactivatePreviousStep;
20 | let state: MockPipelineState;
21 |
22 | let viewPortFactory = (
23 | resultHandler: (val?: any) => any,
24 | strategy: ValueOf = activationStrategy.invokeLifecycle
25 | ): ViewPortPlan => {
26 | return {
27 | strategy: strategy,
28 | prevComponent: { viewModel: { canDeactivate: resultHandler }, childRouter: null as Router }
29 | } as any;
30 | };
31 |
32 | let instructionFactory = (instruction: NavigationInstruction): NavigationInstruction => {
33 | return Object.assign({ router: {} }, instruction);
34 | };
35 |
36 | beforeEach(() => {
37 | step = new CanDeactivatePreviousStep();
38 | state = createPipelineState();
39 | });
40 |
41 | it('should return true for context that canDeactivate', () => {
42 | let instruction: NavigationInstruction = instructionFactory({ plan: { first: viewPortFactory(() => (true)) } } as any);
43 |
44 | step.run(instruction, state.next);
45 | expect(state.result).toBe(true);
46 | });
47 |
48 | it('should return true for context that canDeactivate with activationStrategy.replace', () => {
49 | let instruction: NavigationInstruction = instructionFactory({
50 | plan: {
51 | first: viewPortFactory(
52 | () => (true),
53 | activationStrategy.replace
54 | )
55 | }
56 | } as any);
57 |
58 | step.run(instruction, state.next);
59 | expect(state.result).toBe(true);
60 | });
61 |
62 | it('should cancel for context that cannot Deactivate', () => {
63 | let instruction: NavigationInstruction = instructionFactory({
64 | plan: {
65 | first: viewPortFactory(() => (false))
66 | }
67 | } as any);
68 |
69 | step.run(instruction, state.next);
70 | expect(state.rejection).toBeTruthy();
71 | });
72 |
73 | it('should return true for context that cannot Deactivate with unknown strategy', () => {
74 | let instruction: NavigationInstruction = instructionFactory({
75 | plan: {
76 | first: viewPortFactory(() => (false), 'unknown' as any)
77 | }
78 | } as any);
79 |
80 | step.run(instruction, state.next);
81 | expect(state.result).toBe(true);
82 | });
83 |
84 |
85 | it('should return true for context that canDeactivate with a promise', (done) => {
86 | let instruction: NavigationInstruction = instructionFactory({
87 | plan: {
88 | first: viewPortFactory(() => (Promise.resolve(true)))
89 | }
90 | } as any);
91 |
92 | step.run(instruction, state.next).then(() => {
93 | expect(state.result).toBe(true);
94 | done();
95 | });
96 | });
97 |
98 | it('should cancel for context that cantDeactivate with a promise', (done) => {
99 | let instruction: NavigationInstruction = instructionFactory({
100 | plan: {
101 | first: viewPortFactory(() => (Promise.resolve(false)))
102 | }
103 | } as any);
104 |
105 | step.run(instruction, state.next).then(() => {
106 | expect(state.rejection).toBeTruthy();
107 | done();
108 | });
109 | });
110 |
111 | it('should cancel for context that throws in canDeactivate', (done) => {
112 | let instruction: NavigationInstruction = instructionFactory({
113 | plan: {
114 | first: viewPortFactory(() => { throw new Error('oops'); })
115 | }
116 | } as any);
117 |
118 | step.run(instruction, state.next).then(() => {
119 | expect(state.rejection).toBeTruthy();
120 | done();
121 | });
122 | });
123 |
124 | it('should return true when all plans return true', () => {
125 | let instruction: NavigationInstruction = instructionFactory({
126 | plan: {
127 | first: viewPortFactory(() => (true)),
128 | second: viewPortFactory(() => (true))
129 | }
130 | } as any);
131 |
132 | step.run(instruction, state.next);
133 | expect(state.result).toBe(true);
134 | });
135 |
136 | it('should cancel when some plans return false', () => {
137 | let instruction: NavigationInstruction = instructionFactory({
138 | plan: {
139 | first: viewPortFactory(() => (true)),
140 | second: viewPortFactory(() => (false))
141 | }
142 | } as any);
143 |
144 | step.run(instruction, state.next);
145 | expect(state.rejection).toBeTruthy();
146 | });
147 |
148 | it('should pass a navigationInstruction to the callback function', () => {
149 | const instruction: NavigationInstruction = instructionFactory({
150 | plan: {
151 | first: viewPortFactory(() => (true))
152 | }
153 | } as any);
154 | const viewModel = instruction.plan.first.prevComponent.viewModel;
155 | spyOn(viewModel, 'canDeactivate').and.callThrough();
156 | step.run(instruction, state.next);
157 | expect(viewModel.canDeactivate).toHaveBeenCalledWith(instruction);
158 | });
159 |
160 | describe('with a childNavigationInstruction', () => {
161 | // let viewPort = viewPortFactory(() => (true));
162 | // let instruction: NavigationInstruction = { plan: { first: viewPort } } as any;
163 |
164 | describe('when navigating on the parent', () => {
165 |
166 | const viewPortInstructionFactory = (resultHandler: (val?: any) => any) => {
167 | return {
168 | component: { viewModel: { canDeactivate: resultHandler } }
169 | } as ViewPortInstruction;
170 | };
171 |
172 | it('should return true when the currentInstruction can deactivate', () => {
173 | let viewPort = viewPortFactory(() => (true), activationStrategy.replace) as any;
174 | let currentInstruction: NavigationInstruction = instructionFactory({
175 | viewPortInstructions: {
176 | first: viewPortInstructionFactory(() => (true))
177 | }
178 | } as any);
179 | viewPort.prevComponent.childRouter = ({ currentInstruction } as any);
180 | let instruction: NavigationInstruction = instructionFactory({
181 | plan: {
182 | first: viewPort
183 | }
184 | } as any);
185 | step.run(instruction, state.next);
186 | expect(state.result).toBe(true);
187 | });
188 |
189 | it('should cancel when router instruction cannot deactivate', () => {
190 | let viewPort = viewPortFactory(() => (true), activationStrategy.replace);
191 | let currentInstruction: NavigationInstruction = instructionFactory({
192 | viewPortInstructions: {
193 | first: viewPortInstructionFactory(() => (false))
194 | }
195 | } as any);
196 | viewPort.prevComponent.childRouter = ({ currentInstruction } as any);
197 | let instruction: NavigationInstruction = instructionFactory({
198 | plan: {
199 | first: viewPort
200 | }
201 | } as any);
202 | step.run(instruction, state.next);
203 | expect(state.rejection).toBeTruthy();
204 | });
205 |
206 | });
207 | });
208 | });
209 |
210 | describe('CanActivateNextStep', () => {
211 | let step: CanActivateNextStep;
212 | let state: MockPipelineState;
213 |
214 | function getNavigationInstruction(
215 | canActivateHandler: () => any,
216 | strategy: ValueOf = activationStrategy.invokeLifecycle
217 | ): NavigationInstruction {
218 | return {
219 | plan: {
220 | default: {
221 | strategy: strategy
222 | }
223 | },
224 | viewPortInstructions: {
225 | default: {
226 | component: { viewModel: { canActivate: canActivateHandler } },
227 | lifecycleArgs: []
228 | }
229 | }
230 | } as any;
231 | }
232 |
233 | beforeEach(() => {
234 | step = new CanActivateNextStep();
235 | state = createPipelineState();
236 | });
237 |
238 | it('should return true for context that canActivate', () => {
239 | let instruction = getNavigationInstruction(() => true);
240 |
241 | step.run(instruction, state.next);
242 | expect(state.result).toBe(true);
243 | });
244 |
245 | it('should return true for context that canActivate with activationStrategy.replace', () => {
246 | let instruction = getNavigationInstruction(() => true, activationStrategy.replace);
247 |
248 | step.run(instruction, state.next);
249 | expect(state.result).toBe(true);
250 | });
251 |
252 | it('should cancel for context that cannot activate', () => {
253 | let instruction = getNavigationInstruction(() => false);
254 |
255 | step.run(instruction, state.next);
256 | expect(state.rejection).toBeTruthy();
257 | });
258 | });
259 |
260 | describe('ActivateNextStep', () => {
261 | let step: ActivateNextStep;
262 | let state: MockPipelineState;
263 |
264 | beforeEach(() => {
265 | step = new ActivateNextStep();
266 | state = createPipelineState();
267 | });
268 |
269 | it('should pass current viewport name to activate', (done) => {
270 | const instruction = new NavigationInstruction({
271 | plan: {
272 | 'my-view-port': { strategy: activationStrategy.invokeLifecycle } as ViewPortInstruction
273 | }
274 | } as any);
275 |
276 | const viewModel = {
277 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
278 | activate(params: Record, config: RouteConfig, instruction: NavigationInstruction) {
279 | expect(config.currentViewPort).toBe('my-view-port');
280 | done();
281 | }
282 | };
283 |
284 | instruction.addViewPortInstruction('my-view-port', 'ignored' as any, 'ignored', { viewModel });
285 | step.run(instruction, state.next);
286 | });
287 | });
288 | });
289 |
--------------------------------------------------------------------------------
/test/app-router.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { Container } from 'aurelia-dependency-injection';
3 | import {
4 | Router,
5 | RouteConfig,
6 | PipelineProvider,
7 | AppRouter,
8 | RouteLoader,
9 | Pipeline,
10 | NavigationInstruction,
11 | NavigationCommand,
12 | Next,
13 | RouterConfiguration,
14 | PipelineStep
15 | } from '../src/aurelia-router';
16 | import { MockHistory, MockInstruction } from './shared';
17 | import { EventAggregator } from 'aurelia-event-aggregator';
18 | import { History } from 'aurelia-history';
19 | import {
20 | ViewPortComponent,
21 | ViewPortInstruction,
22 | ViewPort
23 | } from '../src/interfaces';
24 |
25 | declare module 'aurelia-history' {
26 | interface History {
27 | previousLocation: string;
28 | }
29 | }
30 |
31 |
32 | class MockLoader extends RouteLoader {
33 | loadRoute(router: Router, config: RouteConfig): Promise {
34 | return Promise.resolve({
35 | viewModel: {}
36 | } as ViewPortComponent);
37 | }
38 | }
39 |
40 | describe('app-router', () => {
41 | let router: AppRouter;
42 | let history: History;
43 | let ea: EventAggregator;
44 | let viewPort: ViewPort;
45 | let container: Container;
46 | let instruction: NavigationInstruction;
47 | let provider: PipelineProvider;
48 | let pipelineStep: PipelineStep['run'];
49 |
50 | beforeEach(() => {
51 | history = new MockHistory();
52 | container = new Container();
53 | container.registerSingleton(RouteLoader, MockLoader);
54 | // tslint:disable-next-line
55 | ea = { publish() { } } as any;
56 | viewPort = {
57 | process(viewPortInstruction: ViewPortInstruction) {
58 | return Promise.resolve();
59 | },
60 | // tslint:disable-next-line
61 | swap() { }
62 | } as any;
63 |
64 | // tslint:disable-next-line
65 | instruction = { resolve() { } } as any;
66 | provider = {
67 | createPipeline() {
68 | let p = new Pipeline();
69 | p.addStep({ run(inst, next) { return pipelineStep(inst, next); } });
70 | return p;
71 | }
72 | } as any;
73 |
74 | router = new AppRouter(container, history, provider, ea);
75 | });
76 |
77 | it('configures from root view model configureRouter method', (done) => {
78 | let routeConfig: RouteConfig = { route: '', moduleId: './test' };
79 | let viewModel = {
80 | configureRouter(config: RouterConfiguration) {
81 | config.map([routeConfig]);
82 | }
83 | };
84 |
85 | spyOn(viewModel, 'configureRouter').and.callThrough();
86 |
87 | container.viewModel = viewModel;
88 |
89 | expect(router.isConfigured).toBe(false);
90 | expect(router.routes.length).toBe(0);
91 |
92 | Promise.resolve(router.registerViewPort(viewPort))
93 | .then(result => {
94 | expect(viewModel.configureRouter).toHaveBeenCalled();
95 | expect(router.isConfigured).toBe(true);
96 | expect(router.routes.length).toBe(1);
97 | expect(router.routes[0] as Required).toEqual(jasmine.objectContaining(routeConfig as Required));
98 | done();
99 | });
100 | });
101 |
102 | it('configures only once with multiple viewPorts', (done) => {
103 | let routeConfig = { route: '', moduleId: './test' };
104 | let viewModel = {
105 | configureRouter(config: RouterConfiguration) {
106 | config.map([routeConfig]);
107 | }
108 | };
109 |
110 | spyOn(viewModel, 'configureRouter').and.callThrough();
111 |
112 | container.viewModel = viewModel;
113 |
114 | Promise
115 | .all([
116 | router.registerViewPort(viewPort),
117 | router.registerViewPort(viewPort, 'second')
118 | ])
119 | .then((result: any[]) => {
120 | expect((viewModel.configureRouter as jasmine.Spy).calls.count()).toBe(1);
121 | expect(router.isConfigured).toBe(true);
122 | expect(router.routes.length).toBe(1);
123 | done();
124 | });
125 | });
126 |
127 | describe('dequeueInstruction', () => {
128 | let processingResult: any;
129 | let completedResult: any;
130 |
131 | beforeEach(() => {
132 | router._queue.push(instruction);
133 |
134 | spyOn(ea, 'publish');
135 | processingResult = jasmine.objectContaining({ instruction });
136 | completedResult = jasmine.objectContaining({ instruction, result: jasmine.objectContaining({}) });
137 | });
138 |
139 | it('triggers events on successful navigations', (done) => {
140 | pipelineStep = (ctx: any, next: Next) => next.complete({});
141 |
142 | router
143 | ._dequeueInstruction()
144 | .then(result => {
145 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:processing', processingResult);
146 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:success', completedResult);
147 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:complete', completedResult);
148 | })
149 | .catch(expectSuccess)
150 | .then(done);
151 | });
152 |
153 | it('returns expected results from successful navigations', (done) => {
154 | let output = {};
155 | pipelineStep = (ctx: any, next: Next) => next.complete(output);
156 |
157 | router
158 | ._dequeueInstruction()
159 | .then(result => {
160 | if (!result) {
161 | throw new Error('Invalid instruction processing');
162 | }
163 | expect(result.completed).toBe(true);
164 | expect(result.status).toBe('completed');
165 | expect(result.output).toBe(output);
166 | })
167 | .catch(expectSuccess)
168 | .then(done);
169 | });
170 |
171 | it('triggers events on canceled navigations', (done) => {
172 | pipelineStep = (ctx: any, next: Next) => next.cancel('test');
173 |
174 | router
175 | ._dequeueInstruction()
176 | .then(result => {
177 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:processing', processingResult);
178 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:canceled', completedResult);
179 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:complete', completedResult);
180 | })
181 | .catch(expectSuccess)
182 | .then(done);
183 | });
184 |
185 | it('returns expected results from canceled navigations', (done) => {
186 | let output = {};
187 | pipelineStep = (ctx: any, next: Next) => next.cancel(output);
188 |
189 | router._dequeueInstruction()
190 | .then(result => {
191 | if (!result) {
192 | throw new Error('Invalid instruction processing');
193 | }
194 | expect(result.completed).toBe(false);
195 | expect(result.status).toBe('canceled');
196 | expect(result.output).toBe(output);
197 | })
198 | .catch(expectSuccess)
199 | .then(done);
200 | });
201 |
202 | it('triggers events on error navigations', (done) => {
203 | pipelineStep = (ctx: any, next: Next) => { throw new Error('test'); };
204 |
205 | router
206 | ._dequeueInstruction()
207 | .then(result => {
208 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:processing', processingResult);
209 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:error', completedResult);
210 | expect(ea.publish).toHaveBeenCalledWith('router:navigation:complete', completedResult);
211 | })
212 | .catch(expectSuccess)
213 | .then(done);
214 | });
215 |
216 | it('returns expected results from error navigations', (done) => {
217 | let output = new Error('test');
218 | pipelineStep = (ctx: any, next: Next) => next.reject(output);
219 |
220 | router._dequeueInstruction()
221 | .then(result => {
222 | if (!result) {
223 | throw new Error('Invalid instruction processing');
224 | }
225 | expect(result.completed).toBe(false);
226 | expect(result.status).toBe('rejected');
227 | expect(result.output).toBe(output);
228 | })
229 | .catch(expectSuccess)
230 | .then(done);
231 | });
232 | });
233 |
234 | describe('loadUrl', () => {
235 | it('restores previous location when route not found', (done) => {
236 | spyOn(history, 'navigate');
237 |
238 | router.history.previousLocation = 'prev';
239 | router.loadUrl('next')
240 | .then(result => {
241 | expect(result).toBeFalsy();
242 | expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true });
243 | })
244 | .catch(() => expect(true).toBeFalsy('should have succeeded'))
245 | .then(done);
246 | });
247 |
248 | it('navigate to fallback route when route not found and there is no previous location', (done) => {
249 | spyOn(history, 'navigate');
250 |
251 | router.history.previousLocation = null;
252 | router.fallbackRoute = 'fallback';
253 | router.loadUrl('next')
254 | .then(result => {
255 | expect(result).toBeFalsy();
256 | expect(history.navigate).toHaveBeenCalledWith('#/fallback', { trigger: true, replace: true });
257 | })
258 | .catch(result => expect(true).toBeFalsy('should have succeeded'))
259 | .then(done);
260 | });
261 |
262 | it('restores previous location on error', (done) => {
263 | spyOn(history, 'navigate');
264 |
265 | router.history.previousLocation = 'prev';
266 | router.activate();
267 | router.configure(config => {
268 | config.map([
269 | { name: 'test', route: '', moduleId: './test' }
270 | ]);
271 | return config;
272 | });
273 |
274 | router.loadUrl('next')
275 | .then(result => {
276 | expect(result).toBeFalsy();
277 | expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true });
278 | })
279 | .catch(() => expect(true).toBeFalsy('should have succeeded'))
280 | .then(done);
281 | });
282 | });
283 | describe('instruction completes as navigation command', () => {
284 | it('should complete instructions in order before terminating', done => {
285 | const pipeline = new Pipeline()
286 | .addStep({ run(inst: NavigationInstruction, next: Next) { return pipelineStep(inst, next); } });
287 | spyOn(pipeline, 'run').and.callThrough();
288 |
289 | const plProvider: PipelineProvider = {
290 | createPipeline: () => pipeline
291 | } as PipelineProvider;
292 | const router = new AppRouter(container, history, plProvider, ea);
293 | const initialInstruction = new MockInstruction('initial resulting navigation (Promise)');
294 | const instructionAfterNav = new MockInstruction('instruction after navigation');
295 |
296 | const navigationCommand: NavigationCommand = {
297 | navigate: () => new Promise(resolve => {
298 | setTimeout(() => {
299 | router._queue.push(instructionAfterNav);
300 | pipelineStep = (ctx: any, next: Next) => next.complete({});
301 | resolve();
302 | }, 0);
303 | })
304 | };
305 |
306 | router._queue.push(initialInstruction);
307 | pipelineStep = (ctx: any, next: Next) => next.complete(navigationCommand);
308 |
309 | router._dequeueInstruction()
310 | .then(_ => {
311 | expect(pipeline.run).toHaveBeenCalledTimes(2);
312 | expect((pipeline.run as jasmine.Spy).calls.argsFor(0)).toEqual([initialInstruction]);
313 | expect((pipeline.run as jasmine.Spy).calls.argsFor(1)).toEqual([instructionAfterNav]);
314 | done();
315 | });
316 | });
317 | });
318 | });
319 |
320 | function expectSuccess(result: any) {
321 | expect(result).not.toBe(result, 'should have succeeded');
322 | }
323 |
--------------------------------------------------------------------------------
/test/navigation-commands.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Redirect,
3 | RedirectToRoute,
4 | isNavigationCommand,
5 | NavigationCommand
6 | } from '../src/aurelia-router';
7 | import { MockRouter } from './shared';
8 |
9 | describe('isNavigationCommand', () => {
10 | it('should return true for object which has a navigate method', () => {
11 | let nc: NavigationCommand = {
12 | // tslint:disable-next-line
13 | navigate() { }
14 | };
15 |
16 | expect(isNavigationCommand(nc)).toBe(true);
17 | });
18 |
19 | it('should return false for everything that does not have a navigate method', () => {
20 | expect(isNavigationCommand(true)).toBe(false);
21 | expect(isNavigationCommand(1)).toBe(false);
22 | expect(isNavigationCommand({})).toBe(false);
23 | });
24 | });
25 |
26 | describe('Redirect', () => {
27 | it('should accept url in constructor and pass this url to passed router\'s navigate method as first parameter', () => {
28 | let testurl = 'http://aurelia.io/';
29 | let redirect = new Redirect(testurl);
30 | let mockrouter: MockRouter = {
31 | url: '',
32 | navigate(url: string) {
33 | this.url = url;
34 | }
35 | } as any;
36 |
37 | redirect.setRouter(mockrouter);
38 |
39 | expect(mockrouter.url).toBe('');
40 |
41 | redirect.navigate(mockrouter);
42 |
43 | expect(mockrouter.url).toBe(testurl);
44 | });
45 |
46 | it('should accept options in constructor to use the app router', () => {
47 | let testurl = 'http://aurelia.io/';
48 | let redirect = new Redirect(testurl, { useAppRouter: true });
49 | let mockrouter: MockRouter = {
50 | url: '',
51 | navigate(url: string) {
52 | this.url = url;
53 | }
54 | } as any;
55 | let mockapprouter: MockRouter = {
56 | url: '',
57 | navigate(url: string) {
58 | this.url = url;
59 | }
60 | } as any;
61 |
62 | redirect.setRouter(mockrouter);
63 |
64 | expect(mockapprouter.url).toBe('');
65 |
66 | redirect.navigate(mockapprouter);
67 |
68 | expect(mockrouter.url).toBe('');
69 | expect(mockapprouter.url).toBe(testurl);
70 | });
71 | });
72 |
73 | describe('RedirectToRoute', () => {
74 | it('should accept url in constructor and pass this url to passed router\'s navigate method as first parameter', () => {
75 | let testroute = 'test';
76 | let testparams = { id: 1 };
77 | let redirect = new RedirectToRoute(testroute, testparams);
78 | let mockrouter: MockRouter = {
79 | route: '',
80 | params: {},
81 | navigateToRoute(route: string, params: Record) {
82 | this.route = route;
83 | this.params = params;
84 | }
85 | } as any;
86 |
87 | redirect.setRouter(mockrouter);
88 |
89 | expect(mockrouter.route).toBe('');
90 | expect(mockrouter.params).toEqual({});
91 |
92 | redirect.navigate(mockrouter);
93 |
94 | expect(mockrouter.route).toBe(testroute);
95 | expect(mockrouter.params).toEqual(testparams);
96 | });
97 |
98 | it('should accept options in constructor to use the app router', () => {
99 | let testroute = 'test';
100 | let testparams = { id: 1 };
101 | let redirect = new RedirectToRoute(testroute, testparams, { useAppRouter: true });
102 | let mockrouter: MockRouter = {
103 | route: '',
104 | params: {},
105 | navigateToRoute(route: string, params: Record) {
106 | this.route = route;
107 | this.params = params;
108 | }
109 | } as any;
110 |
111 | let mockapprouter: MockRouter = {
112 | route: '',
113 | params: {},
114 | navigateToRoute(route: string, params: Record) {
115 | this.route = route;
116 | this.params = params;
117 | }
118 | } as any;
119 |
120 | redirect.setRouter(mockrouter);
121 |
122 | expect(mockapprouter.route).toBe('');
123 | expect(mockapprouter.params).toEqual({});
124 |
125 | redirect.navigate(mockapprouter);
126 |
127 | expect(mockrouter.route).toBe('');
128 | expect(mockrouter.params).toEqual({});
129 |
130 | expect(mockapprouter.route).toBe(testroute);
131 | expect(mockapprouter.params).toEqual(testparams);
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/test/navigation-instruction.spec.ts:
--------------------------------------------------------------------------------
1 | import { History } from 'aurelia-history';
2 | import { Container } from 'aurelia-dependency-injection';
3 | import { MockHistory } from './shared';
4 | import {
5 | AppRouter,
6 | Router,
7 | RouterConfiguration,
8 | PipelineProvider
9 | } from '../src/aurelia-router';
10 |
11 | import {
12 | ViewPortInstruction
13 | } from '../src/interfaces';
14 |
15 | // const absoluteRoot = 'http://aurelia.io/docs/';
16 |
17 | describe('NavigationInstruction', () => {
18 | let router: Router;
19 | let history: History;
20 |
21 | beforeEach(() => {
22 | history = new MockHistory() as any;
23 | router = new AppRouter(
24 | new Container(),
25 | history,
26 | new PipelineProvider(new Container()),
27 | null
28 | );
29 | });
30 |
31 | describe('build title', () => {
32 | let child: Router;
33 | let config: RouterConfiguration;
34 | beforeEach(() => {
35 | router.addRoute({
36 | name: 'parent',
37 | route: 'parent',
38 | moduleId: 'parent',
39 | title: 'parent',
40 | nav: true
41 | });
42 | child = router.createChild(new Container());
43 | child.addRoute({
44 | name: 'child',
45 | route: 'child',
46 | moduleId: 'child',
47 | title: 'child',
48 | nav: true
49 | });
50 | config = new RouterConfiguration();
51 | spyOn(history, 'setTitle');
52 | });
53 |
54 | it('should generate a title from the nav model', (done) => {
55 | router._createNavigationInstruction('parent/child').then((instruction) => {
56 | child._createNavigationInstruction(instruction.getWildcardPath(), instruction).then((childInstruction) => {
57 | instruction.viewPortInstructions['default'] = {
58 | childNavigationInstruction: childInstruction
59 | } as ViewPortInstruction;
60 | instruction._updateTitle();
61 | expect(history.setTitle).toHaveBeenCalledWith('child | parent');
62 | expect(history.setTitle).not.toHaveBeenCalledWith('parent | child');
63 | done();
64 | });
65 | });
66 | });
67 |
68 | it('should use a router title when generating the page title', (done) => {
69 | config.title = 'app';
70 | router.configure(config);
71 | router._createNavigationInstruction('parent/child').then((instruction) => {
72 | child._createNavigationInstruction(instruction.getWildcardPath(), instruction).then((childInstruction) => {
73 | instruction.viewPortInstructions['default'] = {
74 | childNavigationInstruction: childInstruction
75 | } as ViewPortInstruction;
76 | instruction._updateTitle();
77 | expect(history.setTitle).toHaveBeenCalledWith('child | parent | app');
78 | expect(history.setTitle).not.toHaveBeenCalledWith('parent | child | app');
79 | done();
80 | });
81 | });
82 | });
83 |
84 | it('should use a configured title separator when generating a title', (done) => {
85 | config.titleSeparator = ' <3 ';
86 | router.configure(config);
87 | router._createNavigationInstruction('parent/child').then((instruction) => {
88 | child._createNavigationInstruction(instruction.getWildcardPath(), instruction).then((childInstruction) => {
89 | instruction.viewPortInstructions['default'] = {
90 | childNavigationInstruction: childInstruction
91 | } as ViewPortInstruction;
92 | instruction._updateTitle();
93 | expect(history.setTitle).toHaveBeenCalledWith('child <3 parent');
94 | expect(history.setTitle).not.toHaveBeenCalledWith('child 3 parent');
95 | done();
96 | });
97 | });
98 | });
99 | });
100 |
101 | describe('getBaseUrl()', () => {
102 | let child: Router;
103 | const parentRouteName = 'parent';
104 | const parentParamRouteName = 'parent/:parent';
105 | const childRouteName = 'child';
106 | const childParamRouteName = 'child/:child';
107 |
108 | beforeEach(() => {
109 | router.addRoute({
110 | name: parentRouteName,
111 | route: '',
112 | moduleId: parentRouteName
113 | });
114 | router.addRoute({
115 | name: parentRouteName,
116 | route: parentRouteName,
117 | moduleId: parentRouteName
118 | });
119 | router.addRoute({
120 | name: parentParamRouteName,
121 | route: parentParamRouteName,
122 | moduleId: parentRouteName
123 | });
124 | child = router.createChild(new Container());
125 | child.addRoute({
126 | name: childRouteName,
127 | route: childRouteName,
128 | moduleId: childRouteName
129 | });
130 | child.addRoute({
131 | name: childParamRouteName,
132 | route: childParamRouteName,
133 | moduleId: childRouteName
134 | });
135 | });
136 |
137 | it('should return the raw fragment when no params exist', (done) => {
138 | router._createNavigationInstruction(parentRouteName).then(instruction => {
139 | expect(instruction.getBaseUrl()).toBe(parentRouteName);
140 | done();
141 | })
142 | .catch(fail);
143 | });
144 |
145 | it('should return the raw fragment when no wildcard exists', (done) => {
146 | router._createNavigationInstruction(parentRouteName).then(instruction => {
147 | instruction.params = { fake: 'fakeParams' };
148 | expect(instruction.getBaseUrl()).toBe(parentRouteName);
149 | done();
150 | })
151 | .catch(fail);
152 | });
153 |
154 | describe('when a uri contains spaces', () => {
155 | it('should handle an encoded uri', (done) => {
156 | router._createNavigationInstruction('parent/parent%201').then(instruction => {
157 | expect(instruction.getBaseUrl()).toBe('parent/parent%201');
158 | done();
159 | })
160 | .catch(fail);
161 | });
162 |
163 | it('should encode the uri', (done) => {
164 | router._createNavigationInstruction('parent/parent 1').then(instruction => {
165 | expect(instruction.getBaseUrl()).toBe('parent/parent%201');
166 | done();
167 | })
168 | .catch(fail);
169 | });
170 |
171 | it('should identify encoded fragments', (done) => {
172 | router._createNavigationInstruction('parent/parent%201/child/child%201').then(instruction => {
173 | expect(instruction.getBaseUrl()).toBe('parent/parent%201/');
174 | done();
175 | })
176 | .catch(fail);
177 | });
178 |
179 | it('should identify fragments and encode them', (done) => {
180 | router._createNavigationInstruction('parent/parent 1/child/child 1').then(instruction => {
181 | expect(instruction.getBaseUrl()).toBe('parent/parent%201/');
182 | done();
183 | })
184 | .catch(fail);
185 | });
186 | });
187 |
188 | describe('when using an empty parent route', () => {
189 | it('should return the non-empty matching parent route', (done) => {
190 | router._createNavigationInstruction('').then(parentInstruction => {
191 | router.currentInstruction = parentInstruction;
192 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
193 | child._createNavigationInstruction(childRouteName, parentInstruction).then(instruction => {
194 | expect(child.baseUrl).toBe(parentRouteName);
195 | done();
196 | });
197 | })
198 | .catch(fail);
199 | });
200 | });
201 |
202 | describe('when using an named parent route', () => {
203 | it('should return the non-empty matching parent route', (done) => {
204 | router._createNavigationInstruction(parentRouteName).then(parentInstruction => {
205 | router.currentInstruction = parentInstruction;
206 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
207 | child._createNavigationInstruction(childRouteName, parentInstruction).then(instruction => {
208 | expect(child.baseUrl).toBe(parentRouteName);
209 | done();
210 | });
211 | })
212 | .catch(fail);
213 | });
214 | });
215 |
216 | it('should update the base url when generating navigation instructions', (done) => {
217 | router._createNavigationInstruction(parentRouteName).then(parentInstruction => {
218 | router.currentInstruction = parentInstruction;
219 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
220 | child._createNavigationInstruction(childRouteName, parentInstruction).then(instruction => {
221 | expect(child.baseUrl).toBe(parentRouteName);
222 | done();
223 | });
224 | })
225 | .catch(fail);
226 | });
227 | });
228 | });
229 |
--------------------------------------------------------------------------------
/test/navigation-plan.spec.ts:
--------------------------------------------------------------------------------
1 | import { Container } from 'aurelia-dependency-injection';
2 | import {
3 | AppRouter,
4 | Router,
5 | PipelineProvider,
6 | Redirect,
7 | NavigationInstruction,
8 | NavigationInstructionInit,
9 | BuildNavigationPlanStep,
10 | RouteConfig
11 | } from '../src/aurelia-router';
12 | import { MockHistory, createPipelineState, MockPipelineState } from './shared';
13 |
14 | describe('NavigationPlanStep', () => {
15 | let step: BuildNavigationPlanStep;
16 | let state: MockPipelineState;
17 | let redirectInstruction: NavigationInstruction;
18 | let firstInstruction: NavigationInstruction;
19 | let sameAsFirstInstruction: NavigationInstruction;
20 | let secondInstruction: NavigationInstruction;
21 | let router: Router;
22 | let child: Router;
23 | let grandchild: Router;
24 |
25 | beforeEach(() => {
26 | step = new BuildNavigationPlanStep();
27 | state = createPipelineState();
28 | router = new AppRouter(
29 | new Container(),
30 | new MockHistory(),
31 | new PipelineProvider(new Container()),
32 | null
33 | );
34 | router.useViewPortDefaults({ default: { moduleId: null } });
35 | child = router.createChild(new Container());
36 | grandchild = child.createChild(new Container());
37 |
38 | redirectInstruction = new NavigationInstruction({
39 | fragment: 'first',
40 | queryString: 'q=1',
41 | config: { redirect: 'second' },
42 | router
43 | } as NavigationInstructionInit);
44 |
45 | firstInstruction = new NavigationInstruction({
46 | fragment: 'first',
47 | config: { viewPorts: { default: { moduleId: './first' } } } as RouteConfig,
48 | params: { id: '1' },
49 | router
50 | } as NavigationInstructionInit);
51 |
52 | sameAsFirstInstruction = new NavigationInstruction({
53 | fragment: 'first',
54 | config: { viewPorts: { default: { moduleId: './first' } } } as RouteConfig,
55 | previousInstruction: firstInstruction,
56 | params: { id: '1' },
57 | router
58 | } as NavigationInstructionInit);
59 |
60 | secondInstruction = new NavigationInstruction({
61 | fragment: 'second',
62 | config: { viewPorts: { default: { moduleId: './second' } } } as RouteConfig,
63 | previousInstruction: firstInstruction,
64 | router
65 | } as NavigationInstructionInit);
66 | });
67 |
68 | it('cancels on redirect configs', (done) => {
69 | redirectInstruction.router.addRoute({ route: 'first', name: 'first', redirect: 'second' });
70 | redirectInstruction.router.addRoute({ route: 'second', name: 'second', redirect: 'second' });
71 | step.run(redirectInstruction, state.next)
72 | .then(e => {
73 | expect(state.rejection).toBeTruthy();
74 | expect(e instanceof Redirect).toBe(true);
75 | expect(e.url).toBe('#/second?q=1');
76 | done();
77 | });
78 | });
79 |
80 | it('redirects to routes without names', (done) => {
81 | const url = 'first/10?q=1';
82 | const from = { route: 'first', redirect: 'second' };
83 | const to = { route: 'second', moduleId: './second' };
84 | router.addRoute(from);
85 | router.addRoute(to);
86 | router._createNavigationInstruction(url).then((instruction) => {
87 | step.run(instruction, state.next)
88 | .then(e => {
89 | expect(state.rejection).toBeTruthy();
90 | expect(e instanceof Redirect).toBe(true);
91 | expect(e.url).toBe(`#/second?q=1`);
92 | done();
93 | });
94 | });
95 | });
96 |
97 | it('redirects to routes with static parameters', (done) => {
98 | const url = 'first/10?q=1';
99 | const from = { name: 'first', route: 'first/:id', redirect: 'second/0' };
100 | const to = { name: 'second', route: 'second/:id', moduleId: './second' };
101 |
102 | router.addRoute(from);
103 | router.addRoute(to);
104 | router._createNavigationInstruction(url).then((instruction) => {
105 | step.run(instruction, state.next)
106 | .then(e => {
107 | expect(state.rejection).toBeTruthy();
108 | expect(e instanceof Redirect).toBe(true);
109 | expect(e.url).toBe(`#/second/0?q=1`);
110 | done();
111 | });
112 | });
113 | });
114 |
115 | it('redirects to routes with dynamic parameters', (done) => {
116 | const url = 'first/10?q=1';
117 | const from = { name: 'first', route: 'first/:this', redirect: 'second/:this' };
118 | const to = { name: 'second', route: 'second/:that', moduleId: './second' };
119 |
120 | router.addRoute(from);
121 | router.addRoute(to);
122 | router._createNavigationInstruction(url).then((instruction) => {
123 | step.run(instruction, state.next)
124 | .then(e => {
125 | expect(state.rejection).toBeTruthy();
126 | expect(e instanceof Redirect).toBe(true);
127 | expect(e.url).toBe(`#/second/10?q=1`);
128 | done();
129 | });
130 | });
131 | });
132 |
133 | it('redirects and drops unused dynamic parameters', (done) => {
134 | const url = 'first/10/20?q=1';
135 | const from = { name: 'first', route: 'first/:this/:that', redirect: 'second/:that' };
136 | const to = { name: 'second', route: 'second/:id', moduleId: './second' };
137 |
138 | router.addRoute(from);
139 | router.addRoute(to);
140 | router._createNavigationInstruction(url).then((instruction) => {
141 | step.run(instruction, state.next)
142 | .then(e => {
143 | expect(state.rejection).toBeTruthy();
144 | expect(e instanceof Redirect).toBe(true);
145 | expect(e.url).toBe(`#/second/20?q=1`);
146 | done();
147 | });
148 | });
149 | });
150 |
151 | it('redirects and ignores invalid dynamic parameters', (done) => {
152 | const url = 'first/20?q=1';
153 | const from = { name: 'first', route: 'first/:this', redirect: 'second/:that' };
154 | const to = { name: 'second', route: 'second/:that?', moduleId: './second' };
155 |
156 | router.addRoute(from);
157 | router.addRoute(to);
158 | router._createNavigationInstruction(url).then((instruction) => {
159 | step.run(instruction, state.next)
160 | .then(e => {
161 | expect(state.rejection).toBeTruthy();
162 | expect(e instanceof Redirect).toBe(true);
163 | expect(e.url).toBe(`#/second?q=1`);
164 | done();
165 | });
166 | });
167 | });
168 |
169 | it('redirects unknown routes to statically configured routes', async (done) => {
170 | const url = 'nowhere';
171 | const from = { route: 'first', moduleId: './first' };
172 | const to = { route: 'second', moduleId: './second' };
173 |
174 | await router.configure((config) => config.map([from, to]).mapUnknownRoutes({ redirect: 'second' }));
175 | router.navigate('first');
176 | const instruction = await router._createNavigationInstruction(url);
177 | const result = await step.run(instruction, state.next);
178 | expect(state.rejection).toBeTruthy();
179 | expect(result instanceof Redirect).toBe(true);
180 | expect(result.url).toBe(`#/second`);
181 | done();
182 | });
183 |
184 | it('redirects unknown routes to dynamically configured routes', async (done) => {
185 | const url = 'nowhere';
186 | const from = { route: 'first', moduleId: './first' };
187 | const to = { route: 'second', moduleId: './second' };
188 |
189 | await router.configure((config) => config.map([from, to]).mapUnknownRoutes(() => { return { redirect: 'second' }; }));
190 | router.navigate('first');
191 | const instruction = await router._createNavigationInstruction(url);
192 | const result = await step.run(instruction, state.next);
193 | expect(state.rejection).toBeTruthy();
194 | expect(result instanceof Redirect).toBe(true);
195 | expect(result.url).toBe(`#/second`);
196 | done();
197 | });
198 |
199 | it('redirects unknown routes to statically configured child routes', async (done) => {
200 | const url = 'nowhere';
201 | const base = { route: 'home', moduleId: './home' };
202 | const from = { route: 'first', moduleId: './first' };
203 | const to = { route: 'second', moduleId: './second' };
204 |
205 | await router.configure((config) => config.map([base]).mapUnknownRoutes({ redirect: 'home/second' }));
206 | child.configure(config => config.map([from, to]));
207 | router.navigate('first');
208 | const parentInstruction = await router._createNavigationInstruction(url);
209 | const childInstruction = await child._createNavigationInstruction(parentInstruction.getWildcardPath(), parentInstruction);
210 | const result = await step.run(childInstruction, state.next);
211 | expect(state.rejection).toBeTruthy();
212 | expect(result instanceof Redirect).toBe(true);
213 | expect(result.url).toBe(`#/home/second`);
214 | done();
215 | });
216 |
217 | it('redirects unknown routes to dynamically configured child routes', async (done) => {
218 | const url = 'nowhere';
219 | const base = { route: 'home', moduleId: './home' };
220 | const from = { route: 'first', moduleId: './first' };
221 | const to = { route: 'second', moduleId: './second' };
222 |
223 | await router.configure((config) => config.map([base]).mapUnknownRoutes(() => { return { redirect: 'home/second' }; }));
224 | child.configure(config => config.map([from, to]));
225 | router.navigate('first');
226 | const parentInstruction = await router._createNavigationInstruction(url);
227 | const childInstruction = await child._createNavigationInstruction(parentInstruction.getWildcardPath(), parentInstruction);
228 | const result = await step.run(childInstruction, state.next);
229 | expect(state.rejection).toBeTruthy();
230 | expect(result instanceof Redirect).toBe(true);
231 | expect(result.url).toBe(`#/home/second`);
232 | done();
233 | });
234 |
235 | it('redirects children', (done) => {
236 | const url = 'home/first';
237 | const base = { name: 'home', route: 'home', moduleId: './home' };
238 | const from = { name: 'first', route: 'first', redirect: 'second' };
239 | const to = { name: 'second', route: 'second', moduleId: './second' };
240 |
241 | router.addRoute(base);
242 | child.configure(config => config.map([from, to]));
243 | router.navigate('home');
244 | router._createNavigationInstruction(url).then((parentInstruction) => {
245 | child._createNavigationInstruction(parentInstruction.getWildcardPath(), parentInstruction).then(childInstruction => {
246 | step.run(childInstruction, state.next)
247 | .then(e => {
248 | expect(state.rejection).toBeTruthy();
249 | expect(e instanceof Redirect).toBe(true);
250 | expect(e.url).toBe(`#/home/second`);
251 | done();
252 | });
253 | });
254 | });
255 | });
256 |
257 | it('redirects from parents to children', (done) => {
258 | const url = 'home/shortcut';
259 | const one = { name: 'first', route: 'home', moduleId: './one' };
260 | const two = { name: 'second', route: 'two', moduleId: './two' };
261 | const three = { name: 'third', route: 'three', moduleId: './three' };
262 | const to = { name: 'shortcut', route: 'shortcut', redirect: 'two/three' };
263 |
264 | router.addRoute(one);
265 | child.addRoute(two);
266 | child.addRoute(to);
267 | grandchild.addRoute(three);
268 | router.navigate('home');
269 | router._createNavigationInstruction(url).then((parentInstruction) => {
270 | child._createNavigationInstruction(parentInstruction.getWildcardPath(), parentInstruction).then(childInstruction => {
271 | step.run(childInstruction, state.next)
272 | .then(e => {
273 | expect(state.rejection).toBeTruthy();
274 | expect(e instanceof Redirect).toBe(true);
275 | expect(e.url).toBe('#/home/two/three');
276 | done();
277 | });
278 | });
279 | });
280 | });
281 |
282 | it('redirects from parents to grandchildren', (done) => {
283 | const url = 'shortcut';
284 | const one = { name: 'first', route: ['home', 'one'], moduleId: './one' };
285 | const two = { name: 'second', route: 'two', moduleId: './two' };
286 | const three = { name: 'third', route: 'three', moduleId: './three' };
287 | const to = { name: 'shortcut', route: 'shortcut', redirect: 'one/two/three' };
288 |
289 | router.addRoute(one);
290 | router.addRoute(to);
291 | child.addRoute(two);
292 | grandchild.addRoute(three);
293 | router.navigate('one');
294 | router._createNavigationInstruction(url).then((instruction) => {
295 | step.run(instruction, state.next)
296 | .then(e => {
297 | expect(state.rejection).toBeTruthy();
298 | expect(e instanceof Redirect).toBe(true);
299 | expect(e.url).toBe('#/one/two/three');
300 | done();
301 | });
302 | });
303 | });
304 |
305 | it('redirects from parents to grandchildren with static params', (done) => {
306 | const url = 'shortcut';
307 | const one = { name: 'first', route: ['home', 'one'], moduleId: './one' };
308 | const two = { name: 'second/:id', route: 'two', moduleId: './two' };
309 | const three = { name: 'third/:id', route: 'three', moduleId: './three' };
310 | const to = { name: 'shortcut', route: 'shortcut', redirect: 'one/two/2/three/3' };
311 |
312 | router.addRoute(one);
313 | router.addRoute(to);
314 | child.addRoute(two);
315 | grandchild.addRoute(three);
316 | router.navigate('one');
317 | router._createNavigationInstruction(url).then((instruction) => {
318 | step.run(instruction, state.next)
319 | .then(e => {
320 | expect(state.rejection).toBeTruthy();
321 | expect(e instanceof Redirect).toBe(true);
322 | expect(e.url).toBe('#/one/two/2/three/3');
323 | done();
324 | });
325 | });
326 | });
327 |
328 | it('redirects from parents to grandchildren with dynamic params', (done) => {
329 | const url = 'shortcut/1/2';
330 | const one = { name: 'first', route: ['home', 'one'], moduleId: './one' };
331 | const two = { name: 'second/:id', route: 'two', moduleId: './two' };
332 | const three = { name: 'third/:id', route: 'three', moduleId: './three' };
333 | const to = { name: 'shortcut', route: 'shortcut/:second/:third', redirect: 'one/two/:second/three/:third' };
334 |
335 | router.addRoute(one);
336 | router.addRoute(to);
337 | child.addRoute(two);
338 | grandchild.addRoute(three);
339 | router.navigate('one');
340 | router._createNavigationInstruction(url).then((instruction) => {
341 | step.run(instruction, state.next)
342 | .then(e => {
343 | expect(state.rejection).toBeTruthy();
344 | expect(e instanceof Redirect).toBe(true);
345 | expect(e.url).toBe('#/one/two/1/three/2');
346 | done();
347 | });
348 | });
349 | });
350 |
351 | it('redirects children with static parameters', (done) => {
352 | const url = 'home/first/0';
353 | const base = { name: 'home', route: 'home', moduleId: './home' };
354 | const from = { name: 'first', route: 'first/:id', redirect: 'second/1' };
355 | const to = { name: 'second', route: 'second/:id', moduleId: './second' };
356 |
357 | router.addRoute(base);
358 | child.configure(config => config.map([from, to]));
359 | router.navigate('home');
360 | router._createNavigationInstruction(url).then((parentInstruction) => {
361 | child._createNavigationInstruction(parentInstruction.getWildcardPath(), parentInstruction).then(childInstruction => {
362 | step.run(childInstruction, state.next)
363 | .then(e => {
364 | expect(state.rejection).toBeTruthy();
365 | expect(e instanceof Redirect).toBe(true);
366 | expect(e.url).toBe(`#/home/second/1`);
367 | done();
368 | });
369 | });
370 | });
371 | });
372 |
373 | it('redirects children with dynamic parameters', (done) => {
374 | const url = 'home/first/1';
375 | const base = { name: 'home', route: 'home', moduleId: './home' };
376 | const from = { name: 'first', route: 'first/:id', redirect: 'second/:id' };
377 | const to = { name: 'second', route: 'second/:id', moduleId: './second' };
378 |
379 | router.addRoute(base);
380 | child.configure(config => config.map([from, to]));
381 | router.navigate('home');
382 | router._createNavigationInstruction(url).then((parentInstruction) => {
383 | child._createNavigationInstruction(parentInstruction.getWildcardPath(), parentInstruction).then(childInstruction => {
384 | step.run(childInstruction, state.next)
385 | .then(e => {
386 | expect(state.rejection).toBeTruthy();
387 | expect(e instanceof Redirect).toBe(true);
388 | expect(e.url).toBe(`#/home/second/1`);
389 | done();
390 | });
391 | });
392 | });
393 | });
394 |
395 | describe('generates navigation plans', () => {
396 | it('with no prev step', (done) => {
397 | step.run(firstInstruction, state.next)
398 | .then(() => {
399 | expect(state.result).toBe(true);
400 | expect(firstInstruction.plan).toBeTruthy();
401 | done();
402 | });
403 | });
404 |
405 | it('with prev step', (done) => {
406 | step.run(secondInstruction, state.next)
407 | .then(() => {
408 | expect(state.result).toBe(true);
409 | expect(secondInstruction.plan).toBeTruthy();
410 | done();
411 | });
412 | });
413 |
414 | it('with prev step with viewport', (done) => {
415 | firstInstruction.addViewPortInstruction('default', 'no-change', './first', {});
416 |
417 | step.run(secondInstruction, state.next)
418 | .then(() => {
419 | expect(state.result).toBe(true);
420 | expect(secondInstruction.plan).toBeTruthy();
421 | done();
422 | });
423 | });
424 | });
425 |
426 | describe('activation strategy', () => {
427 | it('is replace when moduleId changes', (done) => {
428 | firstInstruction.addViewPortInstruction('default', 'no-change', './first', {});
429 |
430 | step.run(secondInstruction, state.next)
431 | .then(() => {
432 | expect(state.result).toBe(true);
433 | expect(secondInstruction.plan.default.strategy).toBe('replace');
434 | done();
435 | });
436 | });
437 |
438 | it('is no-change when nothing changes', (done) => {
439 | firstInstruction.addViewPortInstruction('default', 'ignored' as any, './first', { viewModel: {} });
440 |
441 | step.run(sameAsFirstInstruction, state.next)
442 | .then(() => {
443 | expect(state.result).toBe(true);
444 | expect(sameAsFirstInstruction.plan.default.strategy).toBe('no-change');
445 | done();
446 | });
447 | });
448 |
449 | it('can be determined by route config', (done) => {
450 | sameAsFirstInstruction.config.activationStrategy = 'fake-strategy' as any;
451 | firstInstruction.addViewPortInstruction('default', 'ignored' as any, './first', { viewModel: {} });
452 |
453 | step.run(sameAsFirstInstruction, state.next)
454 | .then(() => {
455 | expect(state.result).toBe(true);
456 | expect(sameAsFirstInstruction.plan.default.strategy).toBe('fake-strategy');
457 | done();
458 | });
459 | });
460 |
461 | it('can be determined by view model', (done) => {
462 | let viewModel = { determineActivationStrategy: () => 'vm-strategy' };
463 | firstInstruction.addViewPortInstruction('default', 'ignored' as any, './first', { viewModel });
464 |
465 | step.run(sameAsFirstInstruction, state.next)
466 | .then(() => {
467 | expect(state.result).toBe(true);
468 | expect(sameAsFirstInstruction.plan.default.strategy).toBe('vm-strategy');
469 | done();
470 | });
471 | });
472 |
473 | it('is invoke-lifecycle when only params change', (done) => {
474 | firstInstruction.params = { id: '1' };
475 | sameAsFirstInstruction.params = { id: '2' };
476 | firstInstruction.addViewPortInstruction('default', 'ignored' as any, './first', { viewModel: {} });
477 |
478 | step.run(sameAsFirstInstruction, state.next)
479 | .then(() => {
480 | expect(state.result).toBe(true);
481 | expect(sameAsFirstInstruction.plan.default.strategy).toBe('invoke-lifecycle');
482 | done();
483 | });
484 | });
485 |
486 | it('is invoke-lifecycle when query params change and ignoreQueryParams is false', (done) => {
487 | firstInstruction.queryParams = { param: 'foo' };
488 | sameAsFirstInstruction.queryParams = { param: 'bar' };
489 | sameAsFirstInstruction.options.compareQueryParams = true;
490 | firstInstruction.addViewPortInstruction('default', 'ignored' as any, './first', { viewModel: {} });
491 |
492 | step.run(sameAsFirstInstruction, state.next)
493 | .then(() => {
494 | expect(state.result).toBe(true);
495 | expect(sameAsFirstInstruction.plan.default.strategy).toBe('invoke-lifecycle');
496 | done();
497 | });
498 | });
499 | });
500 | });
501 |
--------------------------------------------------------------------------------
/test/pipeline.spec.ts:
--------------------------------------------------------------------------------
1 | import { Pipeline, PipelineStep, Next, NavigationInstruction } from '../src/aurelia-router';
2 | import { PipelineStatus } from '../src/pipeline-status';
3 | import { IPipelineSlot, StepRunnerFunction } from '../src/interfaces';
4 |
5 | describe('Pipeline', function() {
6 | let pipeline: Pipeline;
7 |
8 | beforeEach(function() {
9 | pipeline = new Pipeline();
10 | });
11 |
12 | describe('addStep', function() {
13 | it('adds function', () => {
14 | const runnerFn = function() {/**/} as StepRunnerFunction;
15 | pipeline.addStep(runnerFn);
16 | expect(pipeline.steps[0]).toBe(runnerFn);
17 | pipeline.addStep(runnerFn);
18 | expect(pipeline.steps[1]).toBe(runnerFn);
19 | });
20 |
21 | it('adds PipelineStep', async () => {
22 | let stepRunContext: any = {};
23 | const stepResult: any = {};
24 | const step: PipelineStep = {
25 | run() {
26 | // eslint-disable-next-line @typescript-eslint/no-this-alias
27 | stepRunContext = this;
28 | return Promise.resolve(stepResult);
29 | }
30 | };
31 | pipeline.addStep(step);
32 | expect(typeof pipeline.steps[0]).toBe('function');
33 | expect(await pipeline.steps[0](null, null)).toBe(stepResult);
34 | expect(stepRunContext).toBe(step);
35 |
36 | stepRunContext = {};
37 | pipeline.addStep(step);
38 | expect(typeof pipeline.steps[1]).toBe('function');
39 | expect(await pipeline.steps[1](null, null)).toBe(stepResult);
40 | expect(stepRunContext).toBe(step);
41 | });
42 |
43 | it('adds IPipelineSlot', () => {
44 | let callCount = 0;
45 | const steps: (PipelineStep & { result: any })[] = [
46 | {
47 | result: {},
48 | run() {
49 | callCount++;
50 | return this.result;
51 | }
52 | },
53 | {
54 | result: {},
55 | run() {
56 | callCount++;
57 | return this.result;
58 | }
59 | },
60 | {
61 | result: {},
62 | run() {
63 | callCount++;
64 | return this.result;
65 | }
66 | }
67 | ];
68 | const step: IPipelineSlot = {
69 | getSteps() {
70 | return steps;
71 | }
72 | };
73 | const spy = spyOn(pipeline, 'addStep').and.callThrough();
74 | pipeline.addStep(step);
75 | expect(pipeline.steps.length).toBe(3);
76 | const [step0, step1, step2] = pipeline.steps;
77 | for (const stepX of [step0, step1, step2]) {
78 | let idx = pipeline.steps.indexOf(stepX);
79 | expect(typeof stepX).toBe('function');
80 | expect(stepX(null, null)).toBe(steps[idx].result);
81 | }
82 | expect(spy).toHaveBeenCalledTimes(4);
83 | expect(callCount).toBe(3);
84 | });
85 | });
86 |
87 | describe('run', function() {
88 | let navInstruction: NavigationInstruction;
89 |
90 | beforeEach(() => {
91 | navInstruction = {} as any;
92 | });
93 |
94 | // { status, output, completed: status === pipelineStatus.completed }
95 | it('runs to "completed" when there is no step', async () => {
96 | let result = await pipeline.run(navInstruction);
97 | expect(result.status).toBe(PipelineStatus.Completed);
98 | expect(result.completed).toBe(true);
99 | });
100 |
101 | it('runs', async () => {
102 | const fragment: any = {};
103 | const step: PipelineStep = {
104 | run(nav: NavigationInstruction, next: Next): Promise {
105 | nav.fragment = fragment;
106 | return next();
107 | }
108 | };
109 | pipeline.addStep(step);
110 | const result = await pipeline.run(navInstruction);
111 | expect(navInstruction.fragment).toBe(fragment);
112 | expect(result.status).toBe(PipelineStatus.Completed);
113 | expect(result.completed).toBe(true);
114 | });
115 |
116 | describe('Errors / Rejection / Completion / Cancel', () => {
117 | it('completes with "rejected" status when a step throw', async () => {
118 | let firstCalled = 0;
119 | let secondCalled = 0;
120 | let thirdCalled = 0;
121 | const steps: PipelineStep[] = [
122 | {
123 | run(nav: NavigationInstruction, next: Next) {
124 | firstCalled = 1;
125 | return next();
126 | }
127 | },
128 | {
129 | run() {
130 | secondCalled = 1;
131 | throw new Error('Invalid run.');
132 | }
133 | },
134 | {
135 | run(nav: NavigationInstruction, next: Next) {
136 | thirdCalled = 1;
137 | return next();
138 | }
139 | }
140 | ];
141 | for (const step of steps) {
142 | pipeline.addStep(step);
143 | }
144 | const result = await pipeline.run(navInstruction);
145 | expect(firstCalled).toBe(1);
146 | expect(secondCalled).toBe(1);
147 | expect(thirdCalled).toBe(0);
148 | expect(result.status).toBe(PipelineStatus.Rejected);
149 | });
150 |
151 | it('completes with "rejected" status when a step invokes reject()', async () => {
152 | let firstCalled = 0;
153 | let secondCalled = 0;
154 | let thirdCalled = 0;
155 | const steps: PipelineStep[] = [
156 | {
157 | run(nav: NavigationInstruction, next: Next) {
158 | firstCalled = 1;
159 | return next();
160 | }
161 | },
162 | {
163 | run(nav: NavigationInstruction, next: Next) {
164 | secondCalled = 1;
165 | return next.reject(new Error('Invalid abcdef ắếốộ'));
166 | }
167 | },
168 | {
169 | run(nav: NavigationInstruction, next: Next) {
170 | thirdCalled = 1;
171 | return next();
172 | }
173 | }
174 | ];
175 | for (const step of steps) {
176 | pipeline.addStep(step);
177 | }
178 | const result = await pipeline.run(navInstruction);
179 | expect(firstCalled).toBe(1);
180 | expect(secondCalled).toBe(1);
181 | expect(thirdCalled).toBe(0);
182 | expect(result.status).toBe(PipelineStatus.Rejected);
183 | expect(result.output.toString()).toContain('Invalid abcdef ắếốộ');
184 | });
185 |
186 | it('completes with "completed" status when a step invokes complete()', async () => {
187 | let firstCalled = 0;
188 | let secondCalled = 0;
189 | let thirdCalled = 0;
190 | const steps: PipelineStep[] = [
191 | {
192 | run(nav: NavigationInstruction, next: Next) {
193 | firstCalled = 1;
194 | return next.complete(new Error('Valid ắếốộắếốộắếốộ'));
195 | }
196 | },
197 | {
198 | run(nav: NavigationInstruction, next: Next) {
199 | secondCalled = 1;
200 | return next.reject(new Error('Invalid abcdef ắếốộ'));
201 | }
202 | },
203 | {
204 | run(nav: NavigationInstruction, next: Next) {
205 | thirdCalled = 1;
206 | return next();
207 | }
208 | }
209 | ];
210 | for (const step of steps) {
211 | pipeline.addStep(step);
212 | }
213 | const result = await pipeline.run(navInstruction);
214 | expect(firstCalled).toBe(1);
215 | expect(secondCalled).toBe(0);
216 | expect(thirdCalled).toBe(0);
217 | expect(result.status).toBe(PipelineStatus.Completed);
218 | expect(result.output.toString()).toBe(new Error('Valid ắếốộắếốộắếốộ').toString());
219 | });
220 |
221 | it('completes with "canceled" status when a step invokes cancel()', async () => {
222 | let firstCalled = 0;
223 | let secondCalled = 0;
224 | let thirdCalled = 0;
225 | const steps: PipelineStep[] = [
226 | {
227 | run(nav: NavigationInstruction, next: Next) {
228 | firstCalled = 1;
229 | return next.cancel(new Error('Valid ắếốộắếốộắếốộ'));
230 | }
231 | },
232 | {
233 | run(nav: NavigationInstruction, next: Next) {
234 | secondCalled = 1;
235 | return next.reject(new Error('Invalid abcdef ắếốộ'));
236 | }
237 | },
238 | {
239 | run(nav: NavigationInstruction, next: Next) {
240 | thirdCalled = 1;
241 | return next();
242 | }
243 | }
244 | ];
245 | for (const step of steps) {
246 | pipeline.addStep(step);
247 | }
248 | const result = await pipeline.run(navInstruction);
249 | expect(firstCalled).toBe(1);
250 | expect(secondCalled).toBe(0);
251 | expect(thirdCalled).toBe(0);
252 | expect(result.status).toBe(PipelineStatus.Canceled);
253 | expect(result.output.toString()).toBe(new Error('Valid ắếốộắếốộắếốộ').toString());
254 | });
255 | });
256 |
257 | });
258 | });
259 |
--------------------------------------------------------------------------------
/test/route-config-validation.spec.ts:
--------------------------------------------------------------------------------
1 | import { validateRouteConfig } from '../src/router';
2 | import { RouteConfig } from '../src/interfaces';
3 |
4 | describe('RouteConfig validation', () => {
5 | let routeConfig: RouteConfig;
6 |
7 | it('ensures object', function _1_validateConfigObject__Tests() {
8 | [undefined, '', 5, Symbol(), function() {/**/}].forEach((v: any) => {
9 | expect(() => validateRouteConfig(v)).toThrowError('Invalid Route Config');
10 | });
11 | });
12 |
13 | describe('"route" validation', function _2_validateRouteProperty__Tests() {
14 | beforeEach(() => {
15 | routeConfig = {
16 | moduleId: 'a.js'
17 | } as RouteConfig;
18 | });
19 |
20 | it('throws when "route" is not a string', () => {
21 | [undefined, null, 5, Symbol(), function() {/**/}].forEach((v: any) => {
22 | routeConfig.route = v;
23 | expect(() => validateRouteConfig(routeConfig)).toThrowError(/You must specify a "route\:" pattern/);
24 | });
25 | });
26 |
27 | it('ensures valid when "route" is a string', () => {
28 | ['', 'not an empty string'].forEach((route) => {
29 | routeConfig.route = route;
30 | expect(() => validateRouteConfig(routeConfig)).not.toThrow();
31 | });
32 | });
33 | });
34 |
35 | describe('view port view model resolution validation', function _3_ensureViewPortPointer__Tests() {
36 | beforeEach(() => {
37 | routeConfig = { route: '' };
38 | });
39 |
40 | it('throws when there is no "moduleId", "redirect", "navigationStrategy" or "viewPorts" in config', () => {
41 | const expectedError = /You must specify a "moduleId:", "redirect:", "navigationStrategy:", or "viewPorts:"\./;
42 | expect(() => validateRouteConfig(routeConfig)).toThrowError(expectedError);
43 | });
44 |
45 | ['moduleId', 'redirect', 'navigationStrategy', 'viewPorts'].forEach((prop: string) => {
46 | it(`does not throw when there is at least "${prop}"`, () => {
47 | routeConfig[prop] = prop === 'viewModel' ? function() {/**/} : {};
48 | expect(() => validateRouteConfig(routeConfig)).not.toThrow();
49 | });
50 | });
51 | });
52 | });
53 |
54 |
--------------------------------------------------------------------------------
/test/route-loading/load-component.spec.ts:
--------------------------------------------------------------------------------
1 | import '../setup';
2 | import { NavigationInstruction, RouteConfig, Router } from '../../src/aurelia-router';
3 | import { loadComponent } from '../../src/utilities-route-loading';
4 | import { RouteLoader } from '../../src/route-loader';
5 | import {
6 | ViewPortComponent,
7 | } from '../../src/interfaces';
8 |
9 | describe('RouteLoading -- loadComponent()', function() {
10 | // let container: Container;
11 | let router: Router;
12 | let routeLoader: RouteLoader;
13 | let navInstruction: NavigationInstruction;
14 | let config: RouteConfig;
15 | let vpComponent: ViewPortComponent;
16 |
17 | beforeEach(function() {
18 | routeLoader = {
19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
20 | loadRoute(router: Router, config: RouteConfig, nav: NavigationInstruction): Promise {
21 | return Promise.resolve(vpComponent);
22 | }
23 | };
24 | navInstruction = {
25 | router
26 | } as any;
27 | });
28 |
29 | describe('invalid "viewModel" in ViewPortComponent', () => {
30 | [null, undefined].forEach((viewModel: any) => {
31 | it(`throws when view model is ${viewModel}`, async () => {
32 | vpComponent = { router, viewModel: viewModel };
33 | try {
34 | await loadComponent(routeLoader, navInstruction, config);
35 | expect(0).toBe(1, 'It should not have loaded.');
36 | } catch (ex) {
37 | expect(ex.toString()).toBe(`TypeError: Cannot use 'in' operator to search for 'configureRouter' in ${viewModel}`);
38 | }
39 | });
40 | });
41 |
42 | ['a', 5, Symbol()].forEach((viewModel: any) => {
43 | it(`throws when view model is primitive type ${typeof viewModel}`, async () => {
44 | vpComponent = { router, viewModel: viewModel };
45 | try {
46 | await loadComponent(routeLoader, navInstruction, config);
47 | expect(0).toBe(1, 'It should not have loaded.');
48 | } catch (ex) {
49 | expect(ex.toString()).toContain(`Cannot use 'in' operator to search for 'configureRouter' in ${String(viewModel)}`);
50 | }
51 | });
52 | });
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/route-loading/load-route-step.spec.ts:
--------------------------------------------------------------------------------
1 | import '../setup';
2 | import { Container } from 'aurelia-dependency-injection';
3 | import { LoadRouteStep } from '../../src/step-load-route';
4 | import { NavigationInstruction, Next, PipelineResult, activationStrategy, RouteConfig } from '../../src/aurelia-router';
5 | import { createNextFn } from '../../src/next';
6 | import { PipelineStatus } from '../../src/pipeline-status';
7 |
8 | describe('RouteLoading -- LoadRouteStep', function() {
9 | let container: Container;
10 | let navInstruction: NavigationInstruction;
11 | let loadRouteStep: LoadRouteStep;
12 | let next: Next;
13 | // let nextResult: PipelineResult;
14 |
15 | beforeEach(function __setup__() {
16 | container = new Container();
17 | loadRouteStep = container.get(LoadRouteStep);
18 | navInstruction = {
19 | addViewPortInstruction(name = 'default', config = {}) {
20 | return {
21 | name,
22 | config
23 | };
24 | }
25 | } as any;
26 | navInstruction.plan = {
27 | default: {
28 | strategy: activationStrategy.replace,
29 | name: 'default',
30 | config: {} as RouteConfig
31 | }
32 | };
33 | next = createNextFn(navInstruction, [loadRouteStep.run.bind(loadRouteStep)]);
34 | });
35 |
36 | it('without RouteLoader implementation -- wrapped in Promise', async () => {
37 | const result: PipelineResult = await loadRouteStep.run(navInstruction, next);
38 | expect(result.status).toBe(PipelineStatus.Canceled);
39 | expect(result.output.toString()).toBe(new Error('Route loaders must implement "loadRoute(router, config, navigationInstruction)".').toString());
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/setup.ts:
--------------------------------------------------------------------------------
1 | import 'aurelia-polyfills';
2 | // import 'aurelia-loader-webpack';
3 | import { initialize } from 'aurelia-pal-browser';
4 |
5 | initialize();
6 |
--------------------------------------------------------------------------------
/test/shared.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { Router, Next, NavigationInstruction } from '../src/aurelia-router';
3 | import { NavigationOptions, History } from 'aurelia-history';
4 |
5 | export type ValueOf = T[keyof T];
6 |
7 | export interface MockRouter extends Router {
8 | url: string;
9 | route: string;
10 | params: Record;
11 | }
12 |
13 | export class MockHistory extends History {
14 |
15 | activate(opt?: NavigationOptions) {
16 | return false;
17 | }
18 | // tslint:disable-next-line
19 | deactivate() { }
20 | navigate(fragment: string, opt?: NavigationOptions): boolean {
21 | return false;
22 | }
23 | // tslint:disable-next-line
24 | navigateBack() { }
25 | // tslint:disable-next-line
26 | setState(key: any, value: any) { }
27 | getState(key: any): any {
28 | return null;
29 | }
30 |
31 | getAbsoluteRoot() {
32 | return '';
33 | }
34 |
35 | // tslint:disable-next-line
36 | setTitle() { }
37 | }
38 |
39 | // eslint-disable-next-line @typescript-eslint/no-empty-interface
40 | export interface MockInstruction extends NavigationInstruction { }
41 |
42 | export class MockInstruction {
43 |
44 | title: string;
45 |
46 | constructor(title: string) {
47 | this.title = title;
48 | }
49 | // tslint:disable-next-line
50 | resolve(): void { }
51 | }
52 |
53 |
54 | export interface MockNext extends Next {
55 | (): Promise;
56 | cancel(rejection: any): any;
57 | result: boolean;
58 | rejection: boolean;
59 | }
60 |
61 | export interface MockPipelineState {
62 | next: MockNext;
63 | result: any;
64 | rejection: any;
65 | }
66 |
67 | export function createPipelineState() {
68 | let nextResult: any = null;
69 | let cancelResult: any = null;
70 |
71 | let next = (() => {
72 | nextResult = true;
73 | return Promise.resolve(nextResult);
74 | }) as MockNext;
75 |
76 | next.cancel = (rejection) => {
77 | cancelResult = rejection || 'cancel';
78 | return Promise.resolve(cancelResult);
79 | };
80 |
81 | return {
82 | next,
83 | get result() { return nextResult; },
84 | get rejection() { return cancelResult; }
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/test/utils.spec.ts:
--------------------------------------------------------------------------------
1 | import { _createRootedPath, _normalizeAbsolutePath, _resolveUrl } from '../src/util';
2 |
3 | describe('utilities', function Utilities__Tests() {
4 |
5 | beforeEach(function __setup__() {
6 | // setup
7 | });
8 |
9 | describe('_normalizeAbsolutePath', function _1_normalizeAbsolutePath__Tests() {
10 | type ITestCase = [
11 | /* path */ string,
12 | /* hasPushState */ boolean,
13 | /* absolute */ boolean,
14 | /* expected */ string
15 | ];
16 | const cases: ITestCase[] = [
17 | // TODO: cases
18 | ];
19 | for (const [path, hasPushState, absolute, expectedRootPath] of cases) {
20 | it(`creates "${expectedRootPath}" from { fragment: ${path}, hasPushState: ${hasPushState}, absolute: ${absolute} }`, () => {
21 | expect(_normalizeAbsolutePath(path, hasPushState, absolute)).toBe(expectedRootPath);
22 | });
23 | }
24 | });
25 |
26 | describe('_createRootedPath', function _2_createRootedPath__Tests() {
27 | type ITestCase = [
28 | /* fragment */ string,
29 | /* baseUrl */ string,
30 | /* hasPushState */ boolean,
31 | /* absolute */ boolean,
32 | /* expected */ string
33 | ];
34 | const cases: ITestCase[] = [
35 | // TODO: cases
36 | ['http://g.c', '', true, true, 'http://g.c'],
37 | ['https://g.c', '', true, true, 'https://g.c'],
38 | ['//g.c', '', true, true, '//g.c'],
39 | ['///g.c', '', true, true, '///g.c']
40 | ];
41 | for (const [fragment, baseUrl, hasPushState, absolute, expectedRootPath] of cases) {
42 | it(`creates "${expectedRootPath}" from { fragment: ${fragment}, baseUrl: "${baseUrl}", hasPushState: ${hasPushState}, absolute: ${absolute} }`, () => {
43 | expect(_createRootedPath(fragment, baseUrl, hasPushState, absolute)).toBe(expectedRootPath);
44 | });
45 | }
46 | });
47 |
48 | describe('_resolveUrl', function _3_resolveUrl__Tests() {
49 | type ITestCase = [
50 | /* fragment */ string,
51 | /* baseUrl */ string,
52 | /* hasPushState */ boolean,
53 | /* expected */ string
54 | ];
55 | const cases: ITestCase[] = [
56 | // TODO: cases
57 | ];
58 | for (const [fragment, baseUrl, hasPushState, expectedRootPath] of cases) {
59 | it(`creates "${expectedRootPath}" from { fragment: ${fragment}, baseUrl: ${baseUrl}, hasPushState: ${hasPushState} }`, () => {
60 | expect(_resolveUrl(fragment, baseUrl, hasPushState)).toBe(expectedRootPath);
61 | });
62 | }
63 | });
64 |
65 | describe('_ensureArrayWithSingleRoutePerConfig', function _4_ensureArrayWithSingleRoutePerConfig__Tests() {
66 | // tests
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "es2015",
5 | "experimentalDecorators": true,
6 | "moduleResolution": "node",
7 | "importHelpers": true,
8 | "stripInternal": true,
9 | "preserveConstEnums": true,
10 | "noImplicitAny": true,
11 | "noImplicitThis": true,
12 | "strictFunctionTypes": true,
13 | "noImplicitReturns": true,
14 | "sourceMap": true,
15 | "lib": [
16 | "es2015",
17 | "dom",
18 | "es2016.array.include"
19 | ]
20 | },
21 | "exclude": [
22 | "node_modules",
23 | "dist",
24 | "build",
25 | "doc",
26 | "config.js",
27 | "gulpfile.js",
28 | "karma.conf.js"
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "linterOptions": {
3 | "exclude": [
4 | "dist/aurelia-router.d.ts"
5 | ]
6 | },
7 | "rules": {
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "eofline": true,
15 | "encoding": true,
16 | "forin": false,
17 | "indent": [
18 | true,
19 | "spaces"
20 | ],
21 | "label-position": true,
22 | "max-line-length": [
23 | true,
24 | 160
25 | ],
26 | "no-consecutive-blank-lines": [
27 | true,
28 | 3
29 | ],
30 | "member-access": false,
31 | "member-ordering": false,
32 | "no-arg": true,
33 | "no-bitwise": true,
34 | "no-console": [
35 | true,
36 | "debug",
37 | "info",
38 | "time",
39 | "timeEnd",
40 | "trace"
41 | ],
42 | "no-construct": true,
43 | "no-debugger": true,
44 | "no-duplicate-variable": true,
45 | "no-empty": true,
46 | "no-eval": true,
47 | "no-inferrable-types": false,
48 | "no-shadowed-variable": false,
49 | "newline-before-return": false,
50 | "no-string-literal": false,
51 | "no-switch-case-fall-through": true,
52 | "no-trailing-whitespace": true,
53 | "no-unused-expression": true,
54 | "no-use-before-declare": false,
55 | "no-var-keyword": true,
56 | "object-literal-sort-keys": false,
57 | "one-line": [
58 | true,
59 | "check-open-brace",
60 | "check-catch",
61 | "check-finally",
62 | "check-whitespace"
63 | ],
64 | "quotemark": [
65 | true,
66 | "single",
67 | "avoid-escape"
68 | ],
69 | "radix": true,
70 | "semicolon": [
71 | true
72 | ],
73 | "trailing-comma": [
74 | true,
75 | {
76 | "singleline": "never",
77 | "multiline": "never"
78 | }
79 | ],
80 | "triple-equals": [
81 | true,
82 | "allow-null-check"
83 | ],
84 | "typedef-whitespace": [
85 | true,
86 | {
87 | "call-signature": "nospace",
88 | "index-signature": "nospace",
89 | "parameter": "nospace",
90 | "property-declaration": "nospace",
91 | "variable-declaration": "nospace"
92 | }
93 | ],
94 | "variable-name": false,
95 | "no-misused-new": true,
96 | "whitespace": [
97 | true,
98 | "check-branch",
99 | "check-decl",
100 | "check-operator",
101 | "check-separator",
102 | "check-type"
103 | ],
104 | "interface-name": false,
105 | "prefer-const": false
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/typings.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aurelia-router",
3 | "main": "dist/aurelia-router.d.ts"
4 | }
5 |
--------------------------------------------------------------------------------