├── examples ├── vanilla-blog │ ├── index.js │ ├── client │ │ ├── screens │ │ │ └── app │ │ │ │ ├── index.js │ │ │ │ ├── screens │ │ │ │ ├── about │ │ │ │ │ ├── index.js │ │ │ │ │ ├── about.js │ │ │ │ │ └── templates │ │ │ │ │ │ └── about.html │ │ │ │ ├── faq │ │ │ │ │ ├── index.js │ │ │ │ │ ├── templates │ │ │ │ │ │ └── faq.html │ │ │ │ │ └── faq.js │ │ │ │ ├── home │ │ │ │ │ ├── index.js │ │ │ │ │ ├── home.js │ │ │ │ │ └── templates │ │ │ │ │ │ └── home.html │ │ │ │ └── posts │ │ │ │ │ ├── index.js │ │ │ │ │ ├── screens │ │ │ │ │ ├── show │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── show.js │ │ │ │ │ │ └── templates │ │ │ │ │ │ │ └── show.html │ │ │ │ │ ├── search │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── search.js │ │ │ │ │ └── index │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── templates │ │ │ │ │ └── posts.html │ │ │ │ │ └── posts.js │ │ │ │ ├── app.js │ │ │ │ └── templates │ │ │ │ └── app.html │ │ ├── handler.js │ │ ├── shared │ │ │ └── base_handler.js │ │ └── app.js │ ├── README.md │ ├── index.html │ ├── webpack.config.js │ └── package.json ├── tree-shaking │ ├── router.js │ ├── router-intercept.js │ ├── animated-outlet.js │ ├── router-wc.js │ ├── router-routerlinks.js │ ├── router-wc-routerlinks.js │ ├── package.json │ └── webpack.config.js ├── README.md ├── hello-world-jquery │ ├── index.html │ ├── README.md │ ├── style.css │ └── index.js ├── hello-world-wc │ ├── index.html │ ├── style.css │ └── index.js └── index.html ├── .yarnrc.yml ├── types ├── components │ ├── router-links.d.ts │ ├── router-links.d.ts.map │ ├── animated-outlet.d.ts.map │ └── animated-outlet.d.ts ├── logger.d.ts ├── logger.d.ts.map ├── qs.d.ts.map ├── invariant.d.ts.map ├── invariant.d.ts ├── constants.d.ts.map ├── patternCompiler.d.ts.map ├── constants.d.ts ├── patternCompiler.d.ts ├── utils.d.ts.map ├── events.d.ts.map ├── middlewares │ ├── events.d.ts.map │ ├── router-links.d.ts.map │ ├── events.d.ts │ ├── router-links.d.ts │ ├── wc.d.ts.map │ └── wc.d.ts ├── qs.d.ts ├── links.d.ts.map ├── locations │ ├── memory.d.ts.map │ ├── browser.d.ts.map │ ├── location-bar.d.ts.map │ ├── memory.d.ts │ ├── location-bar.d.ts │ └── browser.d.ts ├── path.d.ts.map ├── array-dsl.d.ts.map ├── function-dsl.d.ts.map ├── wc-router.d.ts.map ├── links.d.ts ├── transition.d.ts.map ├── array-dsl.d.ts ├── events.d.ts ├── utils.d.ts ├── function-dsl.d.ts ├── transition.d.ts ├── wc-router.d.ts ├── router.d.ts.map ├── path.d.ts └── router.d.ts ├── .prettierignore ├── .prettierrc ├── lib ├── constants.js ├── logger.js ├── invariant.js ├── qs.js ├── patternCompiler.js ├── events.js ├── utils.js ├── components │ ├── router-links.js │ └── animated-outlet.js ├── wc-router.js ├── array-dsl.js ├── middlewares │ ├── events.js │ └── router-links.js ├── locations │ ├── memory.js │ ├── browser.js │ └── location-bar.js ├── function-dsl.js ├── links.js ├── path.js └── transition.js ├── .gitignore ├── .eslintrc.json ├── tests ├── unit │ ├── pathToRegexPatternCompiler.js │ ├── utilsTest.js │ ├── linksTest.js │ ├── functionDslTest.js │ └── arrayDslTest.js ├── location-bar │ ├── backbone │ │ └── README.md │ ├── .jshintrc │ ├── location_bar_test.html │ ├── backbone_router_test.html │ ├── vendor │ │ ├── runner.js │ │ └── qunit.css │ └── location_bar_test.js ├── lib │ └── fakeHistory.js └── functional │ ├── memoryTest.js │ ├── pushStateTest.js │ ├── testApp.js │ ├── nanodom.js │ ├── eventsTest.js │ ├── routerTest.js │ └── routerLinksTest.js ├── .vscode └── launch.json ├── docs ├── versions-differences.md ├── common-situations.md ├── middlewares │ ├── events.md │ └── routerlinks.md ├── route-transition.md ├── programmatic-navigation-and-link.md ├── components │ └── animated-outlet.md └── intro.md ├── tsconfig.types.json ├── LICENSE ├── package.json └── README.md /examples/vanilla-blog/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./client/app') 2 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./app') 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.0.2.cjs 4 | -------------------------------------------------------------------------------- /types/components/router-links.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=router-links.d.ts.map -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/about/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./about') 2 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/faq/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./faq') 2 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/home/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./home') 2 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./posts') 2 | -------------------------------------------------------------------------------- /examples/tree-shaking/router.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'slick-router' 2 | 3 | const router = new Router() 4 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/screens/show/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./show') 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | examples/** 3 | docs/** 4 | build/** 5 | lib/locations/location-bar.js 6 | tests/location-bar/** -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 100, 5 | "endOfLine": "auto" 6 | } 7 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/screens/search/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./search') 2 | -------------------------------------------------------------------------------- /types/logger.d.ts: -------------------------------------------------------------------------------- 1 | export default function defineLogger(router: any, method: any, fn: any): void; 2 | //# sourceMappingURL=logger.d.ts.map -------------------------------------------------------------------------------- /types/logger.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../lib/logger.js"],"names":[],"mappings":"AAAA,8EAGC"} -------------------------------------------------------------------------------- /types/qs.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"qs.d.ts","sourceRoot":"","sources":["../lib/qs.js"],"names":[],"mappings":";IACE,sCAMC;IAED,wCASC"} -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | export const TRANSITION_REDIRECTED = 'TransitionRedirected' 2 | 3 | export const TRANSITION_CANCELLED = 'TransitionCancelled' 4 | -------------------------------------------------------------------------------- /types/invariant.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"invariant.d.ts","sourceRoot":"","sources":["../lib/invariant.js"],"names":[],"mappings":"AAAA,qFAKC"} -------------------------------------------------------------------------------- /types/invariant.d.ts: -------------------------------------------------------------------------------- 1 | export default function invariant(condition: any, format: any, ...args: any[]): void; 2 | //# sourceMappingURL=invariant.d.ts.map -------------------------------------------------------------------------------- /examples/tree-shaking/router-intercept.js: -------------------------------------------------------------------------------- 1 | import { Router, interceptLinks } from 'slick-router' 2 | 3 | const router = new Router() 4 | interceptLinks(router) 5 | -------------------------------------------------------------------------------- /types/constants.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../lib/constants.js"],"names":[],"mappings":"AAAA,2DAA2D;AAE3D,yDAAyD"} -------------------------------------------------------------------------------- /types/patternCompiler.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"patternCompiler.d.ts","sourceRoot":"","sources":["../lib/patternCompiler.js"],"names":[],"mappings":"AAIA;;;EAaC"} -------------------------------------------------------------------------------- /types/components/router-links.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"router-links.d.ts","sourceRoot":"","sources":["../../lib/components/router-links.js"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | export default function defineLogger(router, method, fn) { 2 | if (fn === true) return 3 | router[method] = typeof fn === 'function' ? fn : () => {} 4 | } 5 | -------------------------------------------------------------------------------- /examples/tree-shaking/animated-outlet.js: -------------------------------------------------------------------------------- 1 | import { AnimatedOutlet } from 'slick-router/components/animated-outlet.js' 2 | 3 | customElements.define('animated-outlet', AnimatedOutlet) -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/faq/templates/faq.html: -------------------------------------------------------------------------------- 1 |
2 |

FAQ

3 |
4 |

FAQ

5 |

Sorted by:>

-------------------------------------------------------------------------------- /types/constants.d.ts: -------------------------------------------------------------------------------- 1 | export const TRANSITION_REDIRECTED: "TransitionRedirected"; 2 | export const TRANSITION_CANCELLED: "TransitionCancelled"; 3 | //# sourceMappingURL=constants.d.ts.map -------------------------------------------------------------------------------- /types/patternCompiler.d.ts: -------------------------------------------------------------------------------- 1 | export function patternCompiler(pattern: any): { 2 | matcher: RegExp; 3 | paramNames: string[]; 4 | }; 5 | //# sourceMappingURL=patternCompiler.d.ts.map -------------------------------------------------------------------------------- /types/utils.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../lib/utils.js"],"names":[],"mappings":"AAMA;;;EAA+B;AAExB,qCAAoF;AAEpF,gDACyF;AAEzF,uDAGN;AAED;;;;;EAAmC"} -------------------------------------------------------------------------------- /types/events.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../lib/events.js"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8BAPW,OAAO,wCAUjB;AAED;;;;;;;;GAQG;AAEH,gCAPW,OAAO,wCAUjB"} -------------------------------------------------------------------------------- /types/middlewares/events.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../lib/middlewares/events.js"],"names":[],"mappings":";IAOU,mCAEP;IAEO,uCAEP;IAEK,qCAEL;IAEO,mDAIP;IAEM,kDAGN"} -------------------------------------------------------------------------------- /types/qs.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _default { 2 | function parse(querystring: any): any; 3 | function stringify(params: any): string; 4 | } 5 | export default _default; 6 | //# sourceMappingURL=qs.d.ts.map -------------------------------------------------------------------------------- /types/links.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../lib/links.js"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,8BANW,OAAO,MACP,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,KAAK,IAAI,YAa7C"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | sauce.json 4 | .sublimelinterrc 5 | examples/*/dist 6 | coverage 7 | build 8 | /.idea/ 9 | /.yarn/install-state.gz 10 | /examples/tree-shaking/.yarn/install-state.gz 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["prettier"], 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": "latest", 9 | "sourceType": "module" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/tree-shaking/router-wc.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'slick-router' 2 | import { wc, queryValue, paramValue } from 'slick-router/middlewares/wc' 3 | 4 | const router = new Router() 5 | router.use(wc) 6 | 7 | queryValue() 8 | paramValue() 9 | -------------------------------------------------------------------------------- /lib/invariant.js: -------------------------------------------------------------------------------- 1 | export default function invariant(condition, format, ...args) { 2 | if (!condition) { 3 | let argIndex = 0 4 | throw new Error('Invariant Violation: ' + format.replace(/%s/g, () => args[argIndex++])) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/tree-shaking/router-routerlinks.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'slick-router' 2 | import { routerLinks, bindRouterLinks } from 'slick-router/middlewares/router-links' 3 | 4 | const router = new Router() 5 | router.use(routerLinks) 6 | 7 | bindRouterLinks() 8 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/faq/faq.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const template = require('./templates/faq.html') 3 | const BaseHandler = require('base_handler') 4 | 5 | module.exports = _.extend({}, BaseHandler, { 6 | template 7 | }) 8 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/home/home.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const template = require('./templates/home.html') 3 | const BaseHandler = require('base_handler') 4 | 5 | module.exports = _.extend({}, BaseHandler, { 6 | template 7 | }) 8 | -------------------------------------------------------------------------------- /types/locations/memory.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../lib/locations/memory.js"],"names":[],"mappings":";AAEA;IACE;;OAEC;IADC,UAAsB;IAGxB,cAEC;IAED,sCAKC;IAED,0CAIC;IAED,8BAEC;IADC,oBAA8B;IAGhC,wCAMC;IAED,0BAEC;IAED,yBAEC;CACF"} -------------------------------------------------------------------------------- /types/path.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../lib/path.js"],"names":[],"mappings":"AAgBA,kDAIC;AAED;;GAEG;AACH,oEAEC;AAED;;;;GAIG;AACH,0EAiBC;AAED;;;GAGG;AACH,6DA4BC;AAED;;;GAGG;AACH,sDAGC;AAED;;;GAGG;AACH,+DAQC;AAED;;GAEG;AACH,6CAEC"} -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/about/about.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const template = require('./templates/about.html') 3 | const BaseHandler = require('base_handler') 4 | 5 | module.exports = _.extend({}, BaseHandler, { 6 | template 7 | }) 8 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/home/templates/home.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome

3 |
4 |

This is a little application demonstrating some of the Slick Router's features

5 |

This application is also used in the functional tests!

-------------------------------------------------------------------------------- /types/array-dsl.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"array-dsl.d.ts","sourceRoot":"","sources":["../lib/array-dsl.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AAEH;;;;GAIG;AACH,yCAHW,QAAQ,EAAE,GACT,KAAK,EAAE,CAmBlB;oBAhCY,OAAO,aAAa,EAAE,KAAK;;UAK1B,MAAM;UACN,MAAM;cACN,QAAQ,EAAE"} -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/screens/index/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | model: function (params, context, transition) { 3 | this.router.replaceWith('posts.show', { id: 1 }) 4 | }, 5 | activate: function () {}, 6 | deactivate: function () {} 7 | } 8 | -------------------------------------------------------------------------------- /types/function-dsl.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"function-dsl.d.ts","sourceRoot":"","sources":["../lib/function-dsl.js"],"names":[],"mappings":"AAEA;;GAEG;AAEH;;;;;GAKG;AAEH;;;GAGG;AAEH;;;;GAIG;AACH,8CAHW,aAAa,GACZ,KAAK,EAAE,CAgElB;oBAlFY,OAAO,aAAa,EAAE,KAAK;mCAK7B,MAAM,mCAEN,aAAa;oCAKb,aAAa"} -------------------------------------------------------------------------------- /types/middlewares/router-links.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"router-links.d.ts","sourceRoot":"","sources":["../../lib/middlewares/router-links.js"],"names":[],"mappings":"AAkKA;;;;;GAKG;AACH,wCAJW,WAAW,YACX,kBAAkB,YAmB5B;;;;;6DAjLU,WAAW;;aAIR,MAAS,iBAAiB;YAC1B,MAAS,iBAAiB;;AA8KxC,6CAEC;AAED,8BAUC"} -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | * all: ensure library is built (`npm run build` on root folder) 4 | * hello-world-jquery, hello-world-react and vanilla-blog: 5 | * Go to example directory and run `npm install` and `npm run start` 6 | * hello-world-wc: 7 | * No build needed, just load `index.html` using a local server -------------------------------------------------------------------------------- /examples/hello-world-jquery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Slick Router Hello World 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /types/wc-router.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"wc-router.d.ts","sourceRoot":"","sources":["../lib/wc-router.js"],"names":[],"mappings":"yBAMa,OAAO,qBAAqB,EAAE,UAAU;2BACxC,OAAO,qBAAqB,EAAE,YAAY;yBAC1C,OAAO,iBAAiB,EAAE,UAAU;AAHjD;;;;GAIG;AAEH;IACE,0BAIC;CACF;+BAjBoD,aAAa;0BACF,qBAAqB;0BAArB,qBAAqB;0BAArB,qBAAqB;2BAArB,qBAAqB;qCADhC,aAAa"} -------------------------------------------------------------------------------- /examples/vanilla-blog/README.md: -------------------------------------------------------------------------------- 1 | # Example: vanilla blog 2 | 3 | This is a simple blog like website using no frameworks other than Cherrytree for routing. 4 | It simply renders out some html templates in each route. 5 | 6 | ``` 7 | npm install 8 | npm start 9 | ``` 10 | 11 | Now open [http://localhost:8000](http://localhost:8000). -------------------------------------------------------------------------------- /tests/unit/pathToRegexPatternCompiler.js: -------------------------------------------------------------------------------- 1 | import { pathToRegexp } from 'path-to-regexp' 2 | 3 | export function patternCompiler(pattern) { 4 | const paramNames = [] 5 | const re = pathToRegexp(pattern, paramNames) 6 | 7 | return { 8 | matcher: re, 9 | paramNames: paramNames.map((p) => p.name), 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /types/middlewares/events.d.ts: -------------------------------------------------------------------------------- 1 | export namespace events { 2 | function create(router: any): void; 3 | function before(transition: any): void; 4 | function done(transition: any): void; 5 | function cancel(transition: any, error: any): void; 6 | function error(transition: any, error: any): void; 7 | } 8 | //# sourceMappingURL=events.d.ts.map -------------------------------------------------------------------------------- /types/components/animated-outlet.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"animated-outlet.d.ts","sourceRoot":"","sources":["../../lib/components/animated-outlet.js"],"names":[],"mappings":"AAyLA,0FAEC;AAED,iFAEC;AA/LD;IACE,0BAEC;IADC,YAAsB;IAGxB,uCAEC;IAED,uCAEC;IAED,8BAEC;IAED,wCAA0B;IAE1B,kCAAoB;IAEpB,6CAEC;CACF;AA2GD;CAgBC;AAED;CA+BC;AAiBD;IACE,2BAgBC;IAJG,eAAmB;IAMvB,2BA4BC;IAbG,cAAoB;CAczB"} -------------------------------------------------------------------------------- /types/links.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Handle link delegation on `el` or the document, 3 | * and invoke `fn(e)` when clickable. 4 | * 5 | * @param {Element} el 6 | * @param {(e: Event, el: HTMLElement) => void} fn 7 | * @return {Function} dispose 8 | * @api public 9 | */ 10 | export function intercept(el: Element, fn: (e: Event, el: HTMLElement) => void): Function; 11 | //# sourceMappingURL=links.d.ts.map -------------------------------------------------------------------------------- /examples/tree-shaking/router-wc-routerlinks.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'slick-router' 2 | import { wc, queryValue, paramValue } from 'slick-router/middlewares/wc' 3 | import { routerLinks, bindRouterLinks } from 'slick-router/middlewares/router-links' 4 | 5 | const router = new Router() 6 | router.use(wc) 7 | router.use(routerLinks) 8 | 9 | queryValue() 10 | paramValue() 11 | 12 | bindRouterLinks() 13 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/templates/posts.html: -------------------------------------------------------------------------------- 1 |
2 | 9 |
10 |
-------------------------------------------------------------------------------- /types/locations/browser.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../lib/locations/browser.js"],"names":[],"mappings":";AAGA;IACE,0BAmBC;IAlBC,UAA8B;IAE9B;;;MAMC;IAID,yBAAoC;IAQtC;;OAEG;IAEH,cAEC;IAED;;;OAGG;IAEH,sCAKC;IAED;;;OAGG;IAEH,0CAKC;IAED;;;OAGG;IACH,mCAEC;IADC,yBAA8B;IAGhC;;;OAGG;IACH,6BAaC;IAED;;;;;;;OAOG;IACH,0BAMC;IAED;;OAEG;IACH,gBAEC;IAED;;;;;;;;OAQG;IACH,kBAKC;CACF;wBAzHuB,mBAAmB"} -------------------------------------------------------------------------------- /examples/hello-world-jquery/README.md: -------------------------------------------------------------------------------- 1 | # Example: hello world 2 | 3 | 4 | 5 | 6 | In the 7 | 8 | Now open [http://localhost:8000](http://localhost:8000). 9 | 10 | Or open [http://localhost:8000/webpack-dev-server/bundle](http://localhost:8000/webpack-dev-server/bundle) to see the live reloading version of the app. 11 | 12 | To compile the app into a single file for production run 13 | 14 | $ npm run bundle 15 | -------------------------------------------------------------------------------- /examples/hello-world-wc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Slick Router Hello World - Web Components 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /types/locations/location-bar.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"location-bar.d.ts","sourceRoot":"","sources":["../../lib/locations/location-bar.js"],"names":[],"mappings":";AAoBA;IAEI,gBAAkB;IAwGpB,oBAIC;IA1GC,mBAA+B;IAC/B,4BAA6B;IAK/B,kBAEC;IAID,kBAGC;IAID,qDAWC;IAID,6BAmDC;IA/CC,iBAAmB;IAInB;;MAAoD;IAGpD,UAAyC;IACzC,0BAAyD;IACzD,yBAAgD;IAChD,uBAA4C;IAY5C,cAAwB;IA6B1B,aAGC;IAID,uCAEC;IAaD,gCAQC;IASD,oDA6BC;IAID,8DAQC;IAKD,8BAEC;IAGD,wBAMC;CACF"} -------------------------------------------------------------------------------- /examples/tree-shaking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-shaking", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "webpack --mode=development", 8 | "prod": "webpack --mode=production" 9 | }, 10 | "dependencies": { 11 | "slick-router": "file:../../build" 12 | }, 13 | "devDependencies": { 14 | "webpack": "^5.89.0", 15 | "webpack-cli": "^5.1.4" 16 | } 17 | } -------------------------------------------------------------------------------- /examples/vanilla-blog/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Slick Router Demo Application 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vanilla-blog/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: './index', 6 | output: { 7 | path: path.join(__dirname, 'dist'), 8 | filename: 'bundle.js' 9 | }, 10 | devtool: 'source-map', 11 | resolve: { 12 | modules: [path.resolve(__dirname, './client/shared'), 'node_modules'] 13 | }, 14 | module: { 15 | rules: [ 16 | { test: /\.html$/, loader: 'underscore-template-loader' } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /types/locations/memory.d.ts: -------------------------------------------------------------------------------- 1 | export default MemoryLocation; 2 | declare class MemoryLocation { 3 | constructor({ path }: { 4 | path: any; 5 | }); 6 | path: any; 7 | getURL(): any; 8 | setURL(path: any, options: any): void; 9 | replaceURL(path: any, options: any): void; 10 | onChange(callback: any): void; 11 | changeCallback: any; 12 | handleURL(url: any, options?: {}): void; 13 | removeRoot(url: any): any; 14 | formatURL(url: any): any; 15 | } 16 | //# sourceMappingURL=memory.d.ts.map -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/posts.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const BaseHandler = require('base_handler') 3 | const template = require('./templates/posts.html') 4 | 5 | module.exports = _.extend({}, BaseHandler, { 6 | template, 7 | model: function (params, context) { 8 | return context.then(function (context) { 9 | return new Promise(function (resolve) { 10 | resolve(_.extend(context, { 11 | allPostsData: ['foo', 'bar'] 12 | })) 13 | }) 14 | }) 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /lib/qs.js: -------------------------------------------------------------------------------- 1 | export default { 2 | parse(querystring) { 3 | return querystring.split('&').reduce((acc, pair) => { 4 | const parts = pair.split('=') 5 | acc[parts[0]] = decodeURIComponent(parts[1]) 6 | return acc 7 | }, {}) 8 | }, 9 | 10 | stringify(params) { 11 | return Object.keys(params) 12 | .reduce((acc, key) => { 13 | if (params[key] !== undefined) { 14 | acc.push(key + '=' + encodeURIComponent(params[key])) 15 | } 16 | return acc 17 | }, []) 18 | .join('&') 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /types/transition.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"transition.d.ts","sourceRoot":"","sources":["../lib/transition.js"],"names":[],"mappings":"AAyCA;;;;GAIG;AACH,kDAFY,UAAU,CAuLrB;oBA9NY,OAAO,aAAa,EAAE,KAAK;6BAI3B,KAAK,UAAU,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAE;;YAKrE,KAAK,EAAE;cACP,MAAM;UACN,MAAM;;;UAGN,cAAc;gBACd,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,UAAU;WACvD,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,GAAG,KAAK,UAAU;YACvD,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,KAAK,IAAI;qBAC/B,MAAM,QAAQ,GAAG,CAAC;;;UAGlB,OAAO;iBACP,OAAO"} -------------------------------------------------------------------------------- /lib/patternCompiler.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'regexparam' 2 | 3 | const splatRegex = /:(\w+)\*/ 4 | 5 | export function patternCompiler(pattern) { 6 | // hack to add (partial) named splat support 7 | const splatMatch = splatRegex.exec(pattern) 8 | const normalizedPattern = splatMatch ? pattern.replace(splatRegex, '*') : pattern 9 | 10 | const { pattern: matcher, keys } = parse(normalizedPattern) 11 | 12 | const paramNames = splatMatch ? keys.map((key) => (key === '*' ? splatMatch[1] : key)) : keys 13 | 14 | return { 15 | matcher, 16 | paramNames, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /types/array-dsl.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./router.js").Route} Route 3 | */ 4 | /** 5 | * @typedef RouteDef 6 | * @property {string} name 7 | * @property {string} path 8 | * @property {RouteDef[]} children 9 | */ 10 | /** 11 | * @export 12 | * @param {RouteDef[]} routes 13 | * @return {Route[]} 14 | */ 15 | export default function arrayDsl(routes: RouteDef[]): Route[]; 16 | export type Route = import("./router.js").Route; 17 | export type RouteDef = { 18 | name: string; 19 | path: string; 20 | children: RouteDef[]; 21 | }; 22 | //# sourceMappingURL=array-dsl.d.ts.map -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "attach", 10 | "name": "Debug Tests (Attach to Karma)", 11 | "address": "localhost", 12 | "port": 9333, 13 | "pathMapping": { 14 | "/": "${workspaceRoot}", 15 | "/base/": "${workspaceRoot}/" 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /types/events.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Bind `el` event `type` to `fn`. 3 | * 4 | * @param {Element} el 5 | * @param {String} type 6 | * @param {Function} fn 7 | * @return {Function} 8 | * @api public 9 | */ 10 | export function bindEvent(el: Element, type: string, fn: Function): Function; 11 | /** 12 | * Unbind `el` event `type`'s callback `fn`. 13 | * 14 | * @param {Element} el 15 | * @param {String} type 16 | * @param {Function} fn 17 | * @return {Function} 18 | * @api public 19 | */ 20 | export function unbindEvent(el: Element, type: string, fn: Function): Function; 21 | //# sourceMappingURL=events.d.ts.map -------------------------------------------------------------------------------- /examples/vanilla-blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-blog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack -w --mode development", 8 | "build": "webpack --mode production", 9 | "test": "mocha" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "underscore-template-loader": "^1.0.0", 15 | "webpack": "^4.41.5", 16 | "webpack-cli": "^3.3.10" 17 | }, 18 | "dependencies": { 19 | "jquery": "^3.4.1", 20 | "slick-router": "^2.1.1", 21 | "underscore": "^1.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/location-bar/backbone/README.md: -------------------------------------------------------------------------------- 1 | This backbone.js is a slightly modified version of backbone that we use to run the original Backbone's Router/History test suite. 2 | 3 | The only change is removal of `Backbone` variable within the closure. Instead we create `window.Backbone = {}`. This way all references to `Backbone.history/Backbone.History` are referencing the window.Backbone variable instead of local to the closure `var Backbone`. This allows us to swap the `window.Backbone.history` and `window.Backbone.History` with `location-bar` module and run the original Backbone's `Router/History` tests. 4 | 5 | This is backbone.js 1.1.2. -------------------------------------------------------------------------------- /types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export const keys: { 2 | (o: object): string[]; 3 | (o: {}): string[]; 4 | }; 5 | export function clone(obj: any): any; 6 | export function pick(obj: any, attrs: any): any; 7 | export function isEqual(obj1: any, obj2: any): boolean; 8 | export const extend: { 9 | (target: T, source: U): T & U; 10 | (target: T_1, source1: U_1, source2: V): T_1 & U_1 & V; 11 | (target: T_2, source1: U_2, source2: V_1, source3: W): T_2 & U_2 & V_1 & W; 12 | (target: object, ...sources: any[]): any; 13 | }; 14 | //# sourceMappingURL=utils.d.ts.map -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bind `el` event `type` to `fn`. 3 | * 4 | * @param {Element} el 5 | * @param {String} type 6 | * @param {Function} fn 7 | * @return {Function} 8 | * @api public 9 | */ 10 | 11 | export function bindEvent(el, type, fn) { 12 | el.addEventListener(type, fn) 13 | return fn 14 | } 15 | 16 | /** 17 | * Unbind `el` event `type`'s callback `fn`. 18 | * 19 | * @param {Element} el 20 | * @param {String} type 21 | * @param {Function} fn 22 | * @return {Function} 23 | * @api public 24 | */ 25 | 26 | export function unbindEvent(el, type, fn) { 27 | el.removeEventListener(type, fn) 28 | return fn 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const assoc = (obj, attr, val) => { 2 | obj[attr] = val 3 | return obj 4 | } 5 | const isArray = Array.isArray 6 | 7 | export const keys = Object.keys 8 | 9 | export const clone = (obj) => (obj ? (isArray(obj) ? obj.slice(0) : extend({}, obj)) : obj) 10 | 11 | export const pick = (obj, attrs) => 12 | attrs.reduce((acc, attr) => (obj[attr] === undefined ? acc : assoc(acc, attr, obj[attr])), {}) 13 | 14 | export const isEqual = (obj1, obj2) => { 15 | const keys1 = keys(obj1) 16 | return keys1.length === keys(obj2).length && keys1.every((key) => obj2[key] === obj1[key]) 17 | } 18 | 19 | export const extend = Object.assign 20 | -------------------------------------------------------------------------------- /lib/components/router-links.js: -------------------------------------------------------------------------------- 1 | import { bindRouterLinks } from '../middlewares/router-links.js' 2 | 3 | class RouterLinks extends HTMLElement { 4 | connectedCallback() { 5 | // Register the web component using bindRouterLinks 6 | this.unbindRouterLinks = bindRouterLinks(this, { 7 | params: this.params, 8 | query: this.query, 9 | }) 10 | } 11 | 12 | disconnectedCallback() { 13 | // Call the return of bindRouterLinks when disconnected 14 | if (typeof this.unbindRouterLinks === 'function') { 15 | this.unbindRouterLinks() 16 | this.unbindRouterLinks = undefined 17 | } 18 | } 19 | } 20 | 21 | customElements.define('router-links', RouterLinks) 22 | -------------------------------------------------------------------------------- /docs/versions-differences.md: -------------------------------------------------------------------------------- 1 | ## Differences between slick-router (v1) and cherrytree 2 | 3 | * API changes: 4 | * Published package as cherrytreex (and later as slick-router) 5 | * Export router constructor instead of factory function 6 | * Add possibility to register middleware with a object containing next/done/error hooks 7 | * Drop ability to customize Promise implementation. To run in browsers without native Promise is necessary a polyfill 8 | * Do not use active state to generate links 9 | * Infrastructure changes 10 | * Update build system simplifying it and producing smaller bundle 11 | * Incorporated location-bar dependency removing shared code 12 | * Upgraded dev dependencies 13 | -------------------------------------------------------------------------------- /examples/tree-shaking/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const DIST_DIR = 'dist' 4 | 5 | const baseConfig = { 6 | output: { 7 | filename: '[name].js', 8 | path: path.resolve(__dirname, DIST_DIR) 9 | }, 10 | devtool: false, 11 | optimization: { 12 | concatenateModules: true, 13 | minimize: true 14 | } 15 | } 16 | 17 | const entries = [ 18 | 'animated-outlet', 19 | 'router', 20 | 'router-intercept', 21 | 'router-wc', 22 | 'router-routerlinks', 23 | 'router-wc-routerlinks' 24 | ] 25 | 26 | const configs = entries.map(entry => { 27 | return Object.assign({ entry: { [entry]: `./${entry}.js` } }, baseConfig) 28 | }) 29 | 30 | module.exports = configs 31 | -------------------------------------------------------------------------------- /types/middlewares/router-links.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @export 3 | * @param {HTMLElement} rootEl 4 | * @param {RouterLinksOptions} [options={}] 5 | * @return {Function} 6 | */ 7 | export function bindRouterLinks(rootEl: HTMLElement, options?: RouterLinksOptions): Function; 8 | export namespace routerLinks { 9 | export { create }; 10 | export { done }; 11 | } 12 | export type RoutePropCallback = (routeName: string, routeEl: HTMLElement) => any; 13 | export type RouterLinksOptions = { 14 | params?: any | RoutePropCallback; 15 | query?: any | RoutePropCallback; 16 | }; 17 | declare function create(instance: any): void; 18 | declare function done(): void; 19 | export {}; 20 | //# sourceMappingURL=router-links.d.ts.map -------------------------------------------------------------------------------- /types/middlewares/wc.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"wc.d.ts","sourceRoot":"","sources":["../../lib/middlewares/wc.js"],"names":[],"mappings":"AA0DA;;;GAGG;AACH,uCAFa,YAAY,CAQxB;AAED;;;;GAIG;AACH,kDAHW,MAAM,WAAW,GACf,YAAY,CAQxB;AAED;;;;;GAKG;AACH,oCAJW,MAAM,UACN,MAAM,WAAW,GACf,YAAY,CAQxB;AAED,4CAEC;;;;;;;yBA3FY,OAAO,kBAAkB,EAAE,UAAU;;UAGpC,MAAM;UACN,MAAM;eACN,MAAM,WAAW;;YACT,MAAM,GAAE,YAAY,GAAG,YAAY,EAAE;;YAC7C,OAAO;yBACE,UAAU,KAAG,QAAQ,OAAO,CAAC,GAAG,OAAO;yBACvC,UAAU,KAAG,QAAQ,OAAO,CAAC,GAAG,OAAO;wBACvC,UAAU,KAAG,QAAQ,IAAI,CAAC,GAAG,IAAI;wBACjC,UAAU,KAAG,QAAQ,IAAI,CAAC,GAAG,IAAI;;6CAM3C,IAAI;;kBAKM,cAAc,KAAG,IAAI;mBACrB,UAAU,QAAE,cAAc,KAAG,IAAI;mBACjC,UAAU,QAAE,cAAc,KAAG,IAAI;+BAC9B,WAAW,KAAG,IAAI;;AAoE5C,6CAGC;AAED,iCAMC;AAoND,yDA6BC;AAED,6CAIC"} -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/app.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery') 2 | const _ = require('lodash') 3 | const template = require('./templates/app.html') 4 | const BaseHandler = require('base_handler') 5 | 6 | module.exports = _.extend({}, BaseHandler, { 7 | template, 8 | model: function () { 9 | const context = { 10 | appRnd: Math.random() 11 | } 12 | // activate eagerly - we want to render this route 13 | // right while the other routes might be loading 14 | this.activate(context) 15 | return context 16 | }, 17 | templateData: function (context) { 18 | return { 19 | rnd: context.appRnd 20 | } 21 | }, 22 | outlet: function () { 23 | return $(document.body) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/handler.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const BaseHandler = require('base_handler') 3 | 4 | const handlers = { 5 | application: require('./screens/app'), 6 | home: require('./screens/app/screens/home'), 7 | about: require('./screens/app/screens/about'), 8 | faq: require('./screens/app/screens/faq'), 9 | posts: require('./screens/app/screens/posts'), 10 | 'posts.index': require('./screens/app/screens/posts/screens/index'), 11 | 'posts.show': require('./screens/app/screens/posts/screens/show'), 12 | 'posts.search': require('./screens/app/screens/posts/screens/search') 13 | } 14 | 15 | module.exports = function getHandler (routeName) { 16 | return handlers[routeName] || _.clone(BaseHandler) 17 | } 18 | -------------------------------------------------------------------------------- /lib/wc-router.js: -------------------------------------------------------------------------------- 1 | import { Router as CoreRouter, interceptLinks } from './router.js' 2 | import { wc, fromParam, fromQuery, fromValue, getRouteEl } from './middlewares/wc.js' 3 | import { routerLinks } from './middlewares/router-links.js' 4 | import './components/router-links.js' 5 | 6 | /** 7 | * @typedef {import("./middlewares/wc.js").WCRouteDef} WCRouteDef 8 | * @typedef {import("./middlewares/wc.js").PropertyHook} PropertyHook 9 | * @typedef {import("./transition.js").Transition} Transition 10 | */ 11 | 12 | class Router extends CoreRouter { 13 | constructor(options) { 14 | super(options) 15 | this.use(wc) 16 | this.use(routerLinks) 17 | } 18 | } 19 | 20 | export { Router, interceptLinks, fromParam, fromQuery, fromValue, getRouteEl } 21 | -------------------------------------------------------------------------------- /types/function-dsl.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./router.js").Route} Route 3 | */ 4 | /** 5 | * @callback registerRoute 6 | * @param {string} name 7 | * @param {Object} options 8 | * @param {routeCallback} [childrenCallback] 9 | */ 10 | /** 11 | * @callback routeCallback 12 | * @param {registerRoute} route 13 | */ 14 | /** 15 | * @export 16 | * @param {routeCallback} callback 17 | * @return {Route[]} 18 | */ 19 | export default function functionDsl(callback: routeCallback): Route[]; 20 | export type Route = import("./router.js").Route; 21 | export type registerRoute = (name: string, options: any, childrenCallback?: routeCallback) => any; 22 | export type routeCallback = (route: registerRoute) => any; 23 | //# sourceMappingURL=function-dsl.d.ts.map -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | // Change this to match your project 3 | "include": [ 4 | "lib/wc-router.js", 5 | "lib/components", 6 | "lib/middlewares" 7 | ], 8 | "compilerOptions": { 9 | "target": "ES2017", 10 | "lib": [ 11 | "es2020", 12 | "DOM" 13 | ], 14 | "module": "NodeNext", 15 | "outDir": "types", 16 | // Tells TypeScript to read JS files, as 17 | // normally they are ignored as source files 18 | "allowJs": true, 19 | // Generate d.ts files 20 | "declaration": true, 21 | // This compiler run should 22 | // only output d.ts files 23 | "emitDeclarationOnly": true, 24 | // go to js file when using IDE functions like 25 | // "Go to Definition" in VSCode 26 | "declarationMap": true 27 | } 28 | } -------------------------------------------------------------------------------- /docs/common-situations.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Common Situations 5 | 6 | ## Handling 404 7 | 8 | There are a couple of ways to handle URLs that don't match any routes. 9 | 10 | You can create a middleware to detects when `transition.routes.length` is 0 and render a 404 page. 11 | 12 | Alternatively, you can also declare a catch all path in your route map: 13 | 14 | ```js 15 | router.map(function (route) { 16 | route('application', {path: '/'}, function () { 17 | route('blog') 18 | route('missing', {path: ':path*'}) 19 | }) 20 | }) 21 | ``` 22 | 23 | In this case, when nothing else matches, a transition to the `missing` route will be initiated with `transition.routes` as ['application', 'missing']. This gives you a chance to activate and render the `application` route before rendering a 404 page. 24 | -------------------------------------------------------------------------------- /lib/array-dsl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("./router.js").Route} Route 3 | */ 4 | 5 | /** 6 | * @typedef RouteDef 7 | * @property {string} name 8 | * @property {string} path 9 | * @property {RouteDef[]} children 10 | */ 11 | 12 | /** 13 | * @export 14 | * @param {RouteDef[]} routes 15 | * @return {Route[]} 16 | */ 17 | export default function arrayDsl(routes) { 18 | const result = [] 19 | 20 | routes.forEach(({ name, children, ...options }) => { 21 | if (typeof options.path !== 'string') { 22 | const parts = name.split('.') 23 | options.path = parts[parts.length - 1] 24 | } 25 | result.push({ 26 | name, 27 | path: options.path, 28 | options, 29 | routes: children ? arrayDsl(children) : [], 30 | }) 31 | }) 32 | 33 | return result 34 | } 35 | -------------------------------------------------------------------------------- /tests/location-bar/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": false, 3 | "eqeqeq": true, 4 | "forin": true, 5 | "immed": true, 6 | "latedef": false, 7 | "newcap": true, 8 | "noarg": true, 9 | "noempty": true, 10 | "nonew": true, 11 | "regexp": false, 12 | "undef": true, 13 | "unused": true, 14 | "trailing": true, 15 | "sub": true, 16 | "expr": true, 17 | "es5": true, 18 | 19 | "browser": true, 20 | "devel": true, 21 | 22 | "indent": 2, 23 | "maxlen": 120, 24 | "white": false, 25 | 26 | "predef": ["_", "test", "module", "Backbone", "equal", "ok", "strictEqual", "asyncTest", "start"], 27 | 28 | "strict": false, 29 | "nomen": false, 30 | "onevar": false, 31 | "plusplus": false, 32 | "jquery": false, 33 | "dojo": false, 34 | "mootools": false, 35 | "prototypejs": false 36 | } -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/templates/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

Slick Router

7 |

A hierarchical stateful router. <%- rnd %>

8 |
9 | 15 |
16 |
17 |
18 |
19 |
20 |
-------------------------------------------------------------------------------- /lib/middlewares/events.js: -------------------------------------------------------------------------------- 1 | let eventPrefix 2 | 3 | function trigger(name, detail) { 4 | window.dispatchEvent(new CustomEvent(`${eventPrefix}${name}`, { detail })) 5 | } 6 | 7 | export const events = { 8 | create: function (router) { 9 | eventPrefix = router.options.eventPrefix || 'router-' 10 | }, 11 | 12 | before: function (transition) { 13 | trigger('before:transition', { transition }) 14 | }, 15 | 16 | done: function (transition) { 17 | trigger('transition', { transition }) 18 | }, 19 | 20 | cancel: function (transition, error) { 21 | if (error.type !== 'TransitionRedirected') { 22 | trigger('abort', { transition, error }) 23 | } 24 | }, 25 | 26 | error: function (transition, error) { 27 | trigger('abort', { transition, error }) 28 | trigger('error', { transition, error }) 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /docs/middlewares/events.md: -------------------------------------------------------------------------------- 1 | # events middleware 2 | 3 | Trigger router events on window instance 4 | 5 | ## Usage 6 | 7 | Listen to events on window: 8 | 9 | ```javascript 10 | window.addEventListener('router-transition', (e) => { 11 | const { transition } = e.detail 12 | console.log(`Route transitioned from ${transition.prev.path} to from ${transition.path}`) 13 | }) 14 | ``` 15 | 16 | ## Options 17 | 18 | ### `eventPrefix` 19 | 20 | Defines the events prefix. 21 | 22 | Defaults to 'router-' 23 | 24 | ## Events 25 | 26 | ### before:transition 27 | 28 | Fired before a route transition is run 29 | 30 | ### transition 31 | 32 | Fired after a successful route transition is completed 33 | 34 | ### error 35 | 36 | Fired when an error occurs while running a route transition 37 | 38 | ### abort 39 | 40 | Fired when an error occurs or when a route transition is cancelled 41 | -------------------------------------------------------------------------------- /types/transition.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @export 3 | * @param {*} options 4 | * @return {Transition} 5 | */ 6 | export default function transition(options: any): Transition; 7 | export type Route = import("./router.js").Route; 8 | export type TransitionData = Pick; 9 | export type Transition = { 10 | routes: Route[]; 11 | pathname: string; 12 | path: string; 13 | params: any; 14 | query: any; 15 | prev: TransitionData; 16 | redirectTo: (name: string, params?: any, query?: any) => Transition; 17 | retry: (name: string, params?: any, query?: any) => Transition; 18 | cancel: (error: string | Error) => void; 19 | followRedirects: () => Promise; 20 | then: Function; 21 | catch: Function; 22 | noop: boolean; 23 | isCancelled: boolean; 24 | }; 25 | //# sourceMappingURL=transition.d.ts.map -------------------------------------------------------------------------------- /types/components/animated-outlet.d.ts: -------------------------------------------------------------------------------- 1 | export function registerAnimation(name: any, AnimationHookClass: any, options?: {}): void; 2 | export function setDefaultAnimation(AnimationHookClass: any, options?: {}): void; 3 | export class AnimationHook { 4 | constructor(options?: {}); 5 | options: {}; 6 | getOption(outlet: any, name: any): any; 7 | hasOption(outlet: any, name: any): any; 8 | runParallel(outlet: any): any; 9 | beforeEnter(outlet: any, el: any): void; 10 | enter(outlet: any, el: any): void; 11 | leave(outlet: any, el: any, done: any): void; 12 | } 13 | export class GenericCSS extends AnimationHook { 14 | } 15 | export class AnimateCSS extends AnimationHook { 16 | } 17 | export class AnimatedOutlet extends HTMLElement { 18 | appendChild(el: any): void; 19 | appending: any; 20 | removeChild(el: any): void; 21 | removing: any; 22 | } 23 | //# sourceMappingURL=animated-outlet.d.ts.map -------------------------------------------------------------------------------- /types/locations/location-bar.d.ts: -------------------------------------------------------------------------------- 1 | export default History; 2 | declare class History { 3 | handlers: any[]; 4 | checkUrl(): boolean; 5 | location: Location; 6 | history: globalThis.History; 7 | atRoot(): boolean; 8 | getHash(): string; 9 | getFragment(fragment: any, forcePushState: any): any; 10 | start(options?: {}): boolean; 11 | started: boolean; 12 | options: { 13 | root: string; 14 | }; 15 | root: any; 16 | _wantsHashChange: boolean; 17 | _wantsPushState: boolean; 18 | _hasPushState: boolean; 19 | fragment: any; 20 | stop(): void; 21 | route(route: any, callback: any): void; 22 | loadUrl(fragment: any): boolean; 23 | update(fragment: any, options: any): boolean | void; 24 | _updateHash(location: any, fragment: any, replace: any): void; 25 | onChange(callback: any): void; 26 | hasPushState(): boolean; 27 | } 28 | //# sourceMappingURL=location-bar.d.ts.map -------------------------------------------------------------------------------- /types/wc-router.d.ts: -------------------------------------------------------------------------------- 1 | export type WCRouteDef = import("./middlewares/wc.js").WCRouteDef; 2 | export type PropertyHook = import("./middlewares/wc.js").PropertyHook; 3 | export type Transition = import("./transition.js").Transition; 4 | /** 5 | * @typedef {import("./middlewares/wc.js").WCRouteDef} WCRouteDef 6 | * @typedef {import("./middlewares/wc.js").PropertyHook} PropertyHook 7 | * @typedef {import("./transition.js").Transition} Transition 8 | */ 9 | export class Router extends CoreRouter { 10 | constructor(options: any); 11 | } 12 | import { interceptLinks } from './router.js'; 13 | import { fromParam } from './middlewares/wc.js'; 14 | import { fromQuery } from './middlewares/wc.js'; 15 | import { fromValue } from './middlewares/wc.js'; 16 | import { getRouteEl } from './middlewares/wc.js'; 17 | import { Router as CoreRouter } from './router.js'; 18 | export { interceptLinks, fromParam, fromQuery, fromValue, getRouteEl }; 19 | //# sourceMappingURL=wc-router.d.ts.map -------------------------------------------------------------------------------- /lib/locations/memory.js: -------------------------------------------------------------------------------- 1 | import { extend } from '../utils.js' 2 | 3 | class MemoryLocation { 4 | constructor({ path }) { 5 | this.path = path || '' 6 | } 7 | 8 | getURL() { 9 | return this.path 10 | } 11 | 12 | setURL(path, options) { 13 | if (this.path !== path) { 14 | this.path = path 15 | this.handleURL(this.getURL(), options) 16 | } 17 | } 18 | 19 | replaceURL(path, options) { 20 | if (this.path !== path) { 21 | this.setURL(path, options) 22 | } 23 | } 24 | 25 | onChange(callback) { 26 | this.changeCallback = callback 27 | } 28 | 29 | handleURL(url, options = {}) { 30 | this.path = url 31 | options = extend({ trigger: true }, options) 32 | if (this.changeCallback && options.trigger) { 33 | this.changeCallback(url) 34 | } 35 | } 36 | 37 | removeRoot(url) { 38 | return url 39 | } 40 | 41 | formatURL(url) { 42 | return url 43 | } 44 | } 45 | 46 | export default MemoryLocation 47 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/screens/search/search.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const BaseHandler = require('base_handler') 3 | 4 | module.exports = _.extend({}, BaseHandler, { 5 | model: function (params) { 6 | return params 7 | }, 8 | activate: function (context) { 9 | this.render(context) 10 | }, 11 | update: function (context) { 12 | this.render(context) 13 | }, 14 | // queryParamsDidChange: function (queryParams) { 15 | // var context = this.getContext() 16 | // context.queryParams = queryParams 17 | // this.setContext(context) 18 | // this.render(context) 19 | // }, 20 | render: function (context) { 21 | if (context.query === 'mine') { 22 | this.outlet().html('My posts...') 23 | } else { 24 | this.outlet().html('No matching blog posts were found') 25 | } 26 | if (context.queryParams.sortBy) { 27 | this.outlet().append('
Sorting by:' + context.queryParams.sortBy + '
') 28 | } 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /examples/hello-world-jquery/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:400,700); 2 | 3 | body, html { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Open Sans', sans-serif; 7 | } 8 | 9 | .App { 10 | width: 800px; 11 | margin: 0 auto 20px auto; 12 | } 13 | 14 | .App-header { 15 | border-bottom: 1px solid #eee; 16 | } 17 | 18 | .App h1 { 19 | display: inline-block; 20 | } 21 | 22 | .Nav { 23 | display: inline-block; 24 | } 25 | 26 | .Nav-item { 27 | list-style: none; 28 | display: inline-block; 29 | } 30 | 31 | .Nav-item a { 32 | padding: 10px; 33 | } 34 | 35 | .Tweet { 36 | border: 1px solid #eee; 37 | border-radius: 3px; 38 | padding: 10px; 39 | border-bottom: none; 40 | } 41 | 42 | .Tweet:last-child { 43 | border-bottom: 1px solid #eee; 44 | } 45 | 46 | .Tweet-author { 47 | font-weight: bold; 48 | display: inline-block; 49 | } 50 | 51 | .Tweet-time { 52 | color: #888; 53 | display: inline-block; 54 | margin-left: 20px; 55 | font-size: 12px; 56 | } -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/screens/show/show.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const template = require('./templates/show.html') 3 | const BaseHandler = require('base_handler') 4 | 5 | module.exports = _.extend({}, BaseHandler, { 6 | template, 7 | willTransition: function (transition) { 8 | // if (this.postId === '2') { 9 | // transition.cancel() 10 | // } 11 | }, 12 | model: function (params, context) { 13 | if (!this.sessionStore) { 14 | this.sessionStore = 1 15 | } else { 16 | this.sessionStore++ 17 | } 18 | const self = this 19 | return context.then(function (context) { 20 | self.postId = params.id 21 | return new Promise(function (resolve) { 22 | resolve({ title: 'Blog ' + params.id, subtitle: context.allPostsData[0] + context.appRnd }) 23 | }) 24 | }) 25 | }, 26 | templateData: function (context) { 27 | return { 28 | title: 'Blog post #' + context.title + ' (' + context.subtitle + ')' 29 | } 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Slick Router Examples 9 | 25 | 26 | 27 | 28 |
29 |
Hello World Web Components
30 |
Demonstrate usage with web components, animated routing transitions, automatic route links and router events.
31 |
32 |
33 |
Hello World jQuery
34 |
It's a very simple static twitter like app. It's simple to keep the code short and just show how to get started.
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/lib/fakeHistory.js: -------------------------------------------------------------------------------- 1 | export default function fakeHistory(location) { 2 | const history = [] 3 | 4 | const originalPushState = window.history.pushState 5 | window.history.pushState = function (state, title, url) { 6 | history.push(url) 7 | } 8 | 9 | return { 10 | getURL: function getURL() { 11 | return history[history.length - 1] 12 | }, 13 | 14 | /** 15 | * This method relies on deep internals of 16 | * how location-bar is implented, to simulate 17 | * what happens when the URL in the browser 18 | * changes. It might be better to 19 | * a) build functional tests that include a server with real pushState 20 | * b) unit test around this 21 | */ 22 | setURL: function setURL(url) { 23 | // slick router + location-bar + window.location 24 | location.locationBar.location = { 25 | pathname: url, 26 | search: '', 27 | } 28 | // 'trigger' a popstate 29 | location.locationBar.checkUrl() 30 | }, 31 | 32 | restore: function restore() { 33 | window.history.pushState = originalPushState 34 | }, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/location-bar/location_bar_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | location-bar test suite 6 | 7 | 8 | 9 |

10 | 
11 |   
12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 26 | 27 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /types/router.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../lib/router.js"],"names":[],"mappings":"4BAuBa,OAAO,mBAAmB,EAAE,aAAa;uBACzC,OAAO,gBAAgB,EAAE,QAAQ;yBACjC,OAAO,iBAAiB,EAAE,UAAU;;;;;YAMnC,KAAK,EAAE;;4BAGR,MAAS,SAAS,GAAG,QAAQ;;aAI5B,aAAa,GAAG,QAAQ,EAAE;eAC1B,aAAa;;;;;AAjB3B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;IACE;;OAEG;IACH,sBAFW,aAAa,EAoBvB;IAjBC,eAAe;IACf,UAAe;IACf,kBAAoB;IACpB;;;;;;;;sBAQC;IAQH;;;;;OAKG;IACH,yCAHY,MAAM,CAUjB;IAED;;;;;OAKG;IACH,YAJY,aAAa,GAAG,QAAQ,EAAE,GAC1B,MAAM,CAuFjB;IAlFC,gBAA4E;IAI1D,gBAAkB;IAgFtC;;;;;;OAMG;IACH,uBAJY,UAAU,CAmBrB;IAdmB,cAA+C;IAgBnE;;;;;;;;OAQG;IACH,uDAJY,UAAU,CASrB;IAED;;;;;;;;;OASG;IACH,mCANY,OAAO,MAAM,EAAE,GAAG,CAAC,UACnB,OAAO,MAAM,EAAE,GAAG,CAAC,GACnB,UAAU,CAMrB;IAED;;;;;;;;OAQG;IACH,0DAkBC;IAED;;;OAGG;IACH,gBAWC;IAED;;;;;;;;;OASG;IACH,gCAPY,OAAO,MAAM,EAAE,GAAG,CAAC,UACnB,OAAO,MAAM,EAAE,GAAG,CAAC,4BAmB9B;IAED;;;;;;;OAOG;IACH,uEAFY,UAAU,CA4BrB;IAED;;;;;;OAMG;IACH,yBAkCC;IAED;;;;OAIG;IACH,eAHW,MAAM,GACJ,UAAU,CAsDtB;IAED;;;;;;;;OAQG;IACH,qBALY,aAAa,OAiBxB;IAED,0BAEC;IAED,+BAEC;CACF;AAaD;;;;;;;GAOG;AACH,uCALW,MAAM,OACN,WAAW,iBACX,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,YAKrE;gCA1d+B,sBAAsB"} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Luiz Américo Pereira Câmara 4 | Copyright (c) 2017 Karolis Narkevicius 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples/hello-world-wc/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:400,700); 2 | 3 | body, html { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Open Sans', sans-serif; 7 | } 8 | 9 | .App { 10 | width: 800px; 11 | margin: 0 auto 20px auto; 12 | } 13 | 14 | .App-header { 15 | border-bottom: 1px solid #eee; 16 | } 17 | 18 | .App-header .active { 19 | font-weight: bolder; 20 | } 21 | 22 | .App-footer { 23 | position: fixed; 24 | left: 0; 25 | bottom: 0; 26 | width: 100%; 27 | padding-bottom: 20px; 28 | text-align: center; 29 | } 30 | 31 | .App h1 { 32 | display: inline-block; 33 | } 34 | 35 | .Nav { 36 | display: inline-block; 37 | } 38 | 39 | .Nav-item { 40 | list-style: none; 41 | display: inline-block; 42 | } 43 | 44 | .Nav-item a { 45 | padding: 10px; 46 | } 47 | 48 | .Tweet { 49 | border: 1px solid #eee; 50 | border-radius: 3px; 51 | padding: 10px; 52 | border-bottom: none; 53 | } 54 | 55 | .Tweet:last-child { 56 | border-bottom: 1px solid #eee; 57 | } 58 | 59 | .Tweet-author { 60 | font-weight: bold; 61 | display: inline-block; 62 | } 63 | 64 | .Tweet-time { 65 | color: #888; 66 | display: inline-block; 67 | margin-left: 20px; 68 | font-size: 12px; 69 | } -------------------------------------------------------------------------------- /types/path.d.ts: -------------------------------------------------------------------------------- 1 | export function clearPatternCompilerCache(): void; 2 | /** 3 | * Returns an array of the names of all parameters in the given pattern. 4 | */ 5 | export function extractParamNames(pattern: any, compiler: any): any; 6 | /** 7 | * Extracts the portions of the given URL path that match the given pattern 8 | * and returns an object of param name => value pairs. Returns null if the 9 | * pattern does not match the given path. 10 | */ 11 | export function extractParams(pattern: any, path: any, compiler: any): {}; 12 | /** 13 | * Returns a version of the given route path with params interpolated. Throws 14 | * if there is a dynamic segment of the route path for which there is no param. 15 | */ 16 | export function injectParams(pattern: any, params: any): any; 17 | /** 18 | * Returns an object that is the result of parsing any query string contained 19 | * in the given path, null if the path contains no query string. 20 | */ 21 | export function extractQuery(qs: any, path: any): any; 22 | /** 23 | * Returns a version of the given path with the parameters in the given 24 | * query merged into the query string. 25 | */ 26 | export function withQuery(qs: any, path: any, query: any): any; 27 | /** 28 | * Returns a version of the given path without the query string. 29 | */ 30 | export function withoutQuery(path: any): any; 31 | //# sourceMappingURL=path.d.ts.map -------------------------------------------------------------------------------- /examples/vanilla-blog/client/shared/base_handler.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery') 2 | const _ = require('lodash') 3 | 4 | module.exports = { 5 | template: _.template('
'), 6 | model: function (params) { 7 | const self = this 8 | return new Promise(function (resolve) { 9 | self.timeout = setTimeout(function () { 10 | resolve(params) 11 | }, 300) 12 | }) 13 | }, 14 | deactivate: function () { 15 | window.clearTimeout(this.timeout) 16 | if (this.$view) { 17 | this.$view.remove() 18 | } 19 | }, 20 | templateData: function () { 21 | return {} 22 | }, 23 | view: function (context) { 24 | let tpl = '
' + this.template(this.templateData(context)) + '
' 25 | const router = this.router 26 | tpl = tpl.replace(/\{\{link\:(.*)\}\}/g, function (match, routeId) { 27 | return router.generate(routeId) 28 | }) 29 | return $(tpl) 30 | }, 31 | activate: function () { 32 | this.$view = this.view.apply(this, arguments) 33 | this.$outlet = this.$view.find('.outlet') 34 | this.outlet().html(this.$view) 35 | }, 36 | outlet: function () { 37 | let parent = this.parent 38 | while (parent) { 39 | if (parent.$outlet) { 40 | return parent.$outlet 41 | } else { 42 | parent = parent.parent 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/functional/memoryTest.js: -------------------------------------------------------------------------------- 1 | import $ from './nanodom' 2 | import TestApp from './testApp' 3 | import 'chai/chai.js' 4 | 5 | const { assert } = window.chai 6 | const { describe, it, beforeEach, afterEach } = window 7 | 8 | let app, router 9 | 10 | describe('app using memory', () => { 11 | beforeEach(() => { 12 | window.location.hash = '' 13 | app = new TestApp({ 14 | location: 'memory', 15 | }) 16 | router = app.router 17 | return app.start() 18 | }) 19 | 20 | afterEach(() => { 21 | app.destroy() 22 | }) 23 | 24 | it('transition occurs when setURL', (done) => { 25 | router.use((transition) => { 26 | transition 27 | .then(() => { 28 | assert.equal(transition.path, '/about') 29 | assert.equal($('.application .outlet').html(), 'This is about page') 30 | done() 31 | }) 32 | .catch(done, done) 33 | }) 34 | 35 | router.location.setURL('/about') 36 | }) 37 | 38 | it('programmatic transition via url and route names', async function () { 39 | await router.transitionTo('about') 40 | assert.equal(router.location.getURL(), '/about') 41 | await router.transitionTo('/faq?sortBy=date') 42 | assert.equal(router.location.getURL(), '/faq?sortBy=date') 43 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: date') 44 | await router.transitionTo('faq', {}, { sortBy: 'user' }) 45 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: user') 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /examples/hello-world-wc/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'slick-router' 2 | import { wc } from 'slick-router/middlewares/wc.js' 3 | import { routerLinks } from 'slick-router/middlewares/router-links.js' 4 | import { events } from 'slick-router/middlewares/events.js' 5 | import { AnimatedOutlet, setDefaultAnimation, AnimateCSS } from 'slick-router/components/animated-outlet.js' 6 | import './components.js' 7 | 8 | setDefaultAnimation(AnimateCSS, { enter: 'rotateInDownRight', leave: 'hinge' }) 9 | 10 | customElements.define('router-outlet', AnimatedOutlet) 11 | 12 | // create the router 13 | const router = new Router({ 14 | log: true 15 | }) 16 | 17 | // provide your route map 18 | // in this particular case we configure components by its tag name 19 | 20 | router.map((route) => { 21 | route('application', { path: '/', component: 'application-view' }, () => { 22 | route('home', { path: '', component: 'home-view' }) 23 | route('messages', { component: 'messages-view' }) 24 | route('status', { path: ':user/status/:id' }) 25 | route('profile', { path: ':user', component: 'profile-view' }, () => { 26 | route('profile.index', { path: '', component: 'profile-index-view' }) 27 | route('profile.lists') 28 | route('profile.edit') 29 | }) 30 | }) 31 | }) 32 | 33 | // install middleware that will handle transitions 34 | router.use(wc) 35 | router.use(routerLinks) 36 | router.use(events) 37 | 38 | // start listening to browser's location bar changes 39 | router.listen() 40 | 41 | window.addEventListener('router-transition', function (e) { 42 | console.log('router.transition', e.detail.transition.pathname) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/functional/pushStateTest.js: -------------------------------------------------------------------------------- 1 | import $ from './nanodom' 2 | import fakeHistory from '../lib/fakeHistory' 3 | import TestApp from './testApp' 4 | import 'chai/chai.js' 5 | 6 | const { assert } = window.chai 7 | const { describe, it, beforeEach, afterEach } = window 8 | 9 | let app, router, history 10 | 11 | describe('app using pushState', () => { 12 | beforeEach(() => { 13 | window.location.hash = '' 14 | app = new TestApp({ 15 | pushState: true, 16 | root: '/app', 17 | }) 18 | router = app.router 19 | // eslint-disable-next-line no-return-assign 20 | return app.start().then(() => (history = fakeHistory(router.location))) 21 | }) 22 | 23 | afterEach(() => { 24 | app.destroy() 25 | history.restore() 26 | }) 27 | 28 | it('transition occurs when location.hash changes', (done) => { 29 | router.use((transition) => { 30 | transition 31 | .then(() => { 32 | assert.equal(transition.path, '/about') 33 | assert.equal($('.application .outlet').html(), 'This is about page') 34 | done() 35 | }) 36 | .catch(done, done) 37 | }) 38 | 39 | history.setURL('/app/about') 40 | }) 41 | 42 | it('programmatic transition via url and route names', async function () { 43 | await router.transitionTo('about') 44 | assert.equal(history.getURL(), '/app/about') 45 | await router.transitionTo('/faq?sortBy=date') 46 | assert.equal(history.getURL(), '/app/faq?sortBy=date') 47 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: date') 48 | await router.transitionTo('faq', {}, { sortBy: 'user' }) 49 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: user') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/unit/utilsTest.js: -------------------------------------------------------------------------------- 1 | import { clone, pick, isEqual, extend } from '../../lib/utils' 2 | import 'chai/chai.js' 3 | 4 | const { assert } = window.chai 5 | const { describe, it } = window 6 | 7 | describe('dash', () => { 8 | it('clone arrays', () => { 9 | const a = [1, 2, 3] 10 | const b = clone(a) 11 | b.push(4) 12 | assert.deepEqual(a, [1, 2, 3]) 13 | assert.deepEqual(b, [1, 2, 3, 4]) 14 | }) 15 | 16 | it('clone objects', () => { 17 | const a = { a: 1, b: 2 } 18 | const b = clone(a) 19 | b.c = 3 20 | assert.deepEqual(a, { a: 1, b: 2 }) 21 | assert.deepEqual(b, { a: 1, b: 2, c: 3 }) 22 | }) 23 | 24 | it('clone falsy values', () => { 25 | assert.equal(clone(undefined), undefined) 26 | assert.equal(clone(null), null) 27 | assert.equal(clone(false), false) 28 | assert.equal(clone(0), 0) 29 | }) 30 | 31 | it('pick', () => { 32 | assert.deepEqual(pick({ a: 1, b: 2, c: 3 }, ['a', 'c']), { a: 1, c: 3 }) 33 | assert.deepEqual(pick({ a: 1 }, ['a', 'c']), { a: 1 }) 34 | }) 35 | 36 | it('isEqual', () => { 37 | const arr = [] 38 | assert(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 })) 39 | assert(isEqual({ a: 1, b: arr }, { a: 1, b: arr })) 40 | assert.isNotOk(isEqual({ a: 1, b: 2 }, { a: 1, b: '2' })) 41 | assert.isNotOk(isEqual({ a: 1, b: 2 }, { a: 1 })) 42 | assert.isNotOk(isEqual({ a: 1, b: { c: 3 } }, { a: 1, b: { c: 3 } })) 43 | }) 44 | 45 | it('extend', () => { 46 | assert.deepEqual(extend({}, { a: 1, b: 2 }, null, { c: 3 }), { a: 1, b: 2, c: 3 }) 47 | 48 | const obj = { d: 4 } 49 | const target = {} 50 | extend(target, obj) 51 | target.a = 1 52 | obj.b = 2 53 | assert.deepEqual(obj, { b: 2, d: 4 }) 54 | assert.deepEqual(target, { a: 1, d: 4 }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /tests/unit/linksTest.js: -------------------------------------------------------------------------------- 1 | import $ from '../functional/nanodom' 2 | import { intercept } from '../../lib/links' 3 | import 'chai/chai.js' 4 | 5 | const { assert } = window.chai 6 | const { describe, it, beforeEach, afterEach } = window 7 | let $container 8 | let clickHandler 9 | 10 | describe('links', () => { 11 | beforeEach(() => { 12 | $container = $('
').appendTo('body') 13 | }) 14 | afterEach(() => { 15 | $container.empty().remove() 16 | document.removeEventListener('click', clickHandler) 17 | }) 18 | 19 | it('intercepts link clicks', () => { 20 | const $a = $('foo').appendTo($container) 21 | // prevent navigation 22 | 23 | const calledWith = [] 24 | const cb = (event, el) => calledWith.push({ event, el }) 25 | 26 | // proxy all clicks via this callback 27 | const dispose = intercept(document, cb) 28 | 29 | // install another click handler that will prevent 30 | // the navigation, we must install this after the 31 | // link.intercept has been already called 32 | let navPreventedCount = 0 33 | clickHandler = (e) => { 34 | navPreventedCount++ 35 | e.preventDefault() 36 | } 37 | document.addEventListener('click', clickHandler) 38 | 39 | // now it that when clicking the link, the calledWith 40 | $a.get(0).click() 41 | // it calls back with event and el 42 | assert.equal(calledWith[0].event.target, calledWith[0].el) 43 | // and the el is the link that was clicked 44 | assert.equal(calledWith[0].el, $a.get(0)) 45 | assert.equal(navPreventedCount, 1) 46 | 47 | // it that cleanup works 48 | dispose() 49 | // clicking this time 50 | $a.get(0).click() 51 | // should not call the cb again 52 | assert.equal(calledWith.length, 1) 53 | // only the nav prevention should kick in 54 | assert.equal(navPreventedCount, 2) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /types/middlewares/wc.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {*} value 3 | * @returns {PropertyHook} 4 | */ 5 | export function fromValue(value: any): PropertyHook; 6 | /** 7 | * @param {string} key 8 | * @param {string | Function} [format] 9 | * @returns {PropertyHook} 10 | */ 11 | export function fromQuery(queryKey: any, format?: string | Function): PropertyHook; 12 | /** 13 | * 14 | * @param {string} paramKey 15 | * @param {string | Function} format 16 | * @returns {PropertyHook} 17 | */ 18 | export function fromParam(paramKey: string, format: string | Function): PropertyHook; 19 | export function getRouteEl(route: any): any; 20 | export namespace wc { 21 | export { create }; 22 | export { destroy }; 23 | export { resolve }; 24 | export { done }; 25 | } 26 | export type Transition = import("../transition.js").Transition; 27 | export type WCRouteDef = { 28 | name: string; 29 | path: string; 30 | component: string | Function; 31 | properties?: { 32 | [x: string]: PropertyHook | PropertyHook[]; 33 | }; 34 | reuse?: boolean; 35 | beforeEnter?: (arg0: Transition) => Promise | boolean; 36 | beforeLeave?: (arg0: Transition) => Promise | boolean; 37 | afterEnter?: (arg0: Transition) => Promise | void; 38 | afterLeave?: (arg0: Transition) => Promise | void; 39 | }; 40 | export type PropertySetter = (value: any) => void; 41 | export type PropertyHook = { 42 | init?: (arg0: PropertySetter) => void; 43 | enter?: (arg0: Transition, arg1: PropertySetter) => void; 44 | leave?: (arg0: Transition, arg1: PropertySetter) => void; 45 | update?: (arg0: any, arg1: HTMLElement) => void; 46 | }; 47 | declare function create(instance: any): void; 48 | declare function destroy(): void; 49 | declare function resolve(transition: any): Promise; 50 | declare function done(transition: any): void; 51 | export {}; 52 | //# sourceMappingURL=wc.d.ts.map -------------------------------------------------------------------------------- /lib/function-dsl.js: -------------------------------------------------------------------------------- 1 | import invariant from './invariant.js' 2 | 3 | /** 4 | * @typedef {import("./router.js").Route} Route 5 | */ 6 | 7 | /** 8 | * @callback registerRoute 9 | * @param {string} name 10 | * @param {Object} options 11 | * @param {routeCallback} [childrenCallback] 12 | */ 13 | 14 | /** 15 | * @callback routeCallback 16 | * @param {registerRoute} route 17 | */ 18 | 19 | /** 20 | * @export 21 | * @param {routeCallback} callback 22 | * @return {Route[]} 23 | */ 24 | export default function functionDsl(callback) { 25 | let ancestors = [] 26 | const matches = {} 27 | const names = {} 28 | 29 | callback(function route(name, options, childrenCallback) { 30 | let routes 31 | 32 | invariant( 33 | !names[name], 34 | 'Route names must be unique, but route "%s" is declared multiple times', 35 | name, 36 | ) 37 | 38 | names[name] = true 39 | 40 | if (arguments.length === 1) { 41 | options = {} 42 | } 43 | 44 | if (arguments.length === 2 && typeof options === 'function') { 45 | childrenCallback = options 46 | options = {} 47 | } 48 | 49 | if (typeof options.path !== 'string') { 50 | const parts = name.split('.') 51 | options.path = parts[parts.length - 1] 52 | } 53 | 54 | // go to the next level 55 | if (childrenCallback) { 56 | ancestors = ancestors.concat(name) 57 | childrenCallback() 58 | routes = pop() 59 | ancestors.splice(-1) 60 | } 61 | 62 | // add the node to the tree 63 | push({ 64 | name, 65 | path: options.path, 66 | routes: routes || [], 67 | options, 68 | }) 69 | }) 70 | 71 | function pop() { 72 | return matches[currentLevel()] || [] 73 | } 74 | 75 | function push(route) { 76 | const level = currentLevel() 77 | matches[level] = matches[level] || [] 78 | matches[level].push(route) 79 | } 80 | 81 | function currentLevel() { 82 | return ancestors.join('.') 83 | } 84 | 85 | return pop() 86 | } 87 | -------------------------------------------------------------------------------- /lib/links.js: -------------------------------------------------------------------------------- 1 | import { bindEvent, unbindEvent } from './events.js' 2 | 3 | /** 4 | * Handle link delegation on `el` or the document, 5 | * and invoke `fn(e)` when clickable. 6 | * 7 | * @param {Element} el 8 | * @param {(e: Event, el: HTMLElement) => void} fn 9 | * @return {Function} dispose 10 | * @api public 11 | */ 12 | 13 | export function intercept(el, fn) { 14 | const cb = delegate(el, 'click', function (e, el) { 15 | if (clickable(e, el)) fn(e, el) 16 | }) 17 | 18 | return function dispose() { 19 | unbindEvent(el, 'click', cb) 20 | } 21 | } 22 | 23 | /** 24 | * Delegate event `type` to links 25 | * and invoke `fn(e)`. A callback function 26 | * is returned which may be passed to `.unbind()`. 27 | * 28 | * @param {HTMLElement} el 29 | * @param {String} selector 30 | * @param {String} type 31 | * @param {(e: Event, el: HTMLElement) => void} fn 32 | * @return {Function} 33 | * @api public 34 | */ 35 | 36 | function delegate(el, type, fn) { 37 | return bindEvent(el, type, function (e) { 38 | const el = e.target.closest('a') 39 | if (el) { 40 | fn(e, el) 41 | } 42 | }) 43 | } 44 | 45 | /** 46 | * Check if `e` is clickable. 47 | */ 48 | 49 | /** 50 | * @param {Event} e 51 | * @param {HTMLElement} el 52 | * @return {Boolean | undefined} 53 | */ 54 | function clickable(e, el) { 55 | if (which(e) !== 1) return 56 | if (e.metaKey || e.ctrlKey || e.shiftKey) return 57 | if (e.defaultPrevented) return 58 | 59 | // check target 60 | if (el.target) return 61 | 62 | // check for data-bypass attribute 63 | if (el.getAttribute('data-bypass') !== null) return 64 | 65 | // inspect the href 66 | const href = el.getAttribute('href') 67 | if (!href || href.length === 0) return 68 | 69 | // don't handle hash links, external/absolute links, email links and javascript links 70 | if (/^(#|https{0,1}:\/\/|mailto|javascript:)/i.test(href)) return 71 | 72 | return true 73 | } 74 | 75 | /** 76 | * Event button. 77 | */ 78 | 79 | function which(e) { 80 | e = e || window.event 81 | return e.which === null ? e.button : e.which 82 | } 83 | -------------------------------------------------------------------------------- /types/locations/browser.d.ts: -------------------------------------------------------------------------------- 1 | export default BrowserLocation; 2 | declare class BrowserLocation { 3 | constructor(options?: {}); 4 | path: any; 5 | options: { 6 | pushState: boolean; 7 | root: string; 8 | }; 9 | locationBar: LocationBar; 10 | /** 11 | * Get the current URL 12 | */ 13 | getURL(): any; 14 | /** 15 | * Set the current URL without triggering any events 16 | * back to the router. Add a new entry in browser's history. 17 | */ 18 | setURL(path: any, options?: {}): void; 19 | /** 20 | * Set the current URL without triggering any events 21 | * back to the router. Replace the latest entry in broser's history. 22 | */ 23 | replaceURL(path: any, options?: {}): void; 24 | /** 25 | * Setup a URL change handler 26 | * @param {Function} callback 27 | */ 28 | onChange(callback: Function): void; 29 | changeCallback: Function; 30 | /** 31 | * Given a path, generate a URL appending root 32 | * if pushState is used and # if hash state is used 33 | */ 34 | formatURL(path: any): string; 35 | /** 36 | * When we use pushState with a custom root option, 37 | * we need to take care of removingRoot at certain points. 38 | * Specifically 39 | * - browserLocation.update() can be called with the full URL by router 40 | * - LocationBar expects all .update() calls to be called without root 41 | * - this method is public so that we could dispatch URLs without root in router 42 | */ 43 | removeRoot(url: any): any; 44 | /** 45 | * Stop listening to URL changes and link clicks 46 | */ 47 | destroy(): void; 48 | /** 49 | initially, the changeCallback won't be defined yet, but that's good 50 | because we dont' want to kick off routing right away, the router 51 | does that later by manually calling this handleURL method with the 52 | url it reads of the location. But it's important this is called 53 | first by Backbone, because we wanna set a correct this.path value 54 | 55 | @private 56 | */ 57 | private handleURL; 58 | } 59 | import LocationBar from './location-bar.js'; 60 | //# sourceMappingURL=browser.d.ts.map -------------------------------------------------------------------------------- /docs/route-transition.md: -------------------------------------------------------------------------------- 1 | # Route Transition 2 | 3 | Slick Router defines route transition as the process of changing from a route state, generally represented by an URL, to another one. It provides tools to control with great granularity the transition like cancel, redirect, stop or retry. 4 | 5 | #### transition object 6 | 7 | The transition object is itself a promise. It also contains the following attributes 8 | 9 | * `id`: the transition id 10 | * `routes`: the matched routes 11 | * `path`: the matched path 12 | * `pathname`: the matched path without query params 13 | * `params`: a hash with path params 14 | * `query`: a hash with the query 15 | * `prev`: the previous matched info 16 | * `routes` 17 | * `path` 18 | * `pathname` 19 | * `params` 20 | * `query` 21 | 22 | And the following methods 23 | 24 | * `then` 25 | * `catch` 26 | * `cancel` 27 | * `retry` 28 | * `followRedirects` 29 | * `redirectTo` 30 | 31 | #### route 32 | 33 | During every transition, you can inspect `transition.routes` and `transition.prev.routes` to see where the router is transitioning to. These are arrays that contain a list of route descriptors. Each route descriptor has the following attributes 34 | 35 | * `name` - e.g. `'message'` 36 | * `path` - the path segment, e.g. `'message/:id'` 37 | * `params` - a list of params specifically for this route, e.g `{id: 1}` 38 | * `options` - the options object that was passed to the `route` function in the `map` 39 | 40 | 41 | ## Errors 42 | 43 | Transitions can fail, in which case the transition promise is rejected with the error object. This could happen, for example, if some middleware throws or returns a rejected promise. 44 | 45 | There are also two special errors that can be thrown when a redirect happens or when transition is cancelled completely. 46 | 47 | In case of redirect (someone initiating a router.transitionTo() while another transition was active) and error object will have a `type` attribute set to 'TransitionRedirected' and `nextPath` attribute set to the path of the new transition. 48 | 49 | In case of cancelling (someone calling transition.cancel()) the error object will have a `type` attribute set to 'TransitionCancelled'. 50 | 51 | If you have some error handling middleware - you most likely want to check for these two special errors, because they're normal to the functioning of the router, it's common to perform redirects. 52 | 53 | -------------------------------------------------------------------------------- /docs/programmatic-navigation-and-link.md: -------------------------------------------------------------------------------- 1 | # Programmatic Navigation and Link Handling 2 | 3 | ### router.transitionTo(name, params, query) 4 | 5 | Transition to a route, e.g. 6 | 7 | ```js 8 | router.transitionTo('about') 9 | router.transitionTo('posts.show', {postId: 1}) 10 | router.transitionTo('posts.show', {postId: 2}, {commentId: 2}) 11 | ``` 12 | 13 | ### router.replaceWith(name, params, query) 14 | 15 | Same as transitionTo, but doesn't add an entry in browser's history, instead replaces the current entry. Useful if you don't want this transition to be accessible via browser's Back button, e.g. if you're redirecting, or if you're navigating upon clicking tabs in the UI, etc. 16 | 17 | ### router.generate(name, params, query) 18 | 19 | Generate a URL for a route, e.g. 20 | 21 | ```js 22 | router.generate('about') 23 | router.generate('posts.show', {postId: 1}) 24 | router.generate('posts.show', {postId: 2}, {commentId: 2}) 25 | ``` 26 | 27 | It generates a URL with # if router is in hashChange mode and with no # if router is in pushState mode. 28 | 29 | ### router.isActive(name, params, query, exact) 30 | 31 | Check if a given route, params and query is active. 32 | 33 | ```js 34 | router.isActive('status') 35 | router.isActive('status', {user: 'me'}) 36 | router.isActive('status', {user: 'me'}, {commentId: 2}) 37 | router.isActive('status', null, {commentId: 2}) 38 | ``` 39 | 40 | When optional exact argument is truthy, the route is marked as active only if the path match exactly, e.g., 41 | 42 | ```js 43 | const routes = [ 44 | name: 'app', 45 | children: [ 46 | { 47 | name: 'dashboard' 48 | } 49 | ] 50 | ] 51 | 52 | // path = /app 53 | router.isActive('app', null, null) // true 54 | router.isActive('app', null, null, true) // true 55 | 56 | // path = /app/dashboard 57 | router.isActive('app', null, null) // true 58 | router.isActive('app', null, null, true) // false 59 | ``` 60 | 61 | ### router.state 62 | 63 | The state of the route is always available on the `router.state` object. It contains `activeTransition`, `routes`, `path`, `pathname`, `params` and `query`. 64 | 65 | ### router.matchers 66 | 67 | Use this to inspect all the routes and their URL patterns that exist in your application. It's an array of: 68 | 69 | ```js 70 | { 71 | name, 72 | path, 73 | routes 74 | } 75 | ``` 76 | 77 | listed in the order that they will be matched against the URL. 78 | -------------------------------------------------------------------------------- /tests/location-bar/backbone_router_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Backbone.Router test suite 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 35 | 36 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slick-router", 3 | "version": "3.0.2", 4 | "description": "A powerful and flexible client side router", 5 | "main": "./lib/wc-router.js", 6 | "module": "./lib/wc-router.js", 7 | "types": "./types/wc-router.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "build": "eslint --env browser && node tasks/build.js", 11 | "lint": "eslint --env browser lib", 12 | "format": "prettier --write .", 13 | "start": "web-dev-server --open examples/ --node-resolve", 14 | "test": "web-test-runner \"tests/**/*Test.js\" --node-resolve --puppeteer", 15 | "test:coverage": "web-test-runner \"tests/**/*Test.js\" --node-resolve --coverage", 16 | "types": "tsc --project tsconfig.types.json" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/blikblum/slick-router.git" 21 | }, 22 | "author": "Luiz Américo Pereira Câmara", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/blikblum/slick-router/issues" 26 | }, 27 | "dependencies": { 28 | "regexparam": "^3.0.0" 29 | }, 30 | "keywords": [ 31 | "router", 32 | "web-components", 33 | "browser", 34 | "pushState", 35 | "hierarchical", 36 | "nested" 37 | ], 38 | "exports": { 39 | ".": { 40 | "types": "./types/wc-router.d.ts", 41 | "default": "./lib/wc-router.js" 42 | }, 43 | "./core.js": { 44 | "types": "./types/router.d.ts", 45 | "default": "./lib/router.js" 46 | }, 47 | "./components/*.js": { 48 | "types": "./types/components/*.d.ts", 49 | "default": "./lib/components/*.js" 50 | }, 51 | "./middlewares/*.js": { 52 | "types": "./types/middlewares/*.d.ts", 53 | "default": "./lib/middlewares/*.js" 54 | } 55 | }, 56 | "files": [ 57 | "lib", 58 | "types", 59 | "README.md", 60 | "CHANGELOG.md", 61 | "LICENSE" 62 | ], 63 | "devDependencies": { 64 | "@open-wc/testing": "^4.0.0", 65 | "@web/dev-server": "^0.4.2", 66 | "@web/test-runner": "^0.18.0", 67 | "@web/test-runner-puppeteer": "^0.15.0", 68 | "chai": "^4.3.4", 69 | "eslint": "^8.56.0", 70 | "eslint-config-prettier": "^9.1.0", 71 | "jquery": "^3.6.0", 72 | "lit-element": "^2.5.1", 73 | "path-to-regexp": "6.2.1", 74 | "prettier": "3.2.5", 75 | "sinon": "^11.1.2", 76 | "typescript": "^5.4.4" 77 | }, 78 | "packageManager": "yarn@4.0.2" 79 | } -------------------------------------------------------------------------------- /examples/vanilla-blog/client/app.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('slick-router') 2 | const getHandler = require('./handler') 3 | 4 | // create the router 5 | const router = window.router = new Router({ 6 | log: true 7 | }) 8 | 9 | // define the route map 10 | router.map(function (route) { 11 | route('application', { path: '/', abstract: true }, function () { 12 | route('home', { path: '' }) 13 | route('about') 14 | route('faq') 15 | route('posts', { abstract: true }, function () { 16 | route('posts.index', { path: '' }) 17 | route('posts.popular') 18 | route('posts.search', { path: 'search/:query' }) 19 | route('posts.show', { path: ':id' }) 20 | }) 21 | }) 22 | }) 23 | 24 | // implement a set of middleware 25 | 26 | // load and attach route handlers 27 | // this can load handlers dynamically (TODO) 28 | router.use(function loadHandlers (transition) { 29 | transition.routes.forEach(function (route, i) { 30 | const handler = getHandler(route.name) 31 | handler.name = route.name 32 | handler.router = router 33 | const parentRoute = transition.routes[i - 1] 34 | if (parentRoute) { 35 | handler.parent = parentRoute.handler 36 | } 37 | route.handler = handler 38 | }) 39 | }) 40 | 41 | // willTransition hook 42 | router.use(function willTransition (transition) { 43 | transition.prev.routes.forEach(function (route) { 44 | route.handler.willTransition && route.handler.willTransition(transition) 45 | }) 46 | }) 47 | 48 | // deactive up old routes 49 | // they also get a chance to abort the transition (TODO) 50 | router.use(function deactivateHook (transition) { 51 | transition.prev.routes.forEach(function (route) { 52 | route.handler.deactivate() 53 | }) 54 | }) 55 | 56 | // model hook 57 | // with the loading hook (TODO) 58 | router.use(function modelHook (transition) { 59 | let prevContext = Promise.resolve() 60 | return Promise.all(transition.routes.map(function (route) { 61 | prevContext = Promise.resolve(route.handler.model(transition.params, prevContext, transition)) 62 | return prevContext 63 | })) 64 | }) 65 | 66 | // activate hook 67 | // which only reactives routes starting at the match point (TODO) 68 | router.use(function activateHook (transition, contexts) { 69 | transition.routes.forEach(function (route, i) { 70 | route.handler.activate(contexts[i]) 71 | }) 72 | }) 73 | 74 | // start the routing 75 | router.listen() 76 | -------------------------------------------------------------------------------- /tests/functional/testApp.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable array-callback-return */ 2 | import $ from './nanodom' 3 | import { Router } from '../../lib/router' 4 | 5 | export default function TestApp(options) { 6 | options = options || {} 7 | 8 | // create the router 9 | const router = (this.router = new Router(options)) 10 | 11 | // provide the route map 12 | router.map(function (route) { 13 | route('application', { path: '/' }, function () { 14 | route('about') 15 | route('faq') 16 | route('posts', function () { 17 | route('posts.popular') 18 | route('posts.filter', { path: 'filter/:filterId' }) 19 | route('posts.show', { path: ':id' }) 20 | }) 21 | }) 22 | }) 23 | 24 | const handlers = {} 25 | 26 | handlers.application = { 27 | // this is a hook for 'performing' 28 | // actions upon entering this state 29 | activate: function () { 30 | this.$view = $( 31 | '
', 32 | ) 33 | this.$view.html('

Slick Router Application

') 34 | this.$outlet = this.$view.find('.outlet') 35 | this.$outlet.html('Welcome to this application') 36 | $(document.body).html(this.$view) 37 | }, 38 | } 39 | 40 | handlers.about = { 41 | activate: function () { 42 | this.parent.$outlet.html('This is about page') 43 | }, 44 | } 45 | 46 | handlers.faq = { 47 | activate: function (params, query) { 48 | this.parent.$outlet.html('FAQ. Sorted By: ' + query.sortBy) 49 | }, 50 | } 51 | 52 | handlers.posts = { 53 | activate: function () {}, 54 | } 55 | 56 | handlers['posts.filter'] = { 57 | activate: function (params) { 58 | if (params.filterId === 'mine') { 59 | this.parent.parent.$outlet.html('My posts...') 60 | } else { 61 | this.parent.parent.$outlet.html('Filter not found') 62 | } 63 | }, 64 | } 65 | 66 | router.use((transition) => { 67 | transition.routes.forEach((route, i) => { 68 | const handler = handlers[route.name] 69 | const parentRoute = transition.routes[i - 1] 70 | handler.parent = parentRoute ? handlers[parentRoute.name] : null 71 | handler.activate(transition.params, transition.query) 72 | }) 73 | }) 74 | } 75 | 76 | TestApp.prototype.start = function () { 77 | return this.router.listen() 78 | } 79 | 80 | TestApp.prototype.destroy = function () { 81 | document.body.innerHTML = '' 82 | return this.router.destroy() 83 | } 84 | -------------------------------------------------------------------------------- /tests/functional/nanodom.js: -------------------------------------------------------------------------------- 1 | function Dom() {} 2 | Dom.prototype = new Array() // eslint-disable-line 3 | Dom.prototype.append = function (element) { 4 | element.forEach( 5 | function (e) { 6 | this[0].appendChild(e) 7 | }.bind(this), 8 | ) 9 | return this 10 | } 11 | Dom.prototype.remove = function () { 12 | this.forEach(function (e) { 13 | e.parentNode.removeChild(e) 14 | }) 15 | return this 16 | } 17 | Dom.prototype.prepend = function (element) { 18 | element.forEach( 19 | function (e) { 20 | this[0].insertBefore(e, this[0].hasChildNodes() ? this[0].childNodes[0] : null) 21 | }.bind(this), 22 | ) 23 | return this 24 | } 25 | Dom.prototype.each = function (fn) { 26 | this.forEach(fn) 27 | return this 28 | } 29 | 30 | function stringify(dom) { 31 | return dom 32 | .map(function (el) { 33 | return el.innerHTML 34 | }) 35 | .join() 36 | } 37 | 38 | Dom.prototype.html = function (content) { 39 | if (content === undefined) { 40 | return stringify(this) 41 | } 42 | if (content instanceof Dom) { 43 | return this.empty().append(content) 44 | } 45 | this.forEach(function (e) { 46 | e.innerHTML = content 47 | }) 48 | return this 49 | } 50 | 51 | Dom.prototype.find = function (selector) { 52 | const result = new Dom() 53 | this.forEach(function (el) { 54 | ;[].slice.call(el.querySelectorAll(selector)).forEach(function (e) { 55 | result.push(e) 56 | }) 57 | }) 58 | return result 59 | } 60 | 61 | Dom.prototype.empty = function () { 62 | this.forEach(function (el) { 63 | el.innerHTML = '' 64 | }) 65 | return this 66 | } 67 | 68 | Dom.prototype.appendTo = function (target) { 69 | nanodom(target).append(this) 70 | return this 71 | } 72 | 73 | Dom.prototype.get = function (index) { 74 | return this[index] 75 | } 76 | 77 | function domify(str) { 78 | const d = document.createElement('div') 79 | d.innerHTML = str 80 | return d.childNodes 81 | } 82 | 83 | const nanodom = function (selector) { 84 | let d 85 | if (selector instanceof Dom) return selector 86 | if (selector instanceof HTMLElement) { 87 | d = new Dom() 88 | d.push(selector) 89 | return d 90 | } 91 | if (typeof selector !== 'string') return 92 | d = new Dom() 93 | const c = selector.indexOf('<') === 0 94 | const s = c ? domify(selector) : document.querySelectorAll(selector) 95 | ;[].slice.call(s).forEach(function (e) { 96 | d.push(e) 97 | }) 98 | return d 99 | } 100 | 101 | export default nanodom 102 | -------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/posts/screens/show/templates/show.html: -------------------------------------------------------------------------------- 1 |
2 |

<%- title %>

3 |
4 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rhoncus condimentum sem a facilisis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas lacinia fringilla tristique. Ut sollicitudin felis sem, in aliquam lectus eleifend nec. Pellentesque id nisi non arcu venenatis convallis vel id nulla. Nullam ut accumsan felis. Nunc nec tincidunt sapien, et posuere massa.

5 |

Cras sollicitudin neque ac erat fermentum, quis pulvinar dolor porta. Donec enim lacus, scelerisque ac iaculis eu, rhoncus sed urna. In hac habitasse platea dictumst. Nullam sodales, leo at congue ultricies, mauris sapien ornare ipsum, id dictum orci nulla at leo. Nullam non tristique mauris. Donec aliquam lobortis tortor in volutpat. In at sem sed felis faucibus vulputate. Duis risus leo, aliquet ut lectus vel, sollicitudin egestas dui. Morbi eget justo tristique, tincidunt turpis interdum, faucibus turpis. Sed venenatis ut augue non accumsan. Aliquam at lorem et dui accumsan sodales. Mauris fermentum, ligula imperdiet pharetra feugiat, erat tortor sollicitudin nulla, et condimentum mauris lectus ullamcorper enim. Integer volutpat mauris nisl, non congue turpis fringilla et. Sed rhoncus mollis libero, ut lacinia quam elementum in.

6 |

Pellentesque eu arcu condimentum, vulputate leo nec, tincidunt massa. Vestibulum leo libero, aliquet nec enim quis, consequat faucibus elit. Sed tristique et velit vel iaculis. Sed suscipit commodo tellus nec imperdiet. Aenean tristique at urna eget aliquet. Etiam vestibulum ligula ac nunc viverra, quis scelerisque est facilisis. Praesent fermentum eros urna, nec consequat sapien accumsan sit amet. Maecenas congue elit id lacinia scelerisque. Mauris in nibh justo.

7 |

Suspendisse potenti. Vestibulum eu molestie diam. Quisque tristique volutpat felis, eget tempor lorem ullamcorper sed. Vivamus aliquam cursus mollis. Cras accumsan justo nec augue dignissim lacinia. Mauris eu tortor lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque at lacus a elit elementum semper. Morbi dictum metus lorem, eu ornare nisi gravida ut. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse velit odio, posuere sed mi quis, elementum elementum urna. Etiam mi mi, sollicitudin at ipsum sit amet, posuere gravida sapien.

8 |

Curabitur vel mauris id velit lobortis rutrum. In hac habitasse platea dictumst. Fusce in mattis ante, eget tristique ipsum. Sed lorem augue, laoreet eu viverra eget, porttitor sed massa. Quisque suscipit mauris sem, vitae varius arcu gravida sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut ipsum libero, fringilla eu nibh vitae, porta euismod lacus. Curabitur arcu nibh, tincidunt in fringilla quis, rutrum id purus.

-------------------------------------------------------------------------------- /examples/vanilla-blog/client/screens/app/screens/about/templates/about.html: -------------------------------------------------------------------------------- 1 |
2 |

About

3 |
4 |
5 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec rhoncus condimentum sem a facilisis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas lacinia fringilla tristique. Ut sollicitudin felis sem, in aliquam lectus eleifend nec. Pellentesque id nisi non arcu venenatis convallis vel id nulla. Nullam ut accumsan felis. Nunc nec tincidunt sapien, et posuere massa.

6 |

Cras sollicitudin neque ac erat fermentum, quis pulvinar dolor porta. Donec enim lacus, scelerisque ac iaculis eu, rhoncus sed urna. In hac habitasse platea dictumst. Nullam sodales, leo at congue ultricies, mauris sapien ornare ipsum, id dictum orci nulla at leo. Nullam non tristique mauris. Donec aliquam lobortis tortor in volutpat. In at sem sed felis faucibus vulputate. Duis risus leo, aliquet ut lectus vel, sollicitudin egestas dui. Morbi eget justo tristique, tincidunt turpis interdum, faucibus turpis. Sed venenatis ut augue non accumsan. Aliquam at lorem et dui accumsan sodales. Mauris fermentum, ligula imperdiet pharetra feugiat, erat tortor sollicitudin nulla, et condimentum mauris lectus ullamcorper enim. Integer volutpat mauris nisl, non congue turpis fringilla et. Sed rhoncus mollis libero, ut lacinia quam elementum in.

7 |

Pellentesque eu arcu condimentum, vulputate leo nec, tincidunt massa. Vestibulum leo libero, aliquet nec enim quis, consequat faucibus elit. Sed tristique et velit vel iaculis. Sed suscipit commodo tellus nec imperdiet. Aenean tristique at urna eget aliquet. Etiam vestibulum ligula ac nunc viverra, quis scelerisque est facilisis. Praesent fermentum eros urna, nec consequat sapien accumsan sit amet. Maecenas congue elit id lacinia scelerisque. Mauris in nibh justo.

8 |

Suspendisse potenti. Vestibulum eu molestie diam. Quisque tristique volutpat felis, eget tempor lorem ullamcorper sed. Vivamus aliquam cursus mollis. Cras accumsan justo nec augue dignissim lacinia. Mauris eu tortor lacus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque at lacus a elit elementum semper. Morbi dictum metus lorem, eu ornare nisi gravida ut. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse velit odio, posuere sed mi quis, elementum elementum urna. Etiam mi mi, sollicitudin at ipsum sit amet, posuere gravida sapien.

9 |

Curabitur vel mauris id velit lobortis rutrum. In hac habitasse platea dictumst. Fusce in mattis ante, eget tristique ipsum. Sed lorem augue, laoreet eu viverra eget, porttitor sed massa. Quisque suscipit mauris sem, vitae varius arcu gravida sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut ipsum libero, fringilla eu nibh vitae, porta euismod lacus. Curabitur arcu nibh, tincidunt in fringilla quis, rutrum id purus.

10 |
-------------------------------------------------------------------------------- /lib/path.js: -------------------------------------------------------------------------------- 1 | import invariant from './invariant.js' 2 | 3 | const paramInjectMatcher = /:([a-zA-Z_$][a-zA-Z0-9_$?]*[?+*]?)/g 4 | const specialParamChars = /[+*?]$/g 5 | const queryMatcher = /\?(.+)/ 6 | 7 | const _compiledPatterns = {} 8 | 9 | function compilePattern(pattern, compiler) { 10 | if (!(pattern in _compiledPatterns)) { 11 | _compiledPatterns[pattern] = compiler(pattern) 12 | } 13 | 14 | return _compiledPatterns[pattern] 15 | } 16 | 17 | export function clearPatternCompilerCache() { 18 | for (const x in _compiledPatterns) { 19 | delete _compiledPatterns[x] 20 | } 21 | } 22 | 23 | /** 24 | * Returns an array of the names of all parameters in the given pattern. 25 | */ 26 | export function extractParamNames(pattern, compiler) { 27 | return compilePattern(pattern, compiler).paramNames 28 | } 29 | 30 | /** 31 | * Extracts the portions of the given URL path that match the given pattern 32 | * and returns an object of param name => value pairs. Returns null if the 33 | * pattern does not match the given path. 34 | */ 35 | export function extractParams(pattern, path, compiler) { 36 | const cp = compilePattern(pattern, compiler) 37 | const matcher = cp.matcher 38 | const paramNames = cp.paramNames 39 | const match = path.match(matcher) 40 | 41 | if (!match) { 42 | return null 43 | } 44 | 45 | const params = {} 46 | 47 | paramNames.forEach(function (paramName, index) { 48 | params[paramName] = match[index + 1] && decodeURIComponent(match[index + 1]) 49 | }) 50 | 51 | return params 52 | } 53 | 54 | /** 55 | * Returns a version of the given route path with params interpolated. Throws 56 | * if there is a dynamic segment of the route path for which there is no param. 57 | */ 58 | export function injectParams(pattern, params) { 59 | params = params || {} 60 | 61 | return pattern.replace(paramInjectMatcher, function (match, param) { 62 | const paramName = param.replace(specialParamChars, '') 63 | const lastChar = param.slice(-1) 64 | 65 | // If param is optional don't check for existence 66 | if (lastChar === '?' || lastChar === '*') { 67 | if (params[paramName] == null) { 68 | return '' 69 | } 70 | } else { 71 | invariant( 72 | params[paramName] != null, 73 | "Missing '%s' parameter for path '%s'", 74 | paramName, 75 | pattern, 76 | ) 77 | } 78 | 79 | let paramValue = encodeURIComponent(params[paramName]) 80 | if (lastChar === '*' || lastChar === '+') { 81 | // restore / for splats 82 | paramValue = paramValue.replace('%2F', '/') 83 | } 84 | return paramValue 85 | }) 86 | } 87 | 88 | /** 89 | * Returns an object that is the result of parsing any query string contained 90 | * in the given path, null if the path contains no query string. 91 | */ 92 | export function extractQuery(qs, path) { 93 | const match = path.match(queryMatcher) 94 | return match && qs.parse(match[1]) 95 | } 96 | 97 | /** 98 | * Returns a version of the given path with the parameters in the given 99 | * query merged into the query string. 100 | */ 101 | export function withQuery(qs, path, query) { 102 | const queryString = qs.stringify(query, { indices: false }) 103 | 104 | if (queryString) { 105 | return withoutQuery(path) + '?' + queryString 106 | } 107 | 108 | return path 109 | } 110 | 111 | /** 112 | * Returns a version of the given path without the query string. 113 | */ 114 | export function withoutQuery(path) { 115 | return path.replace(queryMatcher, '') 116 | } 117 | -------------------------------------------------------------------------------- /lib/locations/browser.js: -------------------------------------------------------------------------------- 1 | import { extend } from '../utils.js' 2 | import LocationBar from './location-bar.js' 3 | 4 | class BrowserLocation { 5 | constructor(options = {}) { 6 | this.path = options.path || '' 7 | 8 | this.options = extend( 9 | { 10 | pushState: false, 11 | root: '/', 12 | }, 13 | options, 14 | ) 15 | 16 | // we're using the location-bar module for actual 17 | // URL management 18 | this.locationBar = new LocationBar() 19 | this.locationBar.onChange((path) => { 20 | this.handleURL(`/${path || ''}`) 21 | }) 22 | 23 | this.locationBar.start(options) 24 | } 25 | 26 | /** 27 | * Get the current URL 28 | */ 29 | 30 | getURL() { 31 | return this.path 32 | } 33 | 34 | /** 35 | * Set the current URL without triggering any events 36 | * back to the router. Add a new entry in browser's history. 37 | */ 38 | 39 | setURL(path, options = {}) { 40 | if (this.path !== path) { 41 | this.path = path 42 | this.locationBar.update(path, extend({ trigger: true }, options)) 43 | } 44 | } 45 | 46 | /** 47 | * Set the current URL without triggering any events 48 | * back to the router. Replace the latest entry in broser's history. 49 | */ 50 | 51 | replaceURL(path, options = {}) { 52 | if (this.path !== path) { 53 | this.path = path 54 | this.locationBar.update(path, extend({ trigger: true, replace: true }, options)) 55 | } 56 | } 57 | 58 | /** 59 | * Setup a URL change handler 60 | * @param {Function} callback 61 | */ 62 | onChange(callback) { 63 | this.changeCallback = callback 64 | } 65 | 66 | /** 67 | * Given a path, generate a URL appending root 68 | * if pushState is used and # if hash state is used 69 | */ 70 | formatURL(path) { 71 | if (this.locationBar.hasPushState()) { 72 | let rootURL = this.options.root 73 | if (path !== '') { 74 | rootURL = rootURL.replace(/\/$/, '') 75 | } 76 | return rootURL + path 77 | } else { 78 | if (path[0] === '/') { 79 | path = path.substr(1) 80 | } 81 | return `#${path}` 82 | } 83 | } 84 | 85 | /** 86 | * When we use pushState with a custom root option, 87 | * we need to take care of removingRoot at certain points. 88 | * Specifically 89 | * - browserLocation.update() can be called with the full URL by router 90 | * - LocationBar expects all .update() calls to be called without root 91 | * - this method is public so that we could dispatch URLs without root in router 92 | */ 93 | removeRoot(url) { 94 | if (this.options.pushState && this.options.root && this.options.root !== '/') { 95 | return url.replace(this.options.root, '') 96 | } else { 97 | return url 98 | } 99 | } 100 | 101 | /** 102 | * Stop listening to URL changes and link clicks 103 | */ 104 | destroy() { 105 | this.locationBar.stop() 106 | } 107 | 108 | /** 109 | initially, the changeCallback won't be defined yet, but that's good 110 | because we dont' want to kick off routing right away, the router 111 | does that later by manually calling this handleURL method with the 112 | url it reads of the location. But it's important this is called 113 | first by Backbone, because we wanna set a correct this.path value 114 | 115 | @private 116 | */ 117 | handleURL(url) { 118 | this.path = url 119 | if (this.changeCallback) { 120 | this.changeCallback(url) 121 | } 122 | } 123 | } 124 | 125 | export default BrowserLocation 126 | -------------------------------------------------------------------------------- /tests/location-bar/vendor/runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QtWebKit-powered headless test runner using PhantomJS 3 | * 4 | * PhantomJS binaries: http://phantomjs.org/download.html 5 | * Requires PhantomJS 1.6+ (1.7+ recommended) 6 | * 7 | * Run with: 8 | * phantomjs runner.js [url-of-your-qunit-testsuite] 9 | * 10 | * e.g. 11 | * phantomjs runner.js http://localhost/qunit/test/index.html 12 | */ 13 | 14 | /*jshint latedef:false */ 15 | /*global phantom:false, require:false, console:false, window:false, QUnit:false */ 16 | 17 | (function() { 18 | 'use strict'; 19 | 20 | var args = require('system').args; 21 | 22 | // arg[0]: scriptName, args[1...]: arguments 23 | if (args.length !== 2) { 24 | console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite]'); 25 | phantom.exit(1); 26 | } 27 | 28 | var url = args[1], 29 | page = require('webpage').create(); 30 | 31 | // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) 32 | page.onConsoleMessage = function(msg) { 33 | console.log(msg); 34 | }; 35 | 36 | page.onInitialized = function() { 37 | page.evaluate(addLogging); 38 | }; 39 | 40 | page.onCallback = function(message) { 41 | var result, 42 | failed; 43 | 44 | if (message) { 45 | if (message.name === 'QUnit.done') { 46 | result = message.data; 47 | failed = !result || result.failed; 48 | 49 | phantom.exit(failed ? 1 : 0); 50 | } 51 | } 52 | }; 53 | 54 | page.open(url, function(status) { 55 | if (status !== 'success') { 56 | console.error('Unable to access network: ' + status); 57 | phantom.exit(1); 58 | } else { 59 | // Cannot do this verification with the 'DOMContentLoaded' handler because it 60 | // will be too late to attach it if a page does not have any script tags. 61 | var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); 62 | if (qunitMissing) { 63 | console.error('The `QUnit` object is not present on this page.'); 64 | phantom.exit(1); 65 | } 66 | 67 | // Do nothing... the callback mechanism will handle everything! 68 | } 69 | }); 70 | 71 | function addLogging() { 72 | window.document.addEventListener('DOMContentLoaded', function() { 73 | var current_test_assertions = []; 74 | 75 | QUnit.log(function(details) { 76 | var response; 77 | 78 | // Ignore passing assertions 79 | if (details.result) { 80 | return; 81 | } 82 | 83 | response = details.message || ''; 84 | 85 | if (typeof details.expected !== 'undefined') { 86 | if (response) { 87 | response += ', '; 88 | } 89 | 90 | response += 'expected: ' + details.expected + ', but was: ' + details.actual; 91 | if (details.source) { 92 | response += "\n" + details.source; 93 | } 94 | } 95 | 96 | current_test_assertions.push('Failed assertion: ' + response); 97 | }); 98 | 99 | QUnit.testDone(function(result) { 100 | var i, 101 | len, 102 | name = result.module + ': ' + result.name; 103 | 104 | if (result.failed) { 105 | console.log('Test failed: ' + name); 106 | 107 | for (i = 0, len = current_test_assertions.length; i < len; i++) { 108 | console.log(' ' + current_test_assertions[i]); 109 | } 110 | } 111 | 112 | current_test_assertions.length = 0; 113 | }); 114 | 115 | QUnit.done(function(result) { 116 | console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); 117 | 118 | if (typeof window.callPhantom === 'function') { 119 | window.callPhantom({ 120 | 'name': 'QUnit.done', 121 | 'data': result 122 | }); 123 | } 124 | }); 125 | }, false); 126 | } 127 | })(); 128 | -------------------------------------------------------------------------------- /docs/components/animated-outlet.md: -------------------------------------------------------------------------------- 1 | # animated-outlet component 2 | 3 | Enable animation on web component swapping triggered by route transitions. 4 | 5 | ## Usage 6 | 7 | 1) Register a `AnimatedOutlet` web component to the tag to be used as router outlet. wc middleware uses 'router-outlet' tag as default, but any tag can be used. 8 | 9 | ```javascript 10 | import { AnimatedOutlet } from 'slick-router/components/animated-outlet' 11 | 12 | customElements.define('router-outlet', AnimatedOutlet) 13 | ``` 14 | 15 | 2) Add an 'animation' attribute to the outlet that will be animated 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | 3) Write the animation using CSS transition or animation 22 | 23 | ```css 24 | .outlet-enter-active, 25 | .outlet-leave-active { 26 | transition: opacity 0.5s; 27 | } 28 | 29 | .outlet-enter, .outlet-leave-to { 30 | opacity: 0; 31 | } 32 | ``` 33 | 34 | The above example adds a fading effect to the element that is entering and the one which is leaving 35 | 36 | > The API is based on [Vue one](https://vuejs.org/v2/guide/transitions.html#Transition-Classes) and most of the Vue animations can be converted with little changes 37 | 38 | Is possible to configure the CSS classes prefix through animation attribute, allowing to create more than one animation in same app: 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | ```css 45 | .bounce-enter { 46 | opacity: 0; 47 | } 48 | 49 | .bounce-enter-active { 50 | animation: bounce-in 0.5s; 51 | } 52 | 53 | .bounce-leave-active { 54 | animation: bounce-in 0.5s reverse; 55 | } 56 | 57 | @keyframes bounce-in { 58 | 0% { 59 | transform: scale(0); 60 | } 61 | 50% { 62 | transform: scale(1.5); 63 | } 64 | 100% { 65 | transform: scale(1); 66 | } 67 | } 68 | ``` 69 | 70 | The example above uses classes prefixed with 'bounce-' instead of 'outlet-' 71 | 72 | [Live Demo](https://codesandbox.io/s/slick-router-css-animations-q0fzs) 73 | 74 | ## Customization 75 | 76 | Is possible to customize how animation is done by creating and registering animation hook classes. It must extend from AnimationHook: 77 | 78 | ```js 79 | import { AnimationHook } from 'slick-router/components/animated-outlet.js' 80 | 81 | class MyAnimation extends AnimationHook { 82 | beforeEnter (outlet, el) { 83 | // prepare element before is connected 84 | } 85 | 86 | enter (outlet, el) { 87 | // run enter animation 88 | } 89 | 90 | leave (outlet, el, done) { 91 | // run leave animation and call done on finish 92 | done() 93 | } 94 | } 95 | ``` 96 | 97 | The hook class can be registered as default with `setDefaultAnimation` or to predefined animations using `registerAnimation` 98 | 99 | Out of box, is provided the `AnimateCSS` class that allows to use [animate.css](https://github.com/daneden/animate.css) 100 | 101 | ```js 102 | import { 103 | AnimatedOutlet, 104 | AnimateCSS, 105 | setDefaultAnimation, 106 | registerAnimation 107 | } from 'slick-router/components/animated-outlet.js' 108 | 109 | setDefaultAnimation(AnimateCSS, { enter: 'fadeIn', leave: 'fadeOut' }) 110 | 111 | registerAnimation('funky', AnimateCSS, { enter: 'rotateInDownRight', leave: 'hinge' }) 112 | ``` 113 | 114 | ```html 115 | 116 | 117 | 118 | 119 | 120 | ``` 121 | 122 | [Live demo](https://codesandbox.io/s/slick-router-animate-css-zpg96) 123 | 124 | It's possible to use JS animation libraries like [GSAP](https://codesandbox.io/s/slick-router-gsap-animations-oqbp5) or even as [standalone component](https://codesandbox.io/s/animated-outlet-page-transitions-7vgcy) (without routing envolved) 125 | 126 | 127 | -------------------------------------------------------------------------------- /examples/hello-world-jquery/index.js: -------------------------------------------------------------------------------- 1 | import 'jquery' 2 | import { Router } from '../../lib/router.js' 3 | 4 | // create the router 5 | const router = new Router({ 6 | log: true 7 | }) 8 | 9 | // create some handlers 10 | const application = { 11 | activate: function () { 12 | this.view = $(` 13 |
14 |
15 |

Application

16 | 21 |
22 |
23 |
24 | `) 25 | } 26 | } 27 | 28 | const home = { 29 | activate: function () { 30 | this.view = $(` 31 |
32 |

Tweets

33 |
34 | 37 |
12m12 minutes ago
38 |
Another use case for \`this.context\` I think might be valid: forms. They're too painful right now.
39 |
40 |
41 | 44 |
12m12 minutes ago
45 |
I just published “What will Datasmoothie bring to the analytics startup landscape?” https://medium.com/@afanasjevas/what-will-datasmoothie-bring-to-the-analytics-startup-landscape-f7dab70d75c3?source=tw-81c4e81fe6f8-1427630532296
46 |
47 |
48 |
49 | LNUG ‏@LNUGorg 50 |
51 |
52m52 minutes ago
52 |
new talks uploaded on our YouTube page - check them out http://bit.ly/1yoXSAO
53 |
54 |
55 | `) 56 | } 57 | } 58 | 59 | const messages = { 60 | activate: function () { 61 | this.view = $(` 62 |
63 |

Messages

64 |

You have no direct messages

65 |
66 | `) 67 | } 68 | } 69 | 70 | const profile = { 71 | activate: function () { 72 | this.view = $(` 73 |
74 |
75 |
76 | `) 77 | } 78 | } 79 | 80 | const profileIndex = { 81 | activate: function (params) { 82 | this.view = $(` 83 |
84 |

${params.user} profile

85 |
86 | `) 87 | } 88 | } 89 | 90 | // provide your route map 91 | // in this particular case we configure handlers by attaching 92 | // them to routes via options. This is one of several ways you 93 | // could choose to handle transitions in your app. 94 | // * you can attach handlers to the route options like here 95 | // * you could get the route handlers of some map somewhere by name 96 | // * you can have a dynamic require that pulls in the route from a file by name 97 | router.map((route) => { 98 | route('application', { path: '/', handler: application, abstract: true }, () => { 99 | route('home', { path: '', handler: home }) 100 | route('messages', { handler: messages }) 101 | route('status', { path: ':user/status/:id' }) 102 | route('profile', { path: ':user', handler: profile, abstract: true }, () => { 103 | route('profile.index', { path: '', handler: profileIndex }) 104 | route('profile.lists') 105 | route('profile.edit') 106 | }) 107 | }) 108 | }) 109 | 110 | // install middleware that will handle transitions 111 | router.use(function activate (transition) { 112 | transition.routes.forEach((route, i) => { 113 | const handler = route.options.handler 114 | router.log(`Transition #${transition.id} activating '${route.name}'`) 115 | handler.activate(transition.params) 116 | if (handler.view) { 117 | const parent = transition.routes[i - 1] 118 | const $container = parent ? parent.options.handler.view.find('.Container') : $(document.body) 119 | $container.html(handler.view) 120 | } 121 | }) 122 | }) 123 | 124 | // start listening to browser's location bar changes 125 | router.listen() 126 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # Usage guide 2 | 3 | When your application starts, the router is responsible for loading data, rendering views and otherwise setting up application state. It does so by translating every URL change to a transition object and a list of matching routes. You then need to apply a middleware function to translate the transition data into the desired state of your application. 4 | 5 | First create an instance of the router. 6 | 7 | ```js 8 | import { Router } from 'slick-router/core.js'; 9 | 10 | const router = new Router({ 11 | pushState: true 12 | }); 13 | ``` 14 | 15 | Then use the `map` method to declare the route map. 16 | 17 | ```js 18 | router.map(function (route) { 19 | route('application', { path: '/', abstract: true, handler: App }, function () { 20 | route('index', { path: '', handler: Index }) 21 | route('about', { handler: About }) 22 | route('favorites', { path: 'favs', handler: Favorites }) 23 | route('message', { path: 'message/:id', handler: Message }) 24 | }) 25 | }); 26 | ``` 27 | 28 | Next, install middleware. 29 | 30 | ```js 31 | router.use(function activate (transition) { 32 | transition.routes.forEach(function (route) { 33 | route.options.handler.activate(transition.params, transition.query) 34 | }) 35 | }) 36 | ``` 37 | 38 | Now, when the user enters `/about` page, Slick Router will call the middleware with the transition object and `transition.routes` will be the route descriptors of `application` and `about` routes. 39 | 40 | Note that you can leave off the path if you want to use the route name as the path. For example, these are equivalent 41 | 42 | ```js 43 | router.map(function(route) { 44 | route('about'); 45 | }); 46 | 47 | // or 48 | 49 | router.map(function(route) { 50 | route('about', {path: 'about'}); 51 | }); 52 | ``` 53 | 54 | To generate links to the different routes use `generate` and pass the name of the route: 55 | 56 | ```js 57 | router.generate('favorites') 58 | // => /favs 59 | router.generate('index'); 60 | // => / 61 | router.generate('messages', {id: 24}); 62 | ``` 63 | 64 | If you disable pushState (`pushState: false`), the generated links will start with `#`. 65 | 66 | ### Route params 67 | 68 | Routes can have dynamic urls by specifying patterns in the `path` option. For example: 69 | 70 | ```js 71 | router.map(function(route) { 72 | route('posts'); 73 | route('post', { path: '/post/:postId' }); 74 | }); 75 | 76 | router.use(function (transition) { 77 | console.log(transition.params) 78 | // => {postId: 5} 79 | }); 80 | 81 | router.transitionTo('/post/5') 82 | ``` 83 | 84 | See what other types of dynamic routes is supported in the [api docs](api.md#dynamic-paths). 85 | 86 | ### Route Nesting 87 | 88 | Route nesting is one of the core features of slick-router. It's useful to nest routes when you want to configure each route to perform a different role in rendering the page - e.g. the root `application` route can do some initial data loading/initialization, but you can avoid redoing that work on subsequent transitions by checking if the route is already in the middleware. The nested routes can then load data specific for a given page. Nesting routes is also very useful for rendering nested UIs, e.g. if you're building an email application, you might have the following route map 89 | 90 | ```js 91 | router.map(function(route) { 92 | route('gmail', {path: '/', abstract: true}, function () { 93 | route('inbox', {path: ''}, function () { 94 | route('email', {path: 'm/:emailId'}, function () { 95 | route('email.raw') 96 | }) 97 | }) 98 | }) 99 | }) 100 | ``` 101 | 102 | This router creates the following routes: 103 | 104 |
105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
URLRoute NamePurpose
N/AgmailCan't route to it, it's an abstract route.
/inboxLoad 1 page of emails and render it.
/m/:emailId/emailLoad the email contents of email with id `transition.params.emailId` and expand it in the list of emails while keeping the email list rendered.
/m/:mailId/rawemail.rawRender the raw textual version of the email in an expanded pane.
134 |
135 | 136 | ## Examples 137 | 138 | I hope you found this brief guide useful, check out some example apps next in the [examples](../examples) dir. 139 | -------------------------------------------------------------------------------- /docs/middlewares/routerlinks.md: -------------------------------------------------------------------------------- 1 | # routerLinks middleware 2 | 3 | Automatically setup route links and monitor the router transitions. When the corresponding route 4 | is active, the 'active' class will be added to the element 5 | 6 | ## Usage 7 | 8 | Wrap router links elements with `router-links` web component 9 | 10 | ```javascript 11 | import 'slick-router/components/router-links' 12 | 13 | class MyView extends LitElement { 14 | render() { 15 | return html` 16 | 17 | Home 18 | About 19 | 20 | ` 21 | } 22 | } 23 | ``` 24 | 25 | Manually bind a dom element: 26 | 27 | ```js 28 | import { bindRouterLinks } from 'slick-router/middlewares/router-links' 29 | 30 | const navEl = document.getElementById('main-nav') 31 | const unbind = bindRouterLinks(navEl) 32 | 33 | // call unbind when (if) navEl element is removed from DOM 34 | ``` 35 | 36 | ## Options 37 | 38 | Both `router-links` and `bindRouterLinks` can be configured 39 | 40 | 41 | ### `query` and `params` 42 | 43 | Returns default values to `query` or `params` 44 | 45 | It can be defined as a hash or a function that returns a hash. 46 | 47 | The function is called with the onwner element as `this` and route name and link element as arguments 48 | 49 | 50 | ```javascript 51 | const routeParams = { 52 | id: 3 53 | } 54 | 55 | const routeQuery = { 56 | foo: 'bar' 57 | } 58 | 59 | 60 | const unbind = bindRouterLinks(navEl, { params: routeParams, query: routeQuery }) 61 | 62 | // or 63 | class MyView extends LitElement { 64 | render() { 65 | return html` 66 | 67 | Home 68 | About 69 | 70 | ` 71 | } 72 | } 73 | ``` 74 | 75 | 76 | ```javascript 77 | function getRouteParams(route, el) { 78 | if (route === 'home') return { id: this.rootId } 79 | } 80 | 81 | function getRouteQuery(route, el) { 82 | if (route === 'child') return { foo: 'bar' } 83 | if (el.id === 'my-link') return { tag: el.tagName } 84 | } 85 | 86 | const unbind = bindRouterLinks(navEl, { params: getRouteParams, query: getRouteQuery }) 87 | 88 | // or 89 | class MyView extends LitElement { 90 | render() { 91 | return html` 92 | 93 | Home 94 | About 95 | 96 | ` 97 | } 98 | } 99 | ``` 100 | 101 | 102 | ## Markup 103 | 104 | The router links are configured with HTML attributes 105 | 106 | They must be child, direct or not, of an element with 'routerlinks' attribute or the one defined in [selector option](#selector). 107 | 108 | ### route 109 | 110 | Defines the route be transitioned to. Should be the name of a route configured in the router map. 111 | When the element is an anchor (a), its href will be expanded to the route path. 112 | 113 | Adding a route attribute to a non anchor element will setup a click event handler that calls `router.transitionTo` 114 | with the appropriate arguments. The exception is when the element has an anchor child. In this case the anchor href 115 | will be expanded. 116 | 117 | ### param-* 118 | 119 | Defines a param value where the param name is the substring after `param-` prefix 120 | 121 | ### query-* 122 | 123 | Defines a query value where the query name is the substring after `query-` prefix 124 | 125 | ### active-class 126 | 127 | Defines the class to be toggled in the element with route attribute according to the route active state. By default is 'active'. 128 | If is set to an empty string, no class is added. 129 | 130 | ### exact 131 | 132 | When present the active class will be set only the route path matches exactly with the one being transitioned. 133 | 134 | ### replace 135 | 136 | When present it will use `replaceWith` instead of `transitionTo` thus not adding a history entry 137 | 138 | Example: 139 | 140 | ```html 141 | 165 | ``` -------------------------------------------------------------------------------- /tests/unit/functionDslTest.js: -------------------------------------------------------------------------------- 1 | import functionDsl from '../../lib/function-dsl' 2 | import 'chai/chai.js' 3 | 4 | const { assert } = window.chai 5 | const { describe, it } = window 6 | 7 | describe('function-dsl', () => { 8 | it('simple route map', () => { 9 | const routes = functionDsl((route) => { 10 | route('application') 11 | }) 12 | assert.deepEqual(routes, [ 13 | { 14 | name: 'application', 15 | path: 'application', 16 | options: { path: 'application' }, 17 | routes: [], 18 | }, 19 | ]) 20 | }) 21 | 22 | it('simple route map with options', () => { 23 | const routes = functionDsl((route) => { 24 | route('application', { path: '/', foo: 'bar' }) 25 | }) 26 | assert.deepEqual(routes, [ 27 | { 28 | name: 'application', 29 | path: '/', 30 | options: { foo: 'bar', path: '/' }, 31 | routes: [], 32 | }, 33 | ]) 34 | }) 35 | 36 | it('simple nested route map', () => { 37 | const routes = functionDsl((route) => { 38 | route('application', () => { 39 | route('child') 40 | }) 41 | }) 42 | assert.deepEqual(routes, [ 43 | { 44 | name: 'application', 45 | path: 'application', 46 | options: { path: 'application' }, 47 | routes: [ 48 | { 49 | name: 'child', 50 | path: 'child', 51 | options: { path: 'child' }, 52 | routes: [], 53 | }, 54 | ], 55 | }, 56 | ]) 57 | }) 58 | 59 | it('route with dot names and no path', () => { 60 | const routes = functionDsl((route) => { 61 | route('application', () => { 62 | route('application.child') 63 | }) 64 | }) 65 | assert.deepEqual(routes, [ 66 | { 67 | name: 'application', 68 | path: 'application', 69 | options: { path: 'application' }, 70 | routes: [ 71 | { 72 | name: 'application.child', 73 | path: 'child', 74 | options: { path: 'child' }, 75 | routes: [], 76 | }, 77 | ], 78 | }, 79 | ]) 80 | }) 81 | 82 | it('complex example', () => { 83 | const routes = functionDsl((route) => { 84 | route('application', { abstract: true }, () => { 85 | route('notifications') 86 | route('messages', () => { 87 | route('unread', () => { 88 | route('priority') 89 | }) 90 | route('read') 91 | route('draft', { abstract: true }, () => { 92 | route('recent') 93 | }) 94 | }) 95 | route('status', { path: ':user/status/:id' }) 96 | }) 97 | route('anotherTopLevel', () => { 98 | route('withChildren') 99 | }) 100 | }) 101 | assert.deepEqual(routes, [ 102 | { 103 | name: 'application', 104 | path: 'application', 105 | options: { path: 'application', abstract: true }, 106 | routes: [ 107 | { 108 | name: 'notifications', 109 | path: 'notifications', 110 | options: { path: 'notifications' }, 111 | routes: [], 112 | }, 113 | { 114 | name: 'messages', 115 | path: 'messages', 116 | options: { path: 'messages' }, 117 | routes: [ 118 | { 119 | name: 'unread', 120 | path: 'unread', 121 | options: { path: 'unread' }, 122 | routes: [ 123 | { 124 | name: 'priority', 125 | path: 'priority', 126 | options: { path: 'priority' }, 127 | routes: [], 128 | }, 129 | ], 130 | }, 131 | { 132 | name: 'read', 133 | path: 'read', 134 | options: { path: 'read' }, 135 | routes: [], 136 | }, 137 | { 138 | name: 'draft', 139 | path: 'draft', 140 | options: { path: 'draft', abstract: true }, 141 | routes: [ 142 | { 143 | name: 'recent', 144 | path: 'recent', 145 | options: { path: 'recent' }, 146 | routes: [], 147 | }, 148 | ], 149 | }, 150 | ], 151 | }, 152 | { 153 | name: 'status', 154 | path: ':user/status/:id', 155 | options: { path: ':user/status/:id' }, 156 | routes: [], 157 | }, 158 | ], 159 | }, 160 | { 161 | name: 'anotherTopLevel', 162 | path: 'anotherTopLevel', 163 | options: { path: 'anotherTopLevel' }, 164 | routes: [ 165 | { 166 | name: 'withChildren', 167 | path: 'withChildren', 168 | options: { path: 'withChildren' }, 169 | routes: [], 170 | }, 171 | ], 172 | }, 173 | ]) 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /tests/location-bar/location_bar_test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var _ = window.underscore; 4 | var $ = window.nonGlobaljQuery; 5 | var LocationBar = window.LocationBar; 6 | var location; 7 | var locationBar, locationBar1, locationBar2; 8 | 9 | // QUnit.config.filter = "routes (simple)"; 10 | 11 | var Location = function(href) { 12 | this.replace(href); 13 | }; 14 | 15 | _.extend(Location.prototype, { 16 | 17 | replace: function(href) { 18 | _.extend(this, _.pick($('', {href: href})[0], 19 | 'href', 20 | 'hash', 21 | 'host', 22 | 'search', 23 | 'fragment', 24 | 'pathname', 25 | 'protocol' 26 | )); 27 | // In IE, anchor.pathname does not contain a leading slash though 28 | // window.location.pathname does. 29 | if (!/^\//.test(this.pathname)) this.pathname = '/' + this.pathname; 30 | }, 31 | 32 | toString: function() { 33 | return this.href; 34 | } 35 | 36 | }); 37 | 38 | module("location-bar", { 39 | 40 | setup: function() { 41 | location = new Location('http://example.com'); 42 | window.location.hash = "#setup"; 43 | locationBar = new LocationBar(); 44 | locationBar.interval = 9; 45 | locationBar.start({pushState: false}); 46 | }, 47 | 48 | teardown: function() { 49 | locationBar.stop(); 50 | } 51 | 52 | }); 53 | 54 | asyncTest("route", 1, function() { 55 | locationBar.route(/^(.*?)$/, function (path) { 56 | equal(path, 'search/news'); 57 | start(); 58 | }); 59 | window.location.hash = "search/news"; 60 | }); 61 | 62 | asyncTest("onChange", 1, function () { 63 | locationBar.onChange(function (path) { 64 | equal(path, "some/url?withParam=1&moreParams=2"); 65 | start(); 66 | }); 67 | window.location.hash = "some/url?withParam=1&moreParams=2"; 68 | }); 69 | 70 | asyncTest("routes via update", 2, function() { 71 | locationBar.onChange(function (path) { 72 | equal(path, "search/manhattan/p20"); 73 | start(); 74 | }); 75 | locationBar.update("search/manhattan/p20", {trigger: true}); 76 | equal(window.location.hash, "#search/manhattan/p20"); 77 | }); 78 | 79 | test("routes via update with {replace: true}", 1, function() { 80 | location.replace('http://example.com#start_here'); 81 | locationBar.location = location; 82 | locationBar.checkUrl(); 83 | location.replace = function(href) { 84 | strictEqual(href, new Location('http://example.com#end_here').href); 85 | }; 86 | locationBar.update('end_here', {replace: true}); 87 | }); 88 | 89 | asyncTest("routes via update with query params", 2, function() { 90 | locationBar.onChange(function (path) { 91 | equal(path, "search/manhattan/p20?id=1&foo=bar"); 92 | start(); 93 | }); 94 | locationBar.update("search/manhattan/p20?id=1&foo=bar", {trigger: true}); 95 | equal(window.location.hash, "#search/manhattan/p20?id=1&foo=bar"); 96 | }); 97 | 98 | module("multiple instances of location-bar", { 99 | 100 | setup: function() { 101 | location = new Location('http://example.com'); 102 | window.location.hash = "#setup"; 103 | }, 104 | 105 | teardown: function() { 106 | locationBar1.stop(); 107 | locationBar2.stop(); 108 | } 109 | 110 | }); 111 | 112 | asyncTest("can all listen in", 3, function () { 113 | var count = 0; 114 | 115 | locationBar1 = new LocationBar(); 116 | locationBar1.route(/^search\/.*$/, function (path) { 117 | if (count < 2) { 118 | equal(path, 'search/news'); 119 | proceed(); 120 | } else { 121 | equal(path, 'search/food') 122 | start(); 123 | } 124 | }); 125 | locationBar1.start({pushState: false}); 126 | 127 | // navigate first 128 | window.location.hash = "search/news"; 129 | 130 | // and then add another location bar, which when 131 | // started should match immediately 132 | locationBar2 = new LocationBar(); 133 | locationBar2.route(/^search\/news.*$/, function (path) { 134 | equal(path, 'search/news'); 135 | proceed(); 136 | }); 137 | locationBar2.start(); 138 | 139 | // second part of the test where we navigate for the second time 140 | function proceed() { 141 | if (++count < 2) return; 142 | window.location.hash = "search/food"; 143 | } 144 | }); 145 | 146 | asyncTest("can all update", 3, function () { 147 | var count = 0; 148 | locationBar1 = new LocationBar(); 149 | locationBar1.route(/^search\/.*$/, function (path) { 150 | if (count < 2) { 151 | equal(path, 'search/news'); 152 | proceed(); 153 | } else { 154 | equal(path, 'search/food') 155 | start(); 156 | } 157 | }); 158 | locationBar1.start({pushState: false}); 159 | 160 | locationBar2 = new LocationBar(); 161 | locationBar2.route(/^search\/news.*$/, function (path) { 162 | equal(path, 'search/news'); 163 | proceed(); 164 | }); 165 | locationBar2.start(); 166 | 167 | locationBar1.update('search/news', {trigger: true}); 168 | 169 | function proceed() { 170 | if (++count < 2) return; 171 | locationBar1.update('search/food', {trigger: true}); 172 | } 173 | }); 174 | 175 | })(); 176 | -------------------------------------------------------------------------------- /tests/location-bar/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /tests/unit/arrayDslTest.js: -------------------------------------------------------------------------------- 1 | import dsl from '../../lib/array-dsl' 2 | import 'chai/chai.js' 3 | 4 | const { assert } = window.chai 5 | const { describe, it } = window 6 | 7 | describe('array-dsl', () => { 8 | it('simple route map', () => { 9 | const routes = dsl([{ name: 'application' }]) 10 | assert.deepEqual(routes, [ 11 | { 12 | name: 'application', 13 | path: 'application', 14 | options: { path: 'application' }, 15 | routes: [], 16 | }, 17 | ]) 18 | }) 19 | 20 | it('simple route map with options', () => { 21 | const routes = dsl([ 22 | { 23 | name: 'application', 24 | path: '/', 25 | foo: 'bar', 26 | }, 27 | ]) 28 | 29 | assert.deepEqual(routes, [ 30 | { 31 | name: 'application', 32 | path: '/', 33 | options: { foo: 'bar', path: '/' }, 34 | routes: [], 35 | }, 36 | ]) 37 | }) 38 | 39 | it('simple nested route map', () => { 40 | const routes = dsl([ 41 | { 42 | name: 'application', 43 | children: [ 44 | { 45 | name: 'child', 46 | }, 47 | ], 48 | }, 49 | ]) 50 | 51 | assert.deepEqual(routes, [ 52 | { 53 | name: 'application', 54 | path: 'application', 55 | options: { path: 'application' }, 56 | routes: [ 57 | { 58 | name: 'child', 59 | path: 'child', 60 | options: { path: 'child' }, 61 | routes: [], 62 | }, 63 | ], 64 | }, 65 | ]) 66 | }) 67 | 68 | it('route with dot names and no path', () => { 69 | const routes = dsl([ 70 | { 71 | name: 'application', 72 | children: [ 73 | { 74 | name: 'application.child', 75 | }, 76 | ], 77 | }, 78 | ]) 79 | 80 | assert.deepEqual(routes, [ 81 | { 82 | name: 'application', 83 | path: 'application', 84 | options: { path: 'application' }, 85 | routes: [ 86 | { 87 | name: 'application.child', 88 | path: 'child', 89 | options: { path: 'child' }, 90 | routes: [], 91 | }, 92 | ], 93 | }, 94 | ]) 95 | }) 96 | 97 | it('complex example', () => { 98 | const routes = dsl([ 99 | { 100 | name: 'application', 101 | abstract: true, 102 | children: [ 103 | { 104 | name: 'notifications', 105 | }, 106 | { 107 | name: 'messages', 108 | children: [ 109 | { 110 | name: 'unread', 111 | children: [ 112 | { 113 | name: 'priority', 114 | }, 115 | ], 116 | }, 117 | { 118 | name: 'read', 119 | }, 120 | { 121 | name: 'draft', 122 | abstract: true, 123 | children: [ 124 | { 125 | name: 'recent', 126 | }, 127 | ], 128 | }, 129 | ], 130 | }, 131 | { 132 | name: 'status', 133 | path: ':user/status/:id', 134 | }, 135 | ], 136 | }, 137 | { 138 | name: 'anotherTopLevel', 139 | children: [ 140 | { 141 | name: 'withChildren', 142 | }, 143 | ], 144 | }, 145 | ]) 146 | 147 | assert.deepEqual(routes, [ 148 | { 149 | name: 'application', 150 | path: 'application', 151 | options: { path: 'application', abstract: true }, 152 | routes: [ 153 | { 154 | name: 'notifications', 155 | path: 'notifications', 156 | options: { path: 'notifications' }, 157 | routes: [], 158 | }, 159 | { 160 | name: 'messages', 161 | path: 'messages', 162 | options: { path: 'messages' }, 163 | routes: [ 164 | { 165 | name: 'unread', 166 | path: 'unread', 167 | options: { path: 'unread' }, 168 | routes: [ 169 | { 170 | name: 'priority', 171 | path: 'priority', 172 | options: { path: 'priority' }, 173 | routes: [], 174 | }, 175 | ], 176 | }, 177 | { 178 | name: 'read', 179 | path: 'read', 180 | options: { path: 'read' }, 181 | routes: [], 182 | }, 183 | { 184 | name: 'draft', 185 | path: 'draft', 186 | options: { path: 'draft', abstract: true }, 187 | routes: [ 188 | { 189 | name: 'recent', 190 | path: 'recent', 191 | options: { path: 'recent' }, 192 | routes: [], 193 | }, 194 | ], 195 | }, 196 | ], 197 | }, 198 | { 199 | name: 'status', 200 | path: ':user/status/:id', 201 | options: { path: ':user/status/:id' }, 202 | routes: [], 203 | }, 204 | ], 205 | }, 206 | { 207 | name: 'anotherTopLevel', 208 | path: 'anotherTopLevel', 209 | options: { path: 'anotherTopLevel' }, 210 | routes: [ 211 | { 212 | name: 'withChildren', 213 | path: 'withChildren', 214 | options: { path: 'withChildren' }, 215 | routes: [], 216 | }, 217 | ], 218 | }, 219 | ]) 220 | }) 221 | }) 222 | -------------------------------------------------------------------------------- /types/router.d.ts: -------------------------------------------------------------------------------- 1 | export type routeCallback = import('./function-dsl.js').routeCallback; 2 | export type RouteDef = import('./array-dsl.js').RouteDef; 3 | export type Transition = import('./transition.js').Transition; 4 | export type Route = { 5 | name: string; 6 | path: string; 7 | options: any; 8 | routes: Route[]; 9 | }; 10 | export type LocationParam = any | 'browser' | 'memory'; 11 | export type RouterOptions = { 12 | routes?: routeCallback | RouteDef[]; 13 | location?: LocationParam; 14 | logError?: boolean; 15 | qs?: any; 16 | patternCompiler?: any; 17 | }; 18 | /** 19 | * @typedef {import('./function-dsl.js').routeCallback} routeCallback 20 | * @typedef {import('./array-dsl.js').RouteDef} RouteDef 21 | * @typedef {import('./transition.js').Transition} Transition 22 | * 23 | * @typedef Route 24 | * @property {String} name 25 | * @property {String} path 26 | * @property {Object} options 27 | * @property {Route[]} routes 28 | * 29 | 30 | * @typedef {Object | 'browser' | 'memory'} LocationParam 31 | * 32 | * 33 | * @typedef RouterOptions 34 | * @property {routeCallback | RouteDef[]} [routes] 35 | * @property {LocationParam} [location] 36 | * @property {Boolean} [logError] 37 | * @property {Object} [qs] 38 | * @property {Object} [patternCompiler] 39 | * 40 | */ 41 | export class Router { 42 | /** 43 | * @param {RouterOptions} [options] 44 | */ 45 | constructor(options?: RouterOptions); 46 | nextId: number; 47 | state: {}; 48 | middleware: any[]; 49 | options: { 50 | location: string; 51 | logError: boolean; 52 | qs: { 53 | parse(querystring: any): any; 54 | stringify(params: any): string; 55 | }; 56 | patternCompiler: typeof patternCompiler; 57 | } & RouterOptions; 58 | /** 59 | * Add a middleware 60 | * @param {Function} middleware 61 | * @return {Router} 62 | * @api public 63 | */ 64 | use(middleware: Function, options?: {}): Router; 65 | /** 66 | * Add the route map 67 | * @param {routeCallback | RouteDef[]} routes 68 | * @return {Router} 69 | * @api public 70 | */ 71 | map(routes: routeCallback | RouteDef[]): Router; 72 | routes: Route[]; 73 | matchers: any[]; 74 | /** 75 | * Starts listening to the location changes. 76 | * @param {String} [path] 77 | * @return {Transition} initial transition 78 | * 79 | * @api public 80 | */ 81 | listen(path?: string): Transition; 82 | location: any; 83 | /** 84 | * Transition to a different route. Passe in url or a route name followed by params and query 85 | * @param {String} name url or route name 86 | * @param {Object} [params] Optional 87 | * @param {Object} [query] Optional 88 | * @return {Transition} transition 89 | * 90 | * @api public 91 | */ 92 | transitionTo(name: string, params?: any, query?: any): Transition; 93 | /** 94 | * Like transitionTo, but doesn't leave an entry in the browser's history, 95 | * so clicking back will skip this route 96 | * @param {String} name url or route name followed by params and query 97 | * @param {Record} [params] 98 | * @param {Record} [query] 99 | * @return {Transition} transition 100 | * 101 | * @api public 102 | */ 103 | replaceWith(name: string, params?: Record, query?: Record): Transition; 104 | /** 105 | * Create an href 106 | * @param {String} name target route name 107 | * @param {Object} [params] 108 | * @param {Object} [query] 109 | * @return {String} href 110 | * 111 | * @api public 112 | */ 113 | generate(name: string, params?: any, query?: any): string; 114 | /** 115 | * Stop listening to URL changes 116 | * @api public 117 | */ 118 | destroy(): void; 119 | /** 120 | * Check if the given route/params/query combo is active 121 | * @param {String} name target route name 122 | * @param {Record} [params] 123 | * @param {Record} [query] 124 | * @param {Boolean} [exact] 125 | * @return {Boolean} 126 | * 127 | * @api public 128 | */ 129 | isActive(name: string, params?: Record, query?: Record, exact?: boolean): boolean; 130 | /** 131 | * @api private 132 | * @param {String} method pushState or replaceState 133 | * @param {String} name target route name 134 | * @param {Object} [params] 135 | * @param {Object} [query] 136 | * @return {Transition} transition 137 | */ 138 | doTransition(method: string, name: string, params?: any, query?: any): Transition; 139 | /** 140 | * Match the path against the routes 141 | * @param {String} path 142 | * @return {Object} the list of matching routes and params 143 | * 144 | * @api private 145 | */ 146 | match(path: string): any; 147 | /** 148 | * 149 | * @param {string} path 150 | * @returns {Transition} 151 | */ 152 | dispatch(path: string): Transition; 153 | /** 154 | * Create the default location. 155 | * This is used when no custom location is passed to 156 | * the listen call. 157 | * @param {LocationParam} path 158 | * @return {Object} location 159 | * 160 | * @api private 161 | */ 162 | createLocation(path: LocationParam): any; 163 | log(...args: any[]): void; 164 | logError(...args: any[]): void; 165 | } 166 | /** 167 | * @description Helper to intercept links when using pushState but server is not configured for it 168 | * Link clicks are handled via the router avoiding browser page reload 169 | * @param {Router} router 170 | * @param {HTMLElement} el 171 | * @param {(e: Event, link: HTMLAnchorElement, router: Router) => void} clickHandler 172 | * @returns {Function} dispose 173 | */ 174 | export function interceptLinks(router: Router, el?: HTMLElement, clickHandler?: (e: Event, link: HTMLAnchorElement, router: Router) => void): Function; 175 | import { patternCompiler } from './patternCompiler.js'; 176 | //# sourceMappingURL=router.d.ts.map -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slick Router 2 | 3 | Slick Router is a powerful, flexible router that translates URL changes into route transitions allowing to put the application into a desired state. It is derived from [cherrytree](https://github.com/QubitProducts/cherrytree) library (see [differences](docs/versions-differences.md)). 4 | 5 | ## Features 6 | 7 | - Out of the box support for Web Components: 8 | - Streamlined support for code spliting and lazy loading 9 | - Expose route state (query, params) to components 10 | - Property hooks to map global state to component props 11 | - Declarative handling of router links 12 | - Can nest routes allowing to create nested UI and/or state 13 | - Route transition is a first class citizen - abort, pause, resume, retry 14 | - Generate links in a systematic way, e.g. `router.generate('commit', {sha: '1e2760'})` 15 | - Use pushState or hashchange for URL change detection 16 | - Define path dynamic segments 17 | - Trigger router navigate programatically 18 | - With builtin middlewares/components: 19 | - components/animated-outlet: enable animation on route transitions 20 | - components/router-links: handle route related links state 21 | - middlewares/wc: advanced Web Component rendering and lifecycle (included in default export) 22 | - middlewares/router-links: handle route related links state (included in default export) 23 | - middlewares/events: fires route events on window 24 | 25 | ## Installation 26 | 27 | The default export (including web component support and routerLinks) is 17kb. The core Router class is ~12kB minified (without gzip compression). AnimatedOutlet web component, which can be used independent from the router, has a 2.5kb size. 28 | See [webpack test project](examples/tree-shaking) for more results. 29 | 30 | $ npm install --save slick-router 31 | 32 | ## Docs 33 | 34 | - [Intro Guide](docs/intro.md) 35 | - [Router Configuration](docs/router-configuration.md) 36 | - [Programmatic Navigation and Link Handling](docs/programmatic-navigation-and-link.md) 37 | - [Route Transition](docs/route-transition.md) 38 | - [Common Situations](docs/common-situations.md) 39 | - [Changelog](CHANGELOG.md) 40 | 41 | ## Builtin middlewares 42 | 43 | - [wc](docs/middlewares/wc.md) (advanced Web Component rendering and lifecycle) 44 | - [routerLinks](docs/middlewares/routerlinks.md) (handle route related links state) 45 | - [events](docs/middlewares/events.md) (fires route events on window) 46 | 47 | ## Builtin components 48 | 49 | - [animated-outlet](docs/components/animated-outlet.md) (enable animation on route transitions) 50 | - [router-links](docs/middlewares/routerlinks.md) (handle route related links state) 51 | 52 | ## Usage 53 | 54 | ### With Web Components 55 | 56 | The default Router class comes with Web Components and router links support. 57 | 58 | ```js 59 | import { Router } from 'slick-router' 60 | 61 | function checkAuth(transition) { 62 | if (!!localStorage.getItem('token')) { 63 | transition.redirectTo('login') 64 | } 65 | } 66 | 67 | // route tree definition 68 | const routes = function (route) { 69 | route('application', { path: '/', component: 'my-app' }, function () { 70 | route('feed', { path: '' }) 71 | route('messages') 72 | route('status', { path: ':user/status/:id' }) 73 | route('profile', { path: ':user', beforeEnter: checkAuth }, function () { 74 | route('profile.lists', { component: 'profile-lists' }) 75 | route('profile.edit', { component: 'profile-edit' }) 76 | }) 77 | }) 78 | } 79 | 80 | // create the router 81 | var router = new Router({ 82 | routes, 83 | }) 84 | 85 | // start listening to URL changes 86 | router.listen() 87 | ``` 88 | 89 | ### Custom middlewares 90 | 91 | Is possible to create a router with customized behavior by using the core Router with middlewares. 92 | 93 | Check how to create middlewares in the [Router Configuration Guide](docs/router-configuration.md). 94 | 95 | ```js 96 | import { Router } from 'slick-router/core.js' 97 | 98 | // create a router similar to page.js - https://github.com/visionmedia/page.js 99 | 100 | const user = { 101 | list() {}, 102 | async load() {}, 103 | show() {}, 104 | edit() {}, 105 | } 106 | 107 | const routes = [ 108 | { 109 | name: 'users', 110 | path: '/', 111 | handler: user.list, 112 | }, 113 | { 114 | name: 'user.show', 115 | path: '/user/:id/edit', 116 | handler: [user.load, user.show], 117 | }, 118 | , 119 | { 120 | name: 'user.edit', 121 | path: '/user/:id/edit', 122 | handler: [user.load, user.edit], 123 | }, 124 | ] 125 | 126 | const router = new Router({ routes }) 127 | 128 | function normalizeHandlers(handlers) { 129 | return Array.isArray(handlers) ? handlers : [handlers] 130 | } 131 | 132 | router.use(async function (transition) { 133 | for (const route of transition.routes) { 134 | const handlers = normalizeHandlers(route.options.handler) 135 | for (const handler of handlers) { 136 | await handler(transition) 137 | } 138 | } 139 | }) 140 | 141 | // protect private routes 142 | router.use(function privateHandler(transition) { 143 | if (transition.routes.some((route) => route.options.private)) { 144 | if (!userLogged()) { 145 | transition.cancel() 146 | } 147 | } 148 | }) 149 | 150 | // for error logging attach a catch handler to transition promise... 151 | router.use(function errorHandler(transition) { 152 | transition.catch(function (err) { 153 | if (err.type !== 'TransitionCancelled' && err.type !== 'TransitionRedirected') { 154 | console.error(err.stack) 155 | } 156 | }) 157 | }) 158 | 159 | // ...or use the error hook 160 | router.use({ 161 | error: function (transition, err) { 162 | if (err.type !== 'TransitionCancelled' && err.type !== 'TransitionRedirected') { 163 | console.error(err.stack) 164 | } 165 | }, 166 | }) 167 | 168 | // start listening to URL changes 169 | router.listen() 170 | ``` 171 | 172 | ## Examples 173 | 174 | - [lit-element-mobx-realworld](https://github.com/blikblum/lit-element-mobx-realworld-example-app) A complete app that uses router advanced features. [Live demo](https://blikblum.github.io/lit-element-mobx-realworld-example-app) 175 | 176 | You can also clone this repo and run the `examples` locally: 177 | 178 | - [hello-world-jquery](examples/hello-world-jquery) - minimal example with good old jquery 179 | - [hello-world-wc](examples/hello-world-wc) - minimal example with Web Component (no build required) 180 | - [vanilla-blog](examples/vanilla-blog) - a small static demo of blog like app that uses no framework 181 | 182 | ## Browser Support 183 | 184 | Slick Router works in all modern browsers. No IE support. 185 | 186 | --- 187 | 188 | Copyright (c) 2024 Luiz Américo Pereira Câmara 189 | 190 | Copyright (c) 2017 Karolis Narkevicius 191 | -------------------------------------------------------------------------------- /lib/transition.js: -------------------------------------------------------------------------------- 1 | import { clone } from './utils.js' 2 | import invariant from './invariant.js' 3 | import { TRANSITION_CANCELLED, TRANSITION_REDIRECTED } from './constants.js' 4 | 5 | /** 6 | * @typedef {import("./router.js").Route} Route 7 | */ 8 | 9 | /** 10 | * @typedef {Pick} TransitionData 11 | */ 12 | 13 | /** 14 | * @typedef Transition 15 | * @property {Route[]} routes 16 | * @property {string} pathname 17 | * @property {string} path 18 | * @property {Object} params 19 | * @property {Object} query 20 | * @property {TransitionData} prev 21 | * @property {(name: string, params?: any, query?: any) => Transition} redirectTo 22 | * @property {(name: string, params?: any, query?: any) => Transition} retry 23 | * @property {(error: string | Error) => void} cancel 24 | * @property {() => Promise} followRedirects 25 | * @property {function} then 26 | * @property {function} catch 27 | * @property {boolean} noop 28 | * @property {boolean} isCancelled 29 | */ 30 | 31 | /** 32 | * @param {*} router 33 | * @param {Transition} transition 34 | * @param {*} err 35 | */ 36 | function runError(router, transition, err) { 37 | router.middleware.forEach((m) => { 38 | m.error && m.error(transition, err) 39 | }) 40 | } 41 | 42 | /** 43 | * @export 44 | * @param {*} options 45 | * @return {Transition} 46 | */ 47 | export default function transition(options) { 48 | options = options || {} 49 | 50 | const router = options.router 51 | const log = router.log 52 | const logError = router.logError 53 | 54 | const path = options.path 55 | const match = options.match 56 | const routes = match.routes 57 | const params = match.params 58 | const pathname = match.pathname 59 | const query = match.query 60 | 61 | const id = options.id 62 | const startTime = Date.now() 63 | log('---') 64 | log('Transition #' + id, 'to', path) 65 | log( 66 | 'Transition #' + id, 67 | 'routes:', 68 | routes.map((r) => r.name), 69 | ) 70 | log('Transition #' + id, 'params:', params) 71 | log('Transition #' + id, 'query:', query) 72 | 73 | // create the transition promise 74 | let resolve, reject 75 | const promise = new Promise(function (res, rej) { 76 | resolve = res 77 | reject = rej 78 | }) 79 | 80 | // 1. make transition errors loud 81 | // 2. by adding this handler we make sure 82 | // we don't trigger the default 'Potentially 83 | // unhandled rejection' for cancellations 84 | promise 85 | .then(function () { 86 | log('Transition #' + id, 'completed in', Date.now() - startTime + 'ms') 87 | }) 88 | .catch(function (err) { 89 | if (err.type !== TRANSITION_REDIRECTED && err.type !== TRANSITION_CANCELLED) { 90 | log('Transition #' + id, 'FAILED') 91 | logError(err) 92 | } 93 | }) 94 | 95 | let cancelled = false 96 | 97 | const transition = { 98 | id, 99 | prev: { 100 | routes: clone(router.state.routes) || [], 101 | path: router.state.path || '', 102 | pathname: router.state.pathname || '', 103 | params: clone(router.state.params) || {}, 104 | query: clone(router.state.query) || {}, 105 | }, 106 | routes: clone(routes), 107 | path, 108 | pathname, 109 | params: clone(params), 110 | query: clone(query), 111 | redirectTo: function (...args) { 112 | return router.transitionTo(...args) 113 | }, 114 | retry: function () { 115 | return router.transitionTo(path) 116 | }, 117 | cancel: function (err) { 118 | if (router.state.activeTransition !== transition) { 119 | return 120 | } 121 | 122 | if (transition.isCancelled) { 123 | return 124 | } 125 | 126 | router.state.activeTransition = null 127 | transition.isCancelled = true 128 | cancelled = true 129 | 130 | if (!err) { 131 | err = new Error(TRANSITION_CANCELLED) 132 | err.type = TRANSITION_CANCELLED 133 | } 134 | if (err.type === TRANSITION_CANCELLED) { 135 | log('Transition #' + id, 'cancelled') 136 | } 137 | if (err.type === TRANSITION_REDIRECTED) { 138 | log('Transition #' + id, 'redirected') 139 | } 140 | 141 | router.middleware.forEach((m) => { 142 | m.cancel && m.cancel(transition, err) 143 | }) 144 | reject(err) 145 | }, 146 | followRedirects: function () { 147 | return promise.catch(function (reason) { 148 | if (router.state.activeTransition) { 149 | return router.state.activeTransition.followRedirects() 150 | } 151 | return Promise.reject(reason) 152 | }) 153 | }, 154 | 155 | then: promise.then.bind(promise), 156 | catch: promise.catch.bind(promise), 157 | } 158 | 159 | router.middleware.forEach((m) => { 160 | m.before && m.before(transition) 161 | }) 162 | 163 | // here we handle calls to all of the middlewares 164 | function callNext(i, prevResult) { 165 | let middleware 166 | let middlewareName 167 | // if transition has been cancelled - nothing left to do 168 | if (cancelled) { 169 | return 170 | } 171 | // done 172 | if (i < router.middleware.length) { 173 | middleware = router.middleware[i] 174 | middlewareName = middleware.name || 'anonymous' 175 | log('Transition #' + id, 'resolving middleware:', middlewareName) 176 | let middlewarePromise 177 | try { 178 | middlewarePromise = middleware.resolve 179 | ? middleware.resolve(transition, prevResult) 180 | : prevResult 181 | invariant( 182 | transition !== middlewarePromise, 183 | 'Middleware %s returned a transition which resulted in a deadlock', 184 | middlewareName, 185 | ) 186 | } catch (err) { 187 | router.state.activeTransition = null 188 | runError(router, transition, err) 189 | return reject(err) 190 | } 191 | Promise.resolve(middlewarePromise) 192 | .then(function (result) { 193 | callNext(i + 1, result) 194 | }) 195 | .catch(function (err) { 196 | log('Transition #' + id, 'resolving middleware:', middlewareName, 'FAILED') 197 | router.state.activeTransition = null 198 | runError(router, transition, err) 199 | reject(err) 200 | }) 201 | } else { 202 | router.state = { 203 | activeTransition: null, 204 | routes, 205 | path, 206 | pathname, 207 | params, 208 | query, 209 | } 210 | router.middleware.forEach((m) => { 211 | m.done && m.done(transition) 212 | }) 213 | resolve() 214 | } 215 | } 216 | 217 | if (!options.noop) { 218 | Promise.resolve().then(() => callNext(0)) 219 | } else { 220 | resolve() 221 | } 222 | 223 | if (options.noop) { 224 | transition.noop = true 225 | } 226 | 227 | return transition 228 | } 229 | -------------------------------------------------------------------------------- /lib/middlewares/router-links.js: -------------------------------------------------------------------------------- 1 | const routerLinksData = Symbol('routerLinksData') 2 | const linkContainers = new Set() 3 | let router 4 | 5 | /** 6 | * @callback RoutePropCallback 7 | * @param {String} routeName 8 | * @param {HTMLElement} routeEl 9 | * @return {Object} 10 | * 11 | * @typedef {Object} RouterLinksOptions 12 | * @property {Object | RoutePropCallback} [params] 13 | * @property {Object | RoutePropCallback} [query] 14 | */ 15 | 16 | // Make a event delegation handler for the given `eventName` and `selector` 17 | // and attach it to `el`. 18 | // If selector is empty, the listener will be bound to `el`. If not, a 19 | // new handler that will recursively traverse up the event target's DOM 20 | // hierarchy looking for a node that matches the selector. If one is found, 21 | // the event's `delegateTarget` property is set to it and the return the 22 | // result of calling bound `listener` with the parameters given to the 23 | // handler. 24 | 25 | /** 26 | * @param {HTMLElement} el 27 | * @param {String} eventName 28 | * @param {String} selector 29 | * @param {Function} listener 30 | * @param {*} context 31 | * @return {Function} 32 | */ 33 | const delegate = function (el, eventName, selector, listener, context) { 34 | const handler = function (e) { 35 | let node = e.target 36 | for (; node && node !== el; node = node.parentNode) { 37 | if (node.matches && node.matches(selector)) { 38 | e.selectorTarget = node 39 | listener.call(context, e) 40 | } 41 | } 42 | } 43 | 44 | handler.eventName = eventName 45 | el.addEventListener(eventName, handler, false) 46 | return handler 47 | } 48 | 49 | function isModifiedEvent(event) { 50 | return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) 51 | } 52 | 53 | const undelegate = function (el, handler) { 54 | const eventName = handler.eventName 55 | el.removeEventListener(eventName, handler, false) 56 | } 57 | 58 | const camelize = (str) => { 59 | if (str.indexOf('-') === -1) return str 60 | const words = str.split('-') 61 | let result = '' 62 | for (let i = 0; i < words.length; i++) { 63 | const word = words[i] 64 | result += i ? word.charAt(0).toUpperCase() + word.slice(1) : word 65 | } 66 | return result 67 | } 68 | 69 | function mutationHandler(mutations, observer) { 70 | mutations.forEach(function (mutation) { 71 | if (mutation.type === 'attributes') { 72 | const attr = mutation.attributeName 73 | if (attr.indexOf('param-') === 0 || attr.indexOf('query-') === 0) { 74 | updateLink(mutation.target, observer.rootEl) 75 | } 76 | } else { 77 | mutation.addedNodes.forEach((node) => { 78 | if (node.nodeType === 1) { 79 | if (node.getAttribute('route')) updateLink(node, observer.rootEl) 80 | createLinks(observer.rootEl, node) 81 | } 82 | }) 83 | } 84 | }) 85 | } 86 | 87 | const elementsObserverConfig = { childList: true, subtree: true, attributes: true } 88 | 89 | function getAttributeValues(el, prefix, result) { 90 | const attributes = el.attributes 91 | 92 | for (let i = 0; i < attributes.length; i++) { 93 | const attr = attributes[i] 94 | if (attr.name.indexOf(prefix) === 0) { 95 | const paramName = camelize(attr.name.slice(prefix.length)) 96 | result[paramName] = attr.value 97 | } 98 | } 99 | return result 100 | } 101 | 102 | function getDefaults(rootEl, routeName, propName, routeEl, options) { 103 | let result = options[propName] 104 | if (typeof result === 'function') result = result.call(rootEl, routeName, routeEl) 105 | return result || {} 106 | } 107 | 108 | function getRouteProp(rootEl, routeName, routeEl, propName, attrPrefix) { 109 | const options = rootEl[routerLinksData].options 110 | const defaults = getDefaults(rootEl, routeName, propName, routeEl, options) 111 | getAttributeValues(rootEl, attrPrefix, defaults) 112 | return getAttributeValues(routeEl, attrPrefix, defaults) 113 | } 114 | 115 | function updateActiveClass(el, routeName, params, query) { 116 | const activeClass = el.hasAttribute('active-class') ? el.getAttribute('active-class') : 'active' 117 | if (activeClass) { 118 | const isActive = router.isActive(routeName, params, query, el.hasAttribute('exact')) 119 | el.classList.toggle(activeClass, isActive) 120 | } 121 | } 122 | 123 | function updateLink(el, rootEl) { 124 | const routeName = el.getAttribute('route') 125 | if (!routeName) return 126 | const params = getRouteProp(rootEl, routeName, el, 'params', 'param-') 127 | const query = getRouteProp(rootEl, routeName, el, 'query', 'query-') 128 | try { 129 | const href = router.generate(routeName, params, query) 130 | const anchorEl = el.tagName === 'A' ? el : el.querySelector('a') 131 | if (anchorEl) anchorEl.setAttribute('href', href) 132 | if (!router.state.activeTransition) { 133 | updateActiveClass(el, routeName, params, query) 134 | } 135 | } catch (error) { 136 | console.warn(`Error generating link for "${routeName}": ${error}`) 137 | } 138 | } 139 | 140 | /** 141 | * @param {HTMLElement} rootEl 142 | */ 143 | function createLinks(rootEl) { 144 | const routeEls = rootEl.querySelectorAll('[route]') 145 | 146 | routeEls.forEach((el) => { 147 | updateLink(el, rootEl) 148 | }) 149 | } 150 | 151 | function linkClickHandler(e) { 152 | if (e.button !== 0 || isModifiedEvent(e)) return 153 | e.preventDefault() 154 | const el = e.selectorTarget 155 | const routeName = el.getAttribute('route') 156 | if (!routeName) return 157 | const params = getRouteProp(this, routeName, el, 'params', 'param-') 158 | const query = getRouteProp(this, routeName, el, 'query', 'query-') 159 | const method = el.hasAttribute('replace') ? 'replaceWith' : 'transitionTo' 160 | router[method](routeName, params, query) 161 | } 162 | 163 | /** 164 | * @export 165 | * @param {HTMLElement} rootEl 166 | * @param {RouterLinksOptions} [options={}] 167 | * @return {Function} 168 | */ 169 | export function bindRouterLinks(rootEl, options = {}) { 170 | const observer = new MutationObserver(mutationHandler) 171 | 172 | observer.rootEl = rootEl 173 | rootEl[routerLinksData] = { options, observer } 174 | 175 | const eventHandler = delegate(rootEl, 'click', '[route]', linkClickHandler, rootEl) 176 | createLinks(rootEl) 177 | observer.observe(rootEl, elementsObserverConfig) 178 | 179 | linkContainers.add(rootEl) 180 | 181 | return function () { 182 | linkContainers.delete(rootEl) 183 | undelegate(rootEl, eventHandler) 184 | } 185 | } 186 | 187 | function create(instance) { 188 | router = instance 189 | } 190 | 191 | function done() { 192 | linkContainers.forEach((rootEl) => { 193 | rootEl.querySelectorAll('[route]').forEach((el) => { 194 | const routeName = el.getAttribute('route') 195 | if (!routeName) return 196 | const params = getRouteProp(rootEl, routeName, el, 'params', 'param-') 197 | const query = getRouteProp(rootEl, routeName, el, 'query', 'query-') 198 | updateActiveClass(el, routeName, params, query) 199 | }) 200 | }) 201 | } 202 | 203 | export const routerLinks = { 204 | create, 205 | done, 206 | } 207 | -------------------------------------------------------------------------------- /tests/functional/eventsTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import 'chai/chai.js' 4 | import { Router } from '../../lib/router' 5 | import { events } from '../../lib/middlewares/events' 6 | import { expect } from '@open-wc/testing' 7 | import { spy } from 'sinon' 8 | 9 | const { describe, it, beforeEach, afterEach } = window 10 | 11 | describe('events middleware', () => { 12 | const routes = (route) => { 13 | route('application', () => { 14 | route('notifications') 15 | route('messages') 16 | route('status', { path: ':user/status/:id' }) 17 | }) 18 | } 19 | let router 20 | 21 | describe('events', () => { 22 | beforeEach(() => { 23 | router = new Router({ location: 'memory', routes }) 24 | router.use(events) 25 | router.listen() 26 | }) 27 | 28 | afterEach(() => { 29 | router.destroy() 30 | }) 31 | 32 | describe('before:transition', () => { 33 | let beforeTransitionSpy, transitionSpy 34 | 35 | beforeEach(() => { 36 | beforeTransitionSpy = spy() 37 | transitionSpy = spy() 38 | window.addEventListener('router-before:transition', beforeTransitionSpy) 39 | window.addEventListener('router-transition', transitionSpy) 40 | }) 41 | 42 | afterEach(() => { 43 | window.removeEventListener('router-before:transition', beforeTransitionSpy) 44 | window.removeEventListener('router-transition', transitionSpy) 45 | }) 46 | 47 | it('should be fired on completed transition', async () => { 48 | await router.transitionTo('messages') 49 | expect(beforeTransitionSpy).to.be.calledOnce 50 | expect(beforeTransitionSpy).to.be.calledBefore(transitionSpy) 51 | 52 | await router.transitionTo('notifications') 53 | expect(beforeTransitionSpy).to.be.calledTwice 54 | }) 55 | 56 | it('should be fired on cancelled transition', async () => { 57 | router.use((transition) => transition.cancel()) 58 | try { 59 | await router.transitionTo('messages') 60 | } catch (error) {} 61 | expect(beforeTransitionSpy).to.be.called 62 | }) 63 | 64 | it('should be fired on cancelled transition', async () => { 65 | router.use(() => { 66 | throw new Error('error') 67 | }) 68 | try { 69 | await router.transitionTo('messages') 70 | } catch (error) {} 71 | expect(beforeTransitionSpy).to.be.called 72 | }) 73 | }) 74 | 75 | describe('transition', () => { 76 | let transitionSpy 77 | 78 | beforeEach(() => { 79 | transitionSpy = spy() 80 | window.addEventListener('router-transition', transitionSpy) 81 | }) 82 | 83 | afterEach(() => { 84 | window.removeEventListener('router-transition', transitionSpy) 85 | }) 86 | 87 | it('should be fired on completed transition', async () => { 88 | await router.transitionTo('messages') 89 | expect(transitionSpy).to.be.calledOnce 90 | 91 | await router.transitionTo('notifications') 92 | expect(transitionSpy).to.be.calledTwice 93 | }) 94 | 95 | it('should not be fired on cancelled transition', async () => { 96 | router.use((transition) => transition.cancel()) 97 | try { 98 | await router.transitionTo('messages') 99 | } catch (error) {} 100 | expect(transitionSpy).to.not.be.called 101 | }) 102 | 103 | it('should not be fired on cancelled transition', async () => { 104 | router.use(() => { 105 | throw new Error('error') 106 | }) 107 | try { 108 | await router.transitionTo('messages') 109 | } catch (error) {} 110 | expect(transitionSpy).to.not.be.called 111 | }) 112 | }) 113 | 114 | describe('abort', () => { 115 | let abortSpy 116 | 117 | beforeEach(() => { 118 | abortSpy = spy() 119 | window.addEventListener('router-abort', abortSpy) 120 | }) 121 | 122 | afterEach(() => { 123 | window.removeEventListener('router-abort', abortSpy) 124 | }) 125 | 126 | it('should not be fired on completed transition', async () => { 127 | await router.transitionTo('messages') 128 | expect(abortSpy).to.not.be.called 129 | 130 | await router.transitionTo('notifications') 131 | expect(abortSpy).to.not.be.called 132 | }) 133 | 134 | it('should be fired on cancelled transition', async () => { 135 | router.use((transition) => transition.cancel()) 136 | try { 137 | await router.transitionTo('messages') 138 | } catch (error) {} 139 | expect(abortSpy).to.be.calledOnce 140 | }) 141 | 142 | it('should be fired on cancelled transition', async () => { 143 | router.use(() => { 144 | throw new Error('error') 145 | }) 146 | try { 147 | await router.transitionTo('messages') 148 | } catch (error) {} 149 | expect(abortSpy).to.be.calledOnce 150 | }) 151 | }) 152 | 153 | describe('error', () => { 154 | let errorSpy 155 | 156 | beforeEach(() => { 157 | errorSpy = spy() 158 | window.addEventListener('router-error', errorSpy) 159 | }) 160 | 161 | afterEach(() => { 162 | window.removeEventListener('router-error', errorSpy) 163 | }) 164 | 165 | it('should not be fired on completed transition', async () => { 166 | await router.transitionTo('messages') 167 | expect(errorSpy).to.not.be.called 168 | 169 | await router.transitionTo('notifications') 170 | expect(errorSpy).to.not.be.called 171 | }) 172 | 173 | it('should not be fired on cancelled transition', async () => { 174 | router.use((transition) => transition.cancel()) 175 | try { 176 | await router.transitionTo('messages') 177 | } catch (error) {} 178 | expect(errorSpy).to.not.be.called 179 | }) 180 | 181 | it('should be fired on cancelled transition', async () => { 182 | router.use(() => { 183 | throw new Error('error') 184 | }) 185 | try { 186 | await router.transitionTo('messages') 187 | } catch (error) {} 188 | expect(errorSpy).to.be.calledOnce 189 | }) 190 | }) 191 | }) 192 | 193 | describe('eventPrefix', () => { 194 | beforeEach(() => { 195 | router = new Router({ location: 'memory', routes, eventPrefix: 'my-router:' }) 196 | router.use(events) 197 | router.listen() 198 | }) 199 | 200 | afterEach(() => { 201 | router.destroy() 202 | }) 203 | 204 | describe('customized', () => { 205 | let transitionSpy 206 | 207 | beforeEach(() => { 208 | transitionSpy = spy() 209 | window.addEventListener('my-router:transition', transitionSpy) 210 | }) 211 | 212 | afterEach(() => { 213 | window.removeEventListener('my-router:transition', transitionSpy) 214 | }) 215 | 216 | it('should be fired using eventPrefix option', async () => { 217 | await router.transitionTo('messages') 218 | expect(transitionSpy).to.be.calledOnce 219 | 220 | await router.transitionTo('notifications') 221 | expect(transitionSpy).to.be.calledTwice 222 | }) 223 | }) 224 | }) 225 | }) 226 | -------------------------------------------------------------------------------- /lib/components/animated-outlet.js: -------------------------------------------------------------------------------- 1 | export class AnimationHook { 2 | constructor(options = {}) { 3 | this.options = options 4 | } 5 | 6 | getOption(outlet, name) { 7 | return outlet.hasAttribute(name) ? outlet.getAttribute(name) : this.options[name] 8 | } 9 | 10 | hasOption(outlet, name) { 11 | return outlet.hasAttribute(name) || this.options[name] 12 | } 13 | 14 | runParallel(outlet) { 15 | return this.hasOption(outlet, 'parallel') 16 | } 17 | 18 | beforeEnter(outlet, el) {} 19 | 20 | enter(outlet, el) {} 21 | 22 | leave(outlet, el, done) { 23 | done() 24 | } 25 | } 26 | 27 | // code extracted from vue 28 | const raf = window.requestAnimationFrame 29 | const TRANSITION = 'transition' 30 | const ANIMATION = 'animation' 31 | 32 | // Transition property/event sniffing 33 | const transitionProp = 'transition' 34 | const transitionEndEvent = 'transitionend' 35 | const animationProp = 'animation' 36 | const animationEndEvent = 'animationend' 37 | 38 | function nextFrame(fn) { 39 | raf(function () { 40 | raf(fn) 41 | }) 42 | } 43 | 44 | function whenTransitionEnds(el, cb) { 45 | const ref = getTransitionInfo(el) 46 | const type = ref.type 47 | const timeout = ref.timeout 48 | const propCount = ref.propCount 49 | if (!type) { 50 | return cb() 51 | } 52 | const event = type === TRANSITION ? transitionEndEvent : animationEndEvent 53 | let ended = 0 54 | const end = function () { 55 | el.removeEventListener(event, onEnd) 56 | cb() 57 | } 58 | const onEnd = function (e) { 59 | if (e.target === el) { 60 | if (++ended >= propCount) { 61 | end() 62 | } 63 | } 64 | } 65 | setTimeout(function () { 66 | if (ended < propCount) { 67 | end() 68 | } 69 | }, timeout + 1) 70 | el.addEventListener(event, onEnd) 71 | } 72 | 73 | function getTransitionInfo(el) { 74 | const styles = window.getComputedStyle(el) 75 | // JSDOM may return undefined for transition properties 76 | const transitionDelays = (styles[transitionProp + 'Delay'] || '').split(', ') 77 | const transitionDurations = (styles[transitionProp + 'Duration'] || '').split(', ') 78 | const transitionTimeout = getTimeout(transitionDelays, transitionDurations) 79 | const animationDelays = (styles[animationProp + 'Delay'] || '').split(', ') 80 | const animationDurations = (styles[animationProp + 'Duration'] || '').split(', ') 81 | const animationTimeout = getTimeout(animationDelays, animationDurations) 82 | 83 | const timeout = Math.max(transitionTimeout, animationTimeout) 84 | const type = timeout > 0 ? (transitionTimeout > animationTimeout ? TRANSITION : ANIMATION) : null 85 | const propCount = type 86 | ? type === TRANSITION 87 | ? transitionDurations.length 88 | : animationDurations.length 89 | : 0 90 | 91 | return { 92 | type, 93 | timeout, 94 | propCount, 95 | } 96 | } 97 | 98 | function getTimeout(delays, durations) { 99 | /* istanbul ignore next */ 100 | while (delays.length < durations.length) { 101 | delays = delays.concat(delays) 102 | } 103 | 104 | return Math.max.apply( 105 | null, 106 | durations.map(function (d, i) { 107 | return toMs(d) + toMs(delays[i]) 108 | }), 109 | ) 110 | } 111 | 112 | // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers 113 | // in a locale-dependent way, using a comma instead of a dot. 114 | // If comma is not replaced with a dot, the input will be rounded down (i.e. acting 115 | // as a floor function) causing unexpected behaviors 116 | function toMs(s) { 117 | return Number(s.slice(0, -1).replace(',', '.')) * 1000 118 | } 119 | 120 | function runTransition(el, name, type, cb) { 121 | el.classList.add(`${name}-${type}-active`) 122 | nextFrame(function () { 123 | el.classList.remove(`${name}-${type}`) 124 | el.classList.add(`${name}-${type}-to`) 125 | whenTransitionEnds(el, function () { 126 | el.classList.remove(`${name}-${type}-active`, `${name}-${type}-to`) 127 | if (cb) cb() 128 | }) 129 | }) 130 | } 131 | 132 | export class GenericCSS extends AnimationHook { 133 | beforeEnter(outlet, el) { 134 | const name = outlet.getAttribute('animation') || 'outlet' 135 | el.classList.add(`${name}-enter`) 136 | } 137 | 138 | enter(outlet, el) { 139 | const name = outlet.getAttribute('animation') || 'outlet' 140 | runTransition(el, name, 'enter') 141 | } 142 | 143 | leave(outlet, el, done) { 144 | const name = outlet.getAttribute('animation') || 'outlet' 145 | el.classList.add(`${name}-leave`) 146 | runTransition(el, name, 'leave', done) 147 | } 148 | } 149 | 150 | export class AnimateCSS extends AnimationHook { 151 | beforeEnter(outlet, el) { 152 | const enter = this.getOption(outlet, 'enter') 153 | if (enter) { 154 | el.style.display = 'none' 155 | } 156 | } 157 | 158 | enter(outlet, el) { 159 | const enter = this.getOption(outlet, 'enter') 160 | if (!enter) return 161 | el.style.display = 'block' 162 | el.classList.add('animated', enter) 163 | el.addEventListener( 164 | 'animationend', 165 | () => { 166 | el.classList.remove('animated', enter) 167 | }, 168 | { once: true }, 169 | ) 170 | } 171 | 172 | leave(outlet, el, done) { 173 | const leave = this.getOption(outlet, 'leave') 174 | if (!leave) { 175 | done() 176 | return 177 | } 178 | el.classList.add('animated', leave) 179 | el.addEventListener('animationend', done, { once: true }) 180 | } 181 | } 182 | 183 | const animationRegistry = {} 184 | let defaultHook 185 | 186 | export function registerAnimation(name, AnimationHookClass, options = {}) { 187 | animationRegistry[name] = new AnimationHookClass(options) 188 | } 189 | 190 | export function setDefaultAnimation(AnimationHookClass, options = {}) { 191 | defaultHook = new AnimationHookClass(options) 192 | } 193 | 194 | function getAnimationHook(name) { 195 | return animationRegistry[name] || defaultHook || (defaultHook = new GenericCSS()) 196 | } 197 | 198 | export class AnimatedOutlet extends HTMLElement { 199 | appendChild(el) { 200 | if (!this.hasAttribute('animation')) { 201 | super.appendChild(el) 202 | return 203 | } 204 | const hook = getAnimationHook(this.getAttribute('animation')) 205 | const runParallel = hook.runParallel(this) 206 | 207 | hook.beforeEnter(this, el) 208 | super.appendChild(el) 209 | if (!runParallel && this.removing) { 210 | // when removing a previous el, append animation is run after remove one 211 | this.appending = el 212 | } else { 213 | hook.enter(this, el) 214 | } 215 | } 216 | 217 | removeChild(el) { 218 | if (!this.hasAttribute('animation')) { 219 | super.removeChild(el) 220 | return 221 | } 222 | const hook = getAnimationHook(this.getAttribute('animation')) 223 | 224 | if (this.removing && this.removing.parentNode === this) { 225 | super.removeChild(this.removing) 226 | } 227 | 228 | if (el === this.appending) { 229 | if (el.parentNode === this) { 230 | super.removeChild(el) 231 | } 232 | this.removing = null 233 | return 234 | } 235 | 236 | this.removing = el 237 | hook.leave(this, el, () => { 238 | if (this.removing && this.removing.parentNode === this) { 239 | super.removeChild(this.removing) 240 | } 241 | if (this.appending) hook.enter(this, this.appending) 242 | this.appending = null 243 | this.removing = null 244 | }) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /tests/functional/routerTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-return-assign */ 2 | import $ from './nanodom' 3 | import TestApp from './testApp' 4 | import 'chai/chai.js' 5 | 6 | const { assert } = window.chai 7 | const { describe, it, beforeEach, afterEach } = window 8 | let app, router 9 | 10 | describe('app', () => { 11 | beforeEach(() => { 12 | window.location.hash = '/' 13 | app = new TestApp() 14 | router = app.router 15 | return app.start() 16 | }) 17 | 18 | afterEach(() => { 19 | app.destroy() 20 | }) 21 | 22 | it('transition occurs when location.hash changes', (done) => { 23 | router.use((transition) => { 24 | transition 25 | .then(() => { 26 | assert.equal(transition.path, '/about') 27 | assert.equal($('.application .outlet').html(), 'This is about page') 28 | done() 29 | }) 30 | .catch(done, done) 31 | }) 32 | 33 | window.location.hash = '#about' 34 | }) 35 | 36 | it('programmatic transition via url and route names', async function () { 37 | await router.transitionTo('about') 38 | await router.transitionTo('/faq?sortBy=date') 39 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: date') 40 | await router.transitionTo('faq', {}, { sortBy: 'user' }) 41 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: user') 42 | }) 43 | 44 | it('cancelling and retrying transitions', async function () { 45 | await router.transitionTo('/posts/filter/foo') 46 | assert.equal(router.location.getURL(), '/posts/filter/foo') 47 | const transition = router.transitionTo('about') 48 | transition.cancel() 49 | await transition.catch(() => {}) 50 | assert.equal(router.location.getURL(), '/posts/filter/foo') 51 | 52 | await transition.retry() 53 | assert.equal(router.location.getURL(), '/about') 54 | }) 55 | 56 | it('transition.followRedirects resolves when all of the redirects have finished', async function () { 57 | await router.transitionTo('application') 58 | // initiate a transition 59 | const transition = router.transitionTo('/posts/filter/foo') 60 | // and a redirect 61 | router.transitionTo('/about') 62 | 63 | // if followRedirects is not used - the original transition is rejected 64 | let rejected = false 65 | await transition.catch(() => (rejected = true)) 66 | assert(rejected) 67 | 68 | await router.transitionTo('application') 69 | // initiate a transition 70 | const t = router.transitionTo('/posts/filter/foo') 71 | // and a redirect, this time using `redirectTo` 72 | t.redirectTo('/about') 73 | 74 | // when followRedirects is used - the promise is only 75 | // resolved when both transitions finish 76 | await transition.followRedirects() 77 | assert.equal(router.location.getURL(), '/about') 78 | }) 79 | 80 | it('transition.followRedirects is rejected if transition fails', async function () { 81 | // silence the errors for the tests 82 | router.logError = () => {} 83 | 84 | // initiate a transition 85 | const transition = router.transitionTo('/posts/filter/foo') 86 | // install a breaking middleware 87 | router.use(() => { 88 | throw new Error('middleware error') 89 | }) 90 | // and a redirect 91 | router.transitionTo('/about') 92 | 93 | let rejected = false 94 | await transition.followRedirects().catch((err) => (rejected = err.message)) 95 | assert.equal(rejected, 'middleware error') 96 | }) 97 | 98 | it('transition.followRedirects is rejected if transition fails asynchronously', async function () { 99 | // silence the errors for the tests 100 | router.logError = () => {} 101 | 102 | // initiate a transition 103 | const transition = router.transitionTo('/posts/filter/foo') 104 | // install a breaking middleware 105 | router.use(() => { 106 | return Promise.reject(new Error('middleware promise error')) 107 | }) 108 | // and a redirect 109 | router.transitionTo('/about') 110 | 111 | let rejected = false 112 | await transition.followRedirects().catch((err) => (rejected = err.message)) 113 | assert.equal(rejected, 'middleware promise error') 114 | }) 115 | 116 | it.skip('cancelling transition does not add a history entry', async function () { 117 | // we start of at faq 118 | await router.transitionTo('faq') 119 | // then go to posts.filter 120 | await router.transitionTo('posts.filter', { filterId: 'foo' }) 121 | assert.equal(window.location.hash, '#posts/filter/foo') 122 | 123 | // now attempt to transition to about and cancel 124 | const transition = router.transitionTo('/about') 125 | transition.cancel() 126 | await transition.catch(() => {}) 127 | 128 | // the url is still posts.filter 129 | assert.equal(window.location.hash, '#posts/filter/foo') 130 | 131 | // at first look going back now should take to #faq but it does not: 132 | // the initial steps creates this history: #faq > #posts/filter/foo > #about 133 | // calling cancel replaces #about by #posts/filter/foo so the history is now 134 | // #faq > #posts/filter/foo > #posts/filter/foo 135 | // calling history.back goes from #posts/filter/foo to #posts/filter/foo which is ignored 136 | // the native history API does not provide a way to cancel a navigation 137 | // or to go back synchronously and without triggering the events 138 | // the solution would be to handle router own history 139 | await new Promise((resolve, reject) => { 140 | router.use((transition) => { 141 | transition 142 | .then(() => { 143 | assert.equal(window.location.hash, '#faq') 144 | resolve() 145 | }) 146 | .catch(reject) 147 | }) 148 | window.history.back() 149 | }) 150 | }) 151 | 152 | it('navigating around the app', async function () { 153 | assert.equal($('.application .outlet').html(), 'Welcome to this application') 154 | 155 | await router.transitionTo('about') 156 | assert.equal($('.application .outlet').html(), 'This is about page') 157 | 158 | await router.transitionTo('/faq?sortBy=date') 159 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: date') 160 | 161 | await router.transitionTo('faq', {}, { sortBy: 'user' }) 162 | assert.equal($('.application .outlet').html(), 'FAQ. Sorted By: user') 163 | 164 | // we can also change the url directly to cause another transition to happen 165 | await new Promise(function (resolve) { 166 | router.use(resolve) 167 | window.location.hash = '#posts/filter/mine' 168 | }) 169 | assert.equal($('.application .outlet').html(), 'My posts...') 170 | 171 | await new Promise(function (resolve) { 172 | router.use(resolve) 173 | window.location.hash = '#posts/filter/foo' 174 | }) 175 | assert.equal($('.application .outlet').html(), 'Filter not found') 176 | }) 177 | 178 | it('url behaviour during transitions', async function () { 179 | assert.equal(window.location.hash, '#/') 180 | const transition = router.transitionTo('about') 181 | assert.equal(window.location.hash, '#about') 182 | await transition 183 | assert.equal(window.location.hash, '#about') 184 | // would be cool to it history.back() here 185 | // but in IE it reloads the karma iframe, so let's 186 | // use a regular location.hash assignment instead 187 | // window.history.back() 188 | window.location.hash = '#/' 189 | await new Promise((resolve) => { 190 | router.use((transition) => { 191 | assert.equal(window.location.hash, '#/') 192 | resolve() 193 | }) 194 | }) 195 | }) 196 | 197 | it('url behaviour during failed transitions', async function () { 198 | router.logError = () => {} 199 | await router.transitionTo('about') 200 | await new Promise((resolve, reject) => { 201 | // setup a middleware that will fail 202 | router.use((transition) => { 203 | // but catch the error 204 | transition 205 | .catch((err) => { 206 | assert.equal(err.message, 'failed') 207 | assert.equal(window.location.hash, '#faq') 208 | resolve() 209 | }) 210 | .catch(reject) 211 | throw new Error('failed') 212 | }) 213 | router.transitionTo('faq') 214 | }) 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /tests/functional/routerLinksTest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | /* global describe,beforeEach,afterEach,it,$ */ 3 | 4 | import { Router } from '../../lib/router' 5 | import { wc } from '../../lib/middlewares/wc' 6 | import { routerLinks, bindRouterLinks } from '../../lib/middlewares/router-links' 7 | import { defineCE, expect, fixtureSync } from '@open-wc/testing' 8 | import { LitElement, html } from 'lit-element' 9 | import sinon from 'sinon' 10 | import 'jquery' 11 | 12 | class ParentView extends LitElement { 13 | createRenderRoot() { 14 | return this 15 | } 16 | 17 | render() { 18 | return html` 19 |
20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 | 42 | ` 43 | } 44 | } 45 | const parentTag = defineCE(ParentView) 46 | 47 | class ChildView extends LitElement { 48 | createRenderRoot() { 49 | return this 50 | } 51 | 52 | render() { 53 | return html` ` 54 | } 55 | } 56 | defineCE(ChildView) 57 | 58 | describe('bindRouterLinks', () => { 59 | let router, outlet, parentComponent 60 | beforeEach(() => { 61 | outlet = document.createElement('div') 62 | document.body.appendChild(outlet) 63 | const routes = function (route) { 64 | route('parent', { component: () => parentComponent }, function () { 65 | route('child', { component: ChildView }, function () { 66 | route('grandchild', { component: ParentView }) 67 | }) 68 | }) 69 | route('root', { path: 'root/:id', component: ParentView }) 70 | route('secondroot', { path: 'secondroot/:personId', component: ParentView }) 71 | } 72 | parentComponent = ParentView 73 | router = new Router({ location: 'memory', outlet, routes }) 74 | router.use(wc) 75 | router.use(routerLinks) 76 | router.listen() 77 | }) 78 | 79 | afterEach(() => { 80 | outlet.remove() 81 | router.destroy() 82 | }) 83 | 84 | describe('when calling bindRouterLinks in pre-rendered HTML', function () { 85 | let unbind, preRenderedEl 86 | beforeEach(function () { 87 | preRenderedEl = fixtureSync(`
88 |
89 | 90 | 91 | 92 |
93 | 94 | 95 |
96 |
`) 97 | unbind = bindRouterLinks(preRenderedEl) 98 | }) 99 | 100 | it('should generate href attributes in anchor tags with route attribute', function () { 101 | return router.transitionTo('parent').then(async function () { 102 | expect($('#a-preparentlink').attr('href')).to.be.equal('/parent') 103 | expect($('#a-prerootlink2').attr('href')).to.be.equal('/root/2') 104 | expect($('#a-pregrandchildlink').attr('href')).to.be.equal( 105 | '/parent/child/grandchild?name=test', 106 | ) 107 | }) 108 | }) 109 | 110 | it('should set active class in tags with route attribute', function () { 111 | return router.transitionTo('parent').then(async function () { 112 | expect($('#a-preparentlink').hasClass('active')).to.be.true 113 | expect($('#a-prerootlink2').hasClass('active')).to.be.false 114 | expect($('#div-preparentlink').hasClass('active')).to.be.true 115 | expect($('#div-prerootlink1').hasClass('active')).to.be.false 116 | }) 117 | }) 118 | 119 | it('should call transitionTo when a non anchor tags with route attribute is clicked', function () { 120 | return router.transitionTo('parent').then(async function () { 121 | const spy = sinon.spy(router, 'transitionTo') 122 | $('#div-prerootlink1').click() 123 | expect(spy).to.be.calledOnce.and.calledWithExactly('root', { id: '1' }, {}) 124 | 125 | spy.resetHistory() 126 | $('#div-pregrandchildlink').click() 127 | expect(spy).to.be.calledOnce.and.calledWithExactly('grandchild', {}, { name: 'test' }) 128 | 129 | spy.resetHistory() 130 | $('#preinnerparent').click() 131 | expect(spy).to.be.calledOnce.and.calledWithExactly('parent', {}, {}) 132 | }) 133 | }) 134 | 135 | it('should not call transitionTo after calling function returned by bindRouterLinks', function () { 136 | unbind() 137 | return router.transitionTo('parent').then(async function () { 138 | const spy = sinon.spy(router, 'transitionTo') 139 | $('#div-prerootlink1').click() 140 | $('#div-pregrandchildlink').click() 141 | $('#preinnerparent').click() 142 | expect(spy).to.not.be.called 143 | }) 144 | }) 145 | 146 | describe('and nodes are added dynamically', () => { 147 | it('should generate href attributes in anchor tags with route attribute', function (done) { 148 | router.transitionTo('parent').then(async function () { 149 | const parentEl = document.querySelector(parentTag) 150 | await parentEl.updateComplete 151 | $(` 152 | 153 | 154 | `).appendTo(document.querySelector('#prerendered')) 155 | 156 | // links are updated asynchronously by MutationObserver 157 | setTimeout(() => { 158 | expect($('#a-dyn-preparentlink').attr('href')).to.be.equal('/parent') 159 | expect($('#a-dyn-prerootlink2').attr('href')).to.be.equal('/root/2') 160 | expect($('#a-dyn-pregrandchildlink').attr('href')).to.be.equal( 161 | '/parent/child/grandchild?name=test', 162 | ) 163 | done() 164 | }, 0) 165 | }) 166 | }) 167 | 168 | it('should set active class in tags with route attribute', function (done) { 169 | router.transitionTo('parent').then(async function () { 170 | const parentEl = document.querySelector(parentTag) 171 | await parentEl.updateComplete 172 | $(` 173 | 174 | 175 | `).appendTo(document.querySelector('#prerendered')) 176 | 177 | // links are updated asynchronously by MutationObserver 178 | setTimeout(() => { 179 | expect($('#a-dyn-preparentlink').hasClass('active')).to.be.true 180 | expect($('#a-dyn-prerootlink2').hasClass('active')).to.be.false 181 | expect($('#a-dyn-pregrandchildlink').hasClass('active')).to.be.false 182 | done() 183 | }, 0) 184 | }) 185 | }) 186 | }) 187 | }) 188 | }) 189 | -------------------------------------------------------------------------------- /lib/locations/location-bar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // LocationBar module extracted from Backbone.js 1.1.0 3 | // 4 | // the dependency on backbone, underscore and jquery have been removed to turn 5 | // this into a small standalone library for handling browser's history API 6 | // cross browser and with a fallback to hashchange events or polling. 7 | 8 | import {extend} from '../utils.js' 9 | import {bindEvent, unbindEvent} from '../events.js' 10 | 11 | // this is mostly original code with minor modifications 12 | // to avoid dependency on 3rd party libraries 13 | // 14 | // Backbone.History 15 | // ---------------- 16 | 17 | // Handles cross-browser history management, based on either 18 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 19 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 20 | // and URL fragments. 21 | class History { 22 | constructor() { 23 | this.handlers = []; 24 | this.checkUrl = this.checkUrl.bind(this); 25 | this.location = window.location; 26 | this.history = window.history; 27 | } 28 | 29 | // Set up all inheritable **Backbone.History** properties and methods. 30 | // Are we at the app root? 31 | atRoot() { 32 | return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; 33 | } 34 | 35 | // Gets the true hash value. Cannot use location.hash directly due to bug 36 | // in Firefox where location.hash will always be decoded. 37 | getHash() { 38 | const match = this.location.href.match(/#(.*)$/); 39 | return match ? match[1] : ''; 40 | } 41 | 42 | // Get the cross-browser normalized URL fragment, either from the URL, 43 | // the hash, or the override. 44 | getFragment(fragment, forcePushState) { 45 | if (fragment == null) { 46 | if (this._hasPushState || !this._wantsHashChange || forcePushState) { 47 | fragment = decodeURI(this.location.pathname + this.location.search); 48 | const root = this.root.replace(trailingSlash, ''); 49 | if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); 50 | } else { 51 | fragment = this.getHash(); 52 | } 53 | } 54 | return fragment.replace(routeStripper, ''); 55 | } 56 | 57 | // Start the hash change handling, returning `true` if the current URL matches 58 | // an existing route, and `false` otherwise. 59 | start(options = {}) { 60 | // MODIFICATION OF ORIGINAL BACKBONE.HISTORY 61 | // if (History.started) throw new Error("LocationBar has already been started"); 62 | // History.started = true; 63 | this.started = true; 64 | 65 | // Figure out the initial configuration. 66 | // Is pushState desired ... is it available? 67 | this.options = extend({root: '/'}, options); 68 | this.location = this.options.location || this.location; 69 | this.history = this.options.history || this.history; 70 | this.root = this.options.root; 71 | this._wantsHashChange = this.options.hashChange !== false; 72 | this._wantsPushState = !!this.options.pushState; 73 | this._hasPushState = this._wantsPushState; 74 | const fragment = this.getFragment(); 75 | 76 | // Normalize root to always include a leading and trailing slash. 77 | this.root = (`/${this.root}/`).replace(rootStripper, '/'); 78 | 79 | // Depending on whether we're using pushState or hashes, and whether 80 | // 'onhashchange' is supported, determine how we check the URL state. 81 | bindEvent(window, this._hasPushState ? 'popstate' : 'hashchange', this.checkUrl); 82 | 83 | // Determine if we need to change the base url, for a pushState link 84 | // opened by a non-pushState browser. 85 | this.fragment = fragment; 86 | const loc = this.location; 87 | 88 | // Transition from hashChange to pushState or vice versa if both are 89 | // requested. 90 | if (this._wantsHashChange && this._wantsPushState) { 91 | 92 | // If we've started off with a route from a `pushState`-enabled 93 | // browser, but we're currently in a browser that doesn't support it... 94 | if (!this._hasPushState && !this.atRoot()) { 95 | this.fragment = this.getFragment(null, true); 96 | this.location.replace(`${this.root}#${this.fragment}`); 97 | // Return immediately as browser will do redirect to new url 98 | return true; 99 | 100 | // Or if we've started out with a hash-based route, but we're currently 101 | // in a browser where it could be `pushState`-based instead... 102 | } else if (this._hasPushState && this.atRoot() && loc.hash) { 103 | this.fragment = this.getHash().replace(routeStripper, ''); 104 | this.history.replaceState({}, document.title, this.root + this.fragment); 105 | } 106 | 107 | } 108 | 109 | if (!this.options.silent) return this.loadUrl(); 110 | } 111 | 112 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app, 113 | // but possibly useful for unit testing Routers. 114 | stop() { 115 | unbindEvent(window, this._hasPushState ? 'popstate' : 'hashchange', this.checkUrl); 116 | this.started = false; 117 | } 118 | 119 | // Add a route to be tested when the fragment changes. Routes added later 120 | // may override previous routes. 121 | route(route, callback) { 122 | this.handlers.unshift({route, callback}); 123 | } 124 | 125 | // Checks the current URL to see if it has changed, and if it has, 126 | // calls `loadUrl`. 127 | checkUrl() { 128 | const current = this.getFragment(); 129 | if (current === this.fragment) return false; 130 | this.loadUrl(); 131 | } 132 | 133 | // Attempt to load the current URL fragment. If a route succeeds with a 134 | // match, returns `true`. If no defined routes matches the fragment, 135 | // returns `false`. 136 | loadUrl(fragment) { 137 | fragment = this.fragment = this.getFragment(fragment); 138 | return this.handlers.some(handler => { 139 | if (handler.route.test(fragment)) { 140 | handler.callback(fragment); 141 | return true; 142 | } 143 | }); 144 | } 145 | 146 | // Save a fragment into the hash history, or replace the URL state if the 147 | // 'replace' option is passed. You are responsible for properly URL-encoding 148 | // the fragment in advance. 149 | // 150 | // The options object can contain `trigger: true` if you wish to have the 151 | // route callback be fired (not usually desirable), or `replace: true`, if 152 | // you wish to modify the current URL without adding an entry to the history. 153 | update(fragment, options) { 154 | if (!this.started) return false; 155 | if (!options || options === true) options = {trigger: !!options}; 156 | 157 | let url = this.root + (fragment = this.getFragment(fragment || '')); 158 | 159 | // Strip the hash for matching. 160 | fragment = fragment.replace(pathStripper, ''); 161 | 162 | if (this.fragment === fragment) return; 163 | this.fragment = fragment; 164 | 165 | // Don't include a trailing slash on the root. 166 | if (fragment === '' && url !== '/') url = url.slice(0, -1); 167 | 168 | // If pushState is available, we use it to set the fragment as a real URL. 169 | if (this._hasPushState) { 170 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 171 | 172 | // If hash changes haven't been explicitly disabled, update the hash 173 | // fragment to store history. 174 | } else if (this._wantsHashChange) { 175 | this._updateHash(this.location, fragment, options.replace); 176 | // If you've told us that you explicitly don't want fallback hashchange- 177 | // based history, then `update` becomes a page refresh. 178 | } else { 179 | return this.location.assign(url); 180 | } 181 | if (options.trigger) return this.loadUrl(fragment); 182 | } 183 | 184 | // Update the hash location, either replacing the current entry, or adding 185 | // a new one to the browser history. 186 | _updateHash(location, fragment, replace) { 187 | if (replace) { 188 | const href = location.href.replace(/(javascript:|#).*$/, ''); 189 | location.replace(`${href}#${fragment}`); 190 | } else { 191 | // Some browsers require that `hash` contains a leading #. 192 | location.hash = `#${fragment}`; 193 | } 194 | } 195 | 196 | // add some features to History 197 | 198 | // a generic callback for any changes 199 | onChange(callback) { 200 | this.route(/^(.*?)$/, callback); 201 | } 202 | 203 | // checks if the browser has pushstate support 204 | hasPushState() { 205 | // MODIFICATION OF ORIGINAL BACKBONE.HISTORY 206 | if (!this.started) { 207 | throw new Error("only available after LocationBar.start()"); 208 | } 209 | return this._hasPushState; 210 | } 211 | } 212 | 213 | // Cached regex for stripping a leading hash/slash and trailing space. 214 | const routeStripper = /^[#\/]|\s+$/g; 215 | 216 | // Cached regex for stripping leading and trailing slashes. 217 | const rootStripper = /^\/+|\/+$/g; 218 | 219 | // Cached regex for removing a trailing slash. 220 | const trailingSlash = /\/$/; 221 | 222 | // Cached regex for stripping urls of hash. 223 | const pathStripper = /#.*$/; 224 | 225 | 226 | // export 227 | export default History; 228 | --------------------------------------------------------------------------------