├── .gitignore ├── .npmignore ├── .travis.yml ├── .versions ├── History.md ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── lib ├── flow.js ├── fluid.js ├── way.js └── ways.js ├── package.js ├── package.json └── test ├── adapters.js ├── events.js ├── flow-destroy-run.js ├── flow-interruption.js ├── flow-run-destroy.js ├── matchall.js ├── meteor.js ├── mocha.opts ├── no-flow.js └── pathname.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | bower_components -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | coverage 3 | .travis.yml 4 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | # COVERALLS_REPO_TOKEN 4 | - secure: "e90Xq9tu1ZmKZXqtAMMaK/bkDLFTEVzyKaOaWfic4Bx1RfwsMPAfjS6XVZrACgaEq0QzXVzMkrZaTH/y1Rn7xYsg+iwV9ceE1T75kPcfcGERll/KGcDPBnhAD4dPGl0AsUdHEo74fzGmQD2R6028fewm/gOCMJfz2DT5Bg5PM8U=" 5 | 6 | # CODECLIMATE_REPO_TOKEN 7 | - secure: "HbYQcNULTgjqLjx+ZO0E46VX2s2F0KwW+1jLJTEgevUSkQYk0XPr6jyDPDE+qUeN+Vr0U9MQXUhOYq8nbVFsRcXczODc5I4vipMiK9RRP3v6czM5jd07JYd5i3M4VlX8po4psTuBDL7tz2cRdilsj7t1VKuyWK8HVnVpxCMVhbI=" 8 | 9 | language: node_js 10 | 11 | node_js: 12 | - iojs 13 | - 0.10.36 14 | - 0.12 15 | 16 | install: 17 | - npm install 18 | - curl https://install.meteor.com | /bin/sh 19 | 20 | script: 21 | - make test.coverage.coveralls 22 | - make test.meteor.headless -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | arboleya:happens@0.6.0 2 | arboleya:ways@0.4.0 3 | arboleya:ways-addressbar@0.2.1 4 | base64@1.0.3 5 | binary-heap@1.0.3 6 | callback-hook@1.0.3 7 | check@1.0.5 8 | ddp@1.1.0 9 | ejson@1.0.6 10 | geojson-utils@1.0.3 11 | id-map@1.0.3 12 | json@1.0.3 13 | local-test:arboleya:ways@0.4.0 14 | logging@1.0.7 15 | meteor@1.1.6 16 | minimongo@1.0.8 17 | mongo@1.1.0 18 | ordered-dict@1.0.3 19 | random@1.0.3 20 | retry@1.0.3 21 | tinytest@1.0.5 22 | tracker@1.0.7 23 | underscore@1.0.3 24 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.5.0 / 2015-04-25 2 | =================== 3 | * Adding `url:change` event 4 | * Providing `pathname` even if `addressbar` plugin is not enabled 5 | 6 | 0.4.0 / 2015-04-24 7 | =================== 8 | * Refactoring everything 9 | * Adding Meteor support 10 | * Adding Bower support 11 | 12 | 0.3.4 / 2014-01-23 13 | =================== 14 | * Fixing optional splat params parser 15 | 16 | 0.3.3 / 2013-12-09 17 | =================== 18 | * Adding `init` method to keep initial state clean 19 | 20 | 0.3.2 / 2013-12-09 21 | =================== 22 | * Fixing flow-interruption handling 23 | 24 | 0.3.1 / 2013-12-09 25 | =================== 26 | * Upgrading happens 27 | 28 | 0.3.0 / 2013-12-08 29 | =================== 30 | * Removing coffeescript dependency and porting everything to javascript 31 | * Make the API more fluent 32 | 33 | 0.2.3 / 2013-12-08 34 | =================== 35 | * Normalizing urls 36 | 37 | 0.2.2 / 2013-12-07 38 | =================== 39 | * Handling flows' interruptions 40 | 41 | 0.2.1 / 2013-11-30 42 | =================== 43 | * Fixing middlewares integration 44 | * Normalizing API 45 | 46 | 0.2.0 / 2013-11-29 47 | =================== 48 | * Implementing middlewares support 49 | * Renaming project 50 | 51 | 0.1.0 / 2013-11-16 52 | =================== 53 | * Initial commit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Anderson Arboleya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # executables 3 | ################################################################################ 4 | 5 | NPM_CHECK=node_modules/.bin/npm-check 6 | MVERSION=node_modules/.bin/mversion 7 | MOCHA=node_modules/.bin//mocha 8 | _MOCHA=node_modules/.bin//_mocha 9 | COVERALLS=node_modules/.bin/coveralls 10 | ISTANBUL=node_modules/.bin/istanbul 11 | SPACEJAM=node_modules/.bin/spacejam 12 | CODECLIMATE=node_modules/.bin/codeclimate 13 | 14 | ################################################################################ 15 | # variables 16 | ################################################################################ 17 | 18 | VERSION=`egrep -o '[0-9\.]{3,}' package.json -m 1` 19 | 20 | ################################################################################ 21 | # setup everything for development 22 | ################################################################################ 23 | 24 | setup: 25 | @npm install 26 | 27 | ################################################################################ 28 | # nodejs tests 29 | ################################################################################ 30 | 31 | # test code in nodejs 32 | test: 33 | @$(MOCHA) 34 | 35 | # test code in nodejs, and generates coverage 36 | test.coverage: 37 | @$(ISTANBUL) cover $(_MOCHA) 38 | 39 | # test code in nodejs, generates coverage and startup a simple server 40 | test.coverage.preview: test.coverage 41 | @cd coverage/lcov-report && python -m SimpleHTTPServer 8080 42 | 43 | # test code in nodejs, generates coverage and send it to coveralls 44 | test.coverage.coveralls: test.coverage 45 | @sed -i.bak \ 46 | "s/^.*ways\/lib/SF:lib/g" \ 47 | coverage/lcov.info 48 | 49 | @$(CODECLIMATE) < coverage/lcov.info 50 | @cat coverage/lcov.info | $(COVERALLS) 51 | 52 | ################################################################################ 53 | # meteor tests 54 | ################################################################################ 55 | 56 | # run tests and show output in browser 57 | test.meteor: 58 | meteor test-packages ./ 59 | 60 | # run tests and show output in terminal 61 | test.meteor.headless: 62 | @$(SPACEJAM) test-packages ./ 63 | 64 | ################################################################################ 65 | # more tests 66 | ################################################################################ 67 | 68 | test.all: test test.meteor.headless 69 | 70 | ################################################################################ 71 | # manages version bumps 72 | ################################################################################ 73 | 74 | bump.minor: 75 | @$(MVERSION) minor 76 | 77 | bump.major: 78 | @$(MVERSION) major 79 | 80 | bump.patch: 81 | @$(MVERSION) patch 82 | 83 | ################################################################################ 84 | # checking / updating dependencies 85 | ################################################################################ 86 | 87 | deps.check: 88 | @$(NPM_CHECK) 89 | 90 | deps.upgrade: 91 | @$(NPM_CHECK) -u 92 | 93 | ################################################################################ 94 | # publish / re-publish 95 | ################################################################################ 96 | 97 | publish: 98 | git tag -a $(VERSION) -m "Releasing $(VERSION)" 99 | git push origin master --tags 100 | npm publish 101 | meteor publish 102 | 103 | 104 | ################################################################################ 105 | # OTHERS 106 | ################################################################################ 107 | 108 | .PHONY: test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/arboleya/ways.svg)](https://travis-ci.org/arboleya/ways) 2 | [![Coverage Status](https://coveralls.io/repos/arboleya/ways/badge.svg?branch=master)](https://coveralls.io/r/arboleya/ways?branch=master) 3 | [![Code Climate](https://codeclimate.com/repos/553a5f77e30ba071310005d5/badges/b23591a0ab4283258afd/gpa.svg)](https://codeclimate.com/repos/553a5f77e30ba071310005d5/feed) 4 | [![Dependency Status](https://gemnasium.com/arboleya/ways.svg)](https://gemnasium.com/arboleya/ways) 5 | 6 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/ways-addressbar.svg?auth=fbb31316b6b3b70a62d6697c4ce14da3)](https://saucelabs.com/u/ways) 7 | 8 | # Ways 9 | 10 | Fluid router specially designed for [complex page transitions](#flow-mode) 11 | and [granular UI animations](#flow-mode). 12 | 13 | But not only that. 14 | 15 | 16 | - [Installation](#installation) 17 | - [Basics](#basics) 18 | - [AddressBar](#addressbar) 19 | - [Go](#go) 20 | - [Go Silent](#go-silent) 21 | - [Pathname](#pathname) 22 | - [Flow mode](#flow-mode) 23 | - [Activation](#activation) 24 | - [Signature changes](#signature-changes) 25 | - [Example](#example) 26 | - [Step 1](#step-1) 27 | - [Step 2](#step-2) 28 | - [Step 3](#step-3) 29 | - [Events](#events) 30 | - [Restricted urls](#restricted-urls) 31 | 32 | 33 | ## Installation 34 | 35 | ````shell 36 | # node 37 | npm install ways # --save, --save-dev 38 | 39 | # bower 40 | bower install ways # --save, --save-dev 41 | 42 | # meteor 43 | meteor add arboleya:ways # <- Ways is exported with a **capital** W! 44 | ```` 45 | 46 | ## Basics 47 | 48 | Basic signature is `ways(pattern, handler)`. 49 | 50 | > In **Meteor**, consider `Ways` is exported with a capital `W`. 51 | 52 | ````javascript 53 | var ways = require('ways'); 54 | 55 | // simple route 56 | ways('/pages', function(req){ 57 | // req.pattern, req.url, req.params 58 | }); 59 | 60 | // named params 61 | ways('/pages/:id', handler); 62 | 63 | // splat params 64 | ways('/pages/:id/tags/*tags', handler); 65 | 66 | // optional params 67 | ways('/pages/:id?', handler); 68 | ways('/pages/:id?/tags/*tags?', handler); 69 | 70 | // match-all 71 | ways('*', handler); 72 | 73 | // initialize with current url 74 | ways.go(ways.pathname()); 75 | ```` 76 | 77 | ### AddressBar 78 | 79 | By default `Ways` doesn't offers support for `/pushState` and `#hash`, there's 80 | no browser dependency whatsoever. Therefore you can use it wherever you want 81 | to, even on the server. Or keep in the client, but without affecting urls. 82 | 83 | However, sometimes you'll want to activate addressbar support, like this: 84 | 85 | ````javascript 86 | // activate addressbar support 87 | ways.use(ways.addressbar); 88 | ```` 89 | 90 | And you're done. 91 | 92 | 93 | ### Go 94 | 95 | Redirects app. 96 | ```javascript 97 | // ways.go(url, [title, [state]]); 98 | ways.go('/pages'); 99 | ways.go('/pages', 'Page Title'); 100 | ways.go('/pages', 'Page Title', {foo: 'bar'}); 101 | ```` 102 | 103 | ### Go Silent 104 | Same as `ways.go`, but in silent mode, without triggering any route. 105 | 106 | ```javascript 107 | // ways.go.silent(url, [title, [state]]); 108 | ways.go.silent('/pages'); 109 | ways.go.silent('/pages', 'Page Title'); 110 | ways.go.silent('/pages', 'Page Title', {foo: 'bar'}); 111 | ```` 112 | > Think about `go() = pushState`, `go.silent() = replaceState` 113 | 114 | ### Pathname 115 | 116 | Gets current pathname. 117 | 118 | ```javascript 119 | // ways.pathname(); 120 | ways.go(ways.pathname()); 121 | ```` 122 | 123 | ## Flow mode 124 | 125 | Connect your routes altogheter, creating a dependency graph between them. 126 | 127 | Lets say you have three routes: 128 | 129 | ````javascript 130 | ways('/a', function (req) { /* ... */ }); 131 | ways('/b', function (req) { /* ... */ }); 132 | ways('/c', function (req) { /* ... */ }); 133 | ```` 134 | 135 | Now lets assume that `/c` depends on `/b` that depends on `/a`. 136 | 137 | So when we call `/c`, we really want to execute: 138 | 139 | 1. First `/a` 140 | 1. Then `/b` 141 | 1. And finally `/c` 142 | 143 | That's what flow based mode would do for you. 144 | 145 | And more: 146 | 147 | * Routes' execution occurs asynchronously and sequentially 148 | * Dependency chain is computed automatically, no more routes' hell 149 | * Pack your projects with granular UI animations and complex page transitions 150 | * Forget the `Layout <- View` paradigm, embrace the `View <-> View` reality 151 | 152 | > TODO: Maybe explain wtf is `View <-> View` 153 | 154 | 155 | ### Activation 156 | 157 | The passed mode tell the order things should run. 158 | 159 | ```javascript 160 | // ways.flow(mode); 161 | ways.flow('destroy+run'); // destroy first, run after 162 | ways.flow('run+destroy'); // run before, destroy after 163 | ```` 164 | 165 | Don't panic, continue reading. 166 | 167 | ### Signature changes 168 | 169 | In `flow` mode, the routes can be run or destroyed and signature changes a 170 | little. You must pass two handlers instead of one: a `runner` and a `destroyer`. 171 | 172 | Optionally, you may also (*most probably*) pass a `dependency`. 173 | 174 | 175 | ````javascript 176 | // ways(pattern, run, destroy, [dependency]) 177 | 178 | var ways = require('ways'); 179 | 180 | ways.flow('destroy+run'); 181 | 182 | function run(req, done){ 183 | console.log('rendering', req); 184 | done(); 185 | } 186 | 187 | function destroy(req, done){ 188 | console.log('destroying', req); 189 | done(); 190 | } 191 | 192 | ways('/', run, destroy); 193 | ways('/pages/:id', run, destroy, '/'); // [1] 194 | ways('/pages/:id/edit', run, destroy, '/pages/:id'); // [2] 195 | 196 | // [1] 'pages/:id' depends on '/' 197 | // [2] '/pages/:id/edit' depends on '/pages/:id' 198 | ```` 199 | 200 | Both handlers (`run` and `destroy`) will receive two params when called: 201 | 202 | - `req` - infos about the request 203 | - `done`- callback to be called when route finishes running or destroying 204 | 205 | ### Example 206 | 207 | Lets take a look at a full example: 208 | 209 | ````javascript 210 | var ways = require('ways'); 211 | 212 | ways.flow('destroy+run'); 213 | 214 | var running = '+ RUN url=%s, pattern=%s, params=' 215 | var destroying = '- DESTROY url=%s, pattern=%s, params=' 216 | 217 | var run = function(req, done) { 218 | console.log(running, req.url, req.pattern, req.params); 219 | done(); 220 | }; 221 | 222 | var destroy = function(req, done) { 223 | console.log(destroying, req.url, req.pattern, req.params); 224 | done(); 225 | }; 226 | 227 | ways('/', run, destroy); 228 | ways('/pages', run, destroy, '/'); 229 | ways('/pages/:id', run, destroy, '/pages'); 230 | ways('/pages/:id/edit', run, destroy, '/pages/:id'); 231 | ways('*', run, destroy); // <- this is a catch all 232 | ```` 233 | 234 | Ok, now lets start our navigation: 235 | 236 | #### Step 1 237 | 238 | ````javascript 239 | // pretend our firt and current url is '/pages/33/edit', 240 | // we'll use `ways.pathname()` to get it 241 | 242 | ways.go(ways.pathname()); 243 | ```` 244 | 245 | This will produce the following output: 246 | 247 | ```` 248 | + RUN url='/', pattern='/', params= Object {} 249 | + RUN url='/pages', pattern='/pages', params= Object {} 250 | + RUN url='/pages/33', pattern='/pages/:id', params= Object {id: "33"} 251 | + RUN url='/pages/33/edit', pattern='/pages/:id/edit', params= Object {id: "33"} 252 | ```` 253 | > At the beggining there's no route to be destroyed, so the dependency chain is 254 | > computed and every route gets executed, one after another, asynchronously. 255 | 256 | #### Step 2 257 | 258 | ````javascript 259 | ways.go('/pages/22/edit'); 260 | ```` 261 | 262 | This will produce the following output: 263 | 264 | ```` 265 | - DESTROY url='/pages/33/edit', pattern='/pages/:id/edit', params= Object {id: "33"} 266 | - DESTROY url='/pages/33', pattern='/pages/:id', params= Object {id: "33"} 267 | + RUN url='/pages/22', pattern='/pages/:id', params= Object {id: "22"} 268 | + RUN url='/pages/22/edit', pattern='/pages/:id/edit', params= Object {id: "22"} 269 | ```` 270 | 271 | > Here we have two routes being destroyed before running the new ones, this is 272 | > computed again based on the dependency chain. In this case, useless routes are 273 | > `destroyed` before running the new ones, the opposite is achieved by passing 274 | > the mode `run+destroy`. 275 | 276 | #### Step 3 277 | 278 | ````javascript 279 | ways.go('/any/route/here'); 280 | ```` 281 | 282 | This will produce the following output: 283 | 284 | ```` 285 | - DESTROY url='/pages/22/edit', pattern='/pages/:id/edit', params= Object {id: "22"} 286 | - DESTROY url='/pages/22', pattern='/pages/:id', params= Object {id: "22"} 287 | - DESTROY url='/pages', pattern='/pages', params= Object {} 288 | - DESTROY url='/', pattern='/', params= Object {} 289 | + RUN url='/any/thing/here', pattern='*', params= Object {} 290 | ```` 291 | 292 | > As the route in question here has no dependencies, note that every other route 293 | > needs to be destroyed before it runs. 294 | 295 | 296 | ## Events 297 | 298 | There's only one global event you can listen to. 299 | 300 | ````javascript 301 | ways.on('url:change', function(url){ 302 | console.log('current url is', url); 303 | }); 304 | ```` 305 | 306 | ## Restricted urls 307 | 308 | A simple way to have restricted urls would be like: 309 | 310 | ````javascript 311 | function auth(done){ 312 | // your logic here 313 | done(true); 314 | } 315 | 316 | function restrict(action) { 317 | return function(req, done) { 318 | auth(function(authorized) { 319 | if(!authorized) 320 | ways.go('/login'); 321 | else 322 | action(req, done); 323 | }); 324 | } 325 | } 326 | 327 | ways('/pages/secret', restrict(run), destroy) 328 | ```` 329 | 330 | # License 331 | 332 | The MIT License (MIT) 333 | 334 | Copyright (c) 2013 Anderson Arboleya 335 | 336 | Permission is hereby granted, free of charge, to any person obtaining a copy of 337 | this software and associated documentation files (the "Software"), to deal in 338 | the Software without restriction, including without limitation the rights to 339 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 340 | the Software, and to permit persons to whom the Software is furnished to do so, 341 | subject to the following conditions: 342 | 343 | The above copyright notice and this permission notice shall be included in all 344 | copies or substantial portions of the Software. 345 | 346 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 347 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 348 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 349 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 350 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 351 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ways", 3 | "version": "0.5.0", 4 | "homepage": "https://github.com/arboleya/ways", 5 | "authors": [ 6 | "Anderson Arboleya " 7 | ], 8 | "description": "Fluid router specially designed for complex page transitions and granular UI animations", 9 | "main": "lib/ways.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "router", 17 | "flow", 18 | "fluid", 19 | "navigation", 20 | "chain", 21 | "transitions", 22 | "spa" 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests" 31 | ], 32 | "dependencies": { 33 | "happens": "~0.6.0", 34 | "ways-addressbar": "~0.2.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/flow.js: -------------------------------------------------------------------------------- 1 | var u = 'undefined', o = 'object'; 2 | (function (klass){ 3 | o === typeof exports ? module.exports = klass : 4 | o === typeof Package && Package.meteor ? WaysFlow = klass : 5 | this.WaysFlow = klass; 6 | })( 7 | _module( 8 | u !== typeof Happens ? Happens : require('happens'), 9 | u !== typeof WaysFluid ? WaysFluid : require('./fluid') 10 | ) 11 | ); 12 | 13 | 14 | function _module(Happens, Fluid) { 15 | 16 | 'use strict'; 17 | 18 | function Flow(routes, mode) { 19 | Happens(this); 20 | 21 | this.routes = routes; 22 | this.mode = mode; 23 | 24 | this.deads = []; 25 | this.actives = []; 26 | this.pendings = []; 27 | this.status = 'free' 28 | } 29 | 30 | Flow.prototype.run = function(url, route) { 31 | var flu, self = this; 32 | 33 | if( this.status == 'busy') 34 | this.actives.splice(-1, 1); 35 | 36 | this.emit('status:busy'); 37 | 38 | this.deads = []; 39 | this.pendings = []; 40 | 41 | flu = new Fluid(route, url); 42 | this.filter_pendings(flu); 43 | this.filter_deads(); 44 | 45 | this.status = 'busy'; 46 | if (this.mode === 'run+destroy') { 47 | this.run_pendings(function() { 48 | self.destroy_deads(function() { 49 | self.status = 'free'; 50 | self.emit('status:free', self.mode); 51 | }); 52 | }); 53 | } 54 | else if (this.mode === 'destroy+run') { 55 | this.destroy_deads(function() { 56 | self.run_pendings(function() { 57 | self.status = 'free'; 58 | self.emit('status:free', self.mode); 59 | }); 60 | }); 61 | } 62 | }; 63 | 64 | Flow.prototype.find_dependency = function(parent) { 65 | var route, flu; 66 | 67 | flu = find(this.actives, function(f) { 68 | return f.url === parent.dependency; 69 | }); 70 | if(flu != null) return flu; 71 | 72 | route = find(this.routes, function(r) { 73 | return r.matcher.test(parent.route.dependency); 74 | }); 75 | 76 | if(route != null) 77 | return new Fluid(route, parent.dependency); 78 | 79 | return null; 80 | }; 81 | 82 | Flow.prototype.filter_pendings = function(parent) { 83 | var err, flu, route, dep; 84 | 85 | this.pendings.unshift(parent); 86 | if (parent.dependency == null) 87 | return; 88 | 89 | if ((flu = this.find_dependency(parent)) != null) 90 | return this.filter_pendings(flu); 91 | 92 | route = parent.route.pattern; 93 | dep = parent.dependency 94 | err = "Dependency '" + dep + "' not found for route '" + route + "'"; 95 | 96 | throw new Error(err); 97 | }; 98 | 99 | Flow.prototype.filter_deads = function() { 100 | var flu, is_pending, i, len; 101 | 102 | for (i = 0, len = this.actives.length; i < len; i++) { 103 | 104 | flu = this.actives[i]; 105 | is_pending = find(this.pendings, function(f) { 106 | return f.url === flu.url; 107 | }); 108 | 109 | if (!is_pending) { 110 | this.deads.push(flu); 111 | } 112 | } 113 | }; 114 | 115 | Flow.prototype.run_pendings = function(done) { 116 | var flu, is_active, self = this; 117 | 118 | if (this.pendings.length === 0) return done(); 119 | 120 | flu = this.pendings.shift(); 121 | is_active = find(this.actives, function(f) { 122 | return f.url === flu.url; 123 | }); 124 | 125 | if (is_active) 126 | return this.run_pendings(done); 127 | 128 | this.actives.push(flu); 129 | this.emit('run:pending', flu.url); 130 | 131 | flu.run(function() { 132 | self.run_pendings(done); 133 | }); 134 | }; 135 | 136 | Flow.prototype.destroy_deads = function(done) { 137 | var flu, self = this; 138 | 139 | if (this.deads.length === 0) return done(); 140 | 141 | flu = this.deads.pop(); 142 | this.actives = reject(this.actives, function(f) { 143 | return f.url === flu.url; 144 | }); 145 | 146 | flu.destroy(function() { 147 | self.destroy_deads(done); 148 | }); 149 | }; 150 | 151 | function find(arr, filter) { 152 | for (var item, i = 0, len = arr.length; i < len; i++) { 153 | if (filter(item = arr[i])) { 154 | return item; 155 | } 156 | } 157 | }; 158 | 159 | function reject(arr, filter) { 160 | for (var item, copy = [], i = 0, len = arr.length; i < len; i++) { 161 | if (!filter(item = arr[i])) { 162 | copy.push(item); 163 | } 164 | } 165 | return copy; 166 | }; 167 | 168 | return Flow; 169 | }; -------------------------------------------------------------------------------- /lib/fluid.js: -------------------------------------------------------------------------------- 1 | var u = 'undefined', o = 'object'; 2 | (function (klass){ 3 | o === typeof exports ? module.exports = klass : 4 | o === typeof Package && Package.meteor ? WaysFluid = klass : 5 | this.WaysFluid = klass; 6 | })(_module()); 7 | 8 | function _module () { 9 | 10 | 'use strict'; 11 | 12 | function Fluid(route, url) { 13 | this.route = route; 14 | this.url = url; 15 | 16 | if(route.dependency) 17 | this.dependency = route.computed_dependency(url); 18 | } 19 | 20 | Fluid.prototype.run = function(done) { 21 | this.req = this.route.run(this.url, done); 22 | }; 23 | 24 | Fluid.prototype.destroy = function(done){ 25 | if(this.req) this.route.destroy(this.req, done); 26 | }; 27 | 28 | return Fluid; 29 | }; -------------------------------------------------------------------------------- /lib/way.js: -------------------------------------------------------------------------------- 1 | var u = 'undefined', o = 'object'; 2 | (function (klass){ 3 | o === typeof exports ? module.exports = klass : 4 | o === typeof Package && Package.meteor ? WaysWay = klass : 5 | this.WaysWay = klass; 6 | })(_module()); 7 | 8 | function _module() { 9 | 'use strict'; 10 | 11 | var _params_regex = { 12 | named: /:\w+/g, 13 | splat: /\*\w+/g, 14 | optional: /\/(?:\:|\*)(\w+)\?/g 15 | }; 16 | 17 | function Way(pattern, runner, destroyer, dependency) { 18 | 19 | this.matcher = null; 20 | this.pattern = pattern; 21 | this.runner = runner; 22 | this.destroyer = destroyer; 23 | this.dependency = dependency; 24 | 25 | var _params_regex = { 26 | named: /:\w+/g, 27 | splat: /\*\w+/g, 28 | optional: /\/(\:|\*)(\w+)\?/g 29 | }; 30 | 31 | if (pattern === '*') { 32 | this.matcher = /.*/; 33 | } else { 34 | this.matcher = pattern.replace(_params_regex.optional, '(?:\/)?$1$2?'); 35 | this.matcher = this.matcher.replace(_params_regex.named, '([^\/]+)'); 36 | this.matcher = this.matcher.replace(_params_regex.splat, '(.*?)'); 37 | this.matcher = new RegExp("^" + this.matcher + "$", 'm'); 38 | } 39 | }; 40 | 41 | Way.prototype.extract_params = function(url) { 42 | var name, names, params, vals, i, len; 43 | 44 | names = this.pattern.match(/(?::|\*)(\w+)/g); 45 | if (names == null) return {}; 46 | 47 | vals = url.match(this.matcher); 48 | params = {}; 49 | for (i = 0, len = names.length; i < len; i++) { 50 | name = names[i]; 51 | params[name.substr(1)] = vals[i+1]; 52 | } 53 | 54 | return params; 55 | }; 56 | 57 | Way.prototype.rewrite_pattern = function(pattern, url) { 58 | var key, value, reg, params; 59 | 60 | params = this.extract_params(url); 61 | for (key in params) { 62 | value = params[key]; 63 | reg = new RegExp("[\:\*]+" + key, 'g'); 64 | pattern = pattern.replace(reg, value); 65 | } 66 | return pattern; 67 | }; 68 | 69 | Way.prototype.computed_dependency = function(url) { 70 | return this.rewrite_pattern(this.dependency, url); 71 | }; 72 | 73 | Way.prototype.run = function(url, done) { 74 | var req = { 75 | url: url, 76 | pattern: this.pattern, 77 | params: this.extract_params(url) 78 | }; 79 | this.runner(req, done); 80 | return req; 81 | }; 82 | 83 | Way.prototype.destroy = function(req, done) { 84 | this.destroyer(req, done); 85 | }; 86 | 87 | return Way; 88 | }; -------------------------------------------------------------------------------- /lib/ways.js: -------------------------------------------------------------------------------- 1 | var u = 'undefined', o = 'object'; 2 | 3 | (function (klass){ 4 | o === typeof exports ? module.exports = klass : 5 | u !== typeof Meteor ? Ways = klass : 6 | this.Ways = klass; 7 | })( 8 | _module.apply(null, [ 9 | u !== typeof Happens? Happens : require('happens'), 10 | u !== typeof WaysWay ? WaysWay : require('./way'), 11 | u !== typeof WaysFlow ? WaysFlow : require('./flow'), 12 | u !== typeof Meteor && Meteor.isClient ? WaysAddressBar : 13 | u !== typeof Meteor ? null : require('ways-addressbar') 14 | ]) 15 | ); 16 | 17 | 18 | function _module (Happens, Way, Flow, AddresssBar) { 19 | 'use strict'; 20 | 21 | // Config 22 | var flow = null; 23 | var mode = null; 24 | var plugin = null; 25 | var routes = []; 26 | 27 | var current_pathname = null; 28 | 29 | Happens(Ways); 30 | 31 | /** 32 | * Sets up a new route 33 | * @param {String} pattern Pattern string 34 | * @param {Function} runner Route's action runner 35 | * @param {Function} destroyer Optional, Route's action destroyer (flow mode) 36 | * @param {String} dependency Optional, specifies a dependency by pattern 37 | */ 38 | function Ways(pattern, runner, destroyer, dependency){ 39 | 40 | if(flow && arguments.length < 3) 41 | throw new Error('In `flow` mode you must to pass at least 3 args.'); 42 | 43 | var route = new Way(pattern, runner, destroyer, dependency); 44 | routes.push(route); 45 | 46 | return route; 47 | } 48 | 49 | Ways.flow = function (m){ 50 | routes = []; 51 | if((mode = m) != null) 52 | flow = new Flow(routes, mode); 53 | }; 54 | 55 | Ways.use = function(plug){ 56 | plugin = new plug; 57 | plugin.on('url:change', plugin_url_change); 58 | }; 59 | 60 | Ways.pathname = function(){ 61 | if(plugin) 62 | return plugin.pathname(); 63 | else 64 | return current_pathname; 65 | }; 66 | 67 | Ways.go = function(url, title, state){ 68 | if(plugin) 69 | plugin.push(url, title, state); 70 | else 71 | dispatch(url); 72 | }; 73 | 74 | Ways.go.silent = function(url, title, state){ 75 | if(plugin) 76 | plugin.replace(url, title, state); 77 | }; 78 | 79 | Ways.reset = function(){ 80 | if(plugin) 81 | plugin.off('url:change', plugin_url_change) 82 | 83 | flow = null; 84 | mode = null; 85 | plugin = null; 86 | routes = []; 87 | 88 | current_pathname = null; 89 | }; 90 | 91 | Ways.addressbar = AddresssBar || { 92 | error: 'addressbar plugin can be used only in the client' 93 | }; 94 | 95 | function plugin_url_change(url){ 96 | dispatch(plugin.pathname()); 97 | } 98 | 99 | function dispatch(url) { 100 | var i, route, url = '/' + url.replace(/^[\/]+|[\/]+$/mg, ''); 101 | 102 | for(i in routes) 103 | if((route = routes[i]).matcher.test(url)){ 104 | Ways.emit("url:change", current_pathname = url); 105 | return (flow ? flow.run(url, route) : route.run(url)); 106 | } 107 | 108 | throw new Error("Route not found for url '"+ url +"'"); 109 | }; 110 | 111 | return Ways; 112 | }; -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var name = 'arboleya:ways'; 2 | 3 | Package.describe({ 4 | name: name, 5 | version: '0.5.0', 6 | summary: 'Fluid router specially designed for complex page transitions and granular UI animations', 7 | git: 'https://github.com/arboleya/ways', 8 | documentation: 'README.md' 9 | }); 10 | 11 | Package.onUse(function(api) { 12 | api.versionsFrom('1.0'); 13 | api.use('arboleya:happens@0.6.0'); 14 | api.use('arboleya:ways-addressbar@0.2.1'); 15 | api.export('Ways'); 16 | api.addFiles('lib/fluid.js'); 17 | api.addFiles('lib/flow.js'); 18 | api.addFiles('lib/way.js'); 19 | api.addFiles('lib/ways.js'); 20 | }); 21 | 22 | Package.onTest(function (api) { 23 | api.use(name); 24 | api.use('tinytest'); 25 | api.use('arboleya:ways'); 26 | api.addFiles('test/meteor.js'); 27 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ways", 3 | "version": "0.5.0", 4 | "description": "Fluid router specially designed for complex page transitions and granular UI animations", 5 | "author": "Anderson Arboleya ", 6 | "directories": { 7 | "lib": "./lib" 8 | }, 9 | "engines": { 10 | "node": ">= 0.10" 11 | }, 12 | "keywords": [ 13 | "router", 14 | "flow", 15 | "fluid", 16 | "navigation", 17 | "chain", 18 | "transitions", 19 | "spa" 20 | ], 21 | "main": "./lib/ways", 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/arboleya/ways" 25 | }, 26 | "scripts": { 27 | "test": "make test" 28 | }, 29 | "dependencies": { 30 | "happens": "0.6.0", 31 | "ways-addressbar": "^0.2.1" 32 | }, 33 | "devDependencies": { 34 | "chai": "^2.2.0", 35 | "codeclimate-test-reporter": "0.0.4", 36 | "coveralls": "^2.11.2", 37 | "istanbul": "^0.3.13", 38 | "mocha": "^2.2.4", 39 | "mversion": "^1.10.0", 40 | "npm-check": "^3.2.10", 41 | "spacejam": "^1.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/adapters.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'), 2 | happens = require('happens'), 3 | should = require('chai').should(); 4 | 5 | function Adapter() { 6 | happens(this); 7 | } 8 | 9 | Adapter.prototype.url = null; 10 | Adapter.prototype.state = null; 11 | Adapter.prototype.title = null; 12 | 13 | Adapter.prototype.pathname = function() { 14 | return this.url; 15 | }; 16 | 17 | Adapter.prototype.push = function(url, title, state) { 18 | this.url = url; 19 | this.title = title; 20 | this.state = state; 21 | this.emit('url:change'); 22 | }; 23 | 24 | Adapter.prototype.replace = function(url, title, state) { 25 | this.url = url; 26 | this.title = title; 27 | this.state = state; 28 | }; 29 | 30 | 31 | describe('[adapters]', function() { 32 | it('should make proper use of adapters', function(done) { 33 | 34 | var requests = [ 35 | {url: '/pages/33/edit', pattern: '/pages/:id/edit', params: {id: '33'}}, 36 | {url: '/pages', pattern: '/pages', params: {}}, 37 | {url: '/pages/33', pattern: '/pages/:id', params: {id: '33'}}, 38 | {url: '/', pattern: '/', params: {}} 39 | ]; 40 | 41 | var out = { 42 | log: function(type, req) { 43 | type.should.equal('run'); 44 | req.should.deep.equal(requests.shift()); 45 | if (requests.length === 0) { 46 | out.log = null; 47 | done(); 48 | } 49 | } 50 | }; 51 | 52 | var run = function(req) { 53 | out.log('run', req); 54 | }; 55 | 56 | ways.reset(); 57 | ways.use(Adapter); 58 | 59 | ways('/', run); 60 | ways('/pages', run); 61 | ways('/pages/:id', run); 62 | ways('/pages/:id/edit', run); 63 | ways('/no/dep', run); 64 | 65 | ways.go.silent('/pages/33/edit'); // <- shouldn't do anything! 66 | ways.go('/pages/33/edit'); 67 | 68 | should.exist(ways.pathname()); 69 | ways.pathname().should.equal('/pages/33/edit'); 70 | 71 | ways.go('/pages'); 72 | ways.go('/pages/33'); 73 | 74 | ways.go('/'); 75 | }); 76 | 77 | }); -------------------------------------------------------------------------------- /test/events.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'); 2 | var should = require('chai').should(); 3 | 4 | describe('[events]', function(){ 5 | 6 | before(function(){ 7 | ways.reset(); 8 | ways('/pages', function(){}); 9 | }); 10 | 11 | var url_changed; 12 | function url_change(url){ 13 | url_changed = url; 14 | } 15 | 16 | it('should dispatch an event when the url changes', function(done) { 17 | ways.on('url:change', url_change); 18 | ways.go('/pages'); 19 | url_changed.should.equal('/pages'); 20 | done(); 21 | }); 22 | 23 | it('should not dispatch event after listener is removed', function(done) { 24 | url_changed = 'none'; 25 | ways.off('url:change', url_change); 26 | ways.go('/pages'); 27 | url_changed.should.equal('none'); 28 | done(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/flow-destroy-run.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'), 2 | should = require('chai').should(); 3 | 4 | describe('[flow-mode] destroy+run', function() { 5 | 6 | var out = null, 7 | run = null, 8 | destroy = null; 9 | 10 | before(function() { 11 | 12 | out = {}; 13 | 14 | run = function(req, done) { 15 | if (out != null) { 16 | out.log('run', req); 17 | } 18 | done(); 19 | }; 20 | 21 | destroy = function(req, done) { 22 | if (out != null) { 23 | out.log('destroy', req); 24 | } 25 | done(); 26 | }; 27 | 28 | ways.reset(); 29 | ways.flow('destroy+run'); 30 | ways('/', run, destroy); 31 | ways('/pages', run, destroy, '/'); 32 | ways('/pages/:id', run, destroy, '/pages'); 33 | ways('/pages/:id/edit', run, destroy, '/pages/:id'); 34 | ways('/no/dep', run, destroy, '/this/does/not/exist'); 35 | }); 36 | 37 | 38 | it('should run route with param from scratch', function(done) { 39 | 40 | var requests = [ 41 | {url: '/', pattern: '/', params: {}}, 42 | {url: '/pages', pattern: '/pages', params: {}}, 43 | {url: '/pages/33',pattern: '/pages/:id', params: {id: '33'}}, 44 | {url: '/pages/33/edit',pattern: '/pages/:id/edit', params: {id: '33'}} 45 | ]; 46 | 47 | out.log = function(type, req) { 48 | type.should.equal('run'); 49 | req.should.deep.equal(requests.shift()); 50 | if (requests.length === 0) { 51 | out.log = null; 52 | done(); 53 | } 54 | }; 55 | 56 | ways.go('/pages/33/edit'); 57 | }); 58 | 59 | it('should destroy deads and run pendings', function(done) { 60 | var requests, types; 61 | types = 'destroy destroy run run'.split(' '); 62 | requests = [ 63 | {url: '/pages/33/edit',pattern: '/pages/:id/edit',params: {id: '33'}}, 64 | {url: '/pages/33',pattern: '/pages/:id',params: {id: '33'}}, 65 | {url: '/pages/22',pattern: '/pages/:id',params: {id: '22'}}, 66 | {url: '/pages/22/edit',pattern: '/pages/:id/edit',params: {id: '22'} 67 | } 68 | ]; 69 | 70 | out.log = function(type, req) { 71 | type.should.equal(types.shift()); 72 | req.should.deep.equal(requests.shift()); 73 | if (requests.length === 0) { 74 | out.log = null; 75 | done(); 76 | } 77 | }; 78 | ways.go('/pages/22/edit'); 79 | }); 80 | 81 | 82 | it('should error on route not found', function() { 83 | var msg = "Route not found for url '/this/route/does/not/exist'"; 84 | try { 85 | ways.go('/this/route/does/not/exist'); 86 | } catch (err) { 87 | err.message.should.equal(msg); 88 | } 89 | }); 90 | 91 | it('should error on dependency not found', function() { 92 | var msg = "Dependency '/this/does/not/exist' not found for route '/no/dep'"; 93 | try { 94 | ways.go('/no/dep'); 95 | } catch (err) { 96 | err.message.should.equal(msg); 97 | } 98 | }); 99 | }); -------------------------------------------------------------------------------- /test/flow-interruption.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'), 2 | should = require('chai').should(); 3 | 4 | describe('[flow-interruption] destroy+run', function() { 5 | 6 | var out = null, 7 | run = null, 8 | destroy = null; 9 | 10 | 11 | before(function() { 12 | out = {}; 13 | run = function(req, done) { 14 | if (out != null) { 15 | out.log('run', req); 16 | } 17 | if (req.url === '/pages') { 18 | ways.go('/login'); 19 | } 20 | done(); 21 | }; 22 | 23 | destroy = function(req, done) { 24 | if (out != null) { 25 | out.log('destroy', req); 26 | } 27 | done(); 28 | }; 29 | }); 30 | 31 | 32 | it('should interrupt current flow and run new (run+destroy)', function(done){ 33 | 34 | ways.reset(); 35 | ways.flow('destroy+run'); 36 | 37 | ways('/', run, destroy); 38 | ways('/pages', run, destroy, '/'); 39 | ways('/pages/:id', run, destroy, '/pages'); 40 | ways('/login', run, destroy); 41 | 42 | var requests = [ 43 | {url: '/', pattern: '/', params: {}}, 44 | {url: '/pages', pattern: '/pages', params: {}}, 45 | {url: '/login', pattern: '/login', params: {}} 46 | ]; 47 | 48 | out.log = function(type, req) { 49 | type.should.equal('run'); 50 | req.should.deep.equal(requests.shift()); 51 | 52 | if (requests.length === 0) { 53 | out.log = null; 54 | done(); 55 | } 56 | }; 57 | ways.go('/pages/33'); 58 | }); 59 | 60 | 61 | it('should interrupt current flow starts new (destroy+run)', function(done){ 62 | 63 | ways.reset(); 64 | ways.flow('run+destroy'); 65 | 66 | ways('/', run, destroy); 67 | ways('/pages', run, destroy, '/'); 68 | ways('/pages/:id', run, destroy, '/pages'); 69 | ways('/auth', run, destroy); 70 | ways('/login', run, destroy, '/auth'); 71 | 72 | requests = [ 73 | {url: '/', pattern: '/', params: {}}, 74 | {url: '/pages', pattern: '/pages', params: {}}, 75 | {url: '/auth', pattern: '/auth', params: {}}, 76 | {url: '/login', pattern: '/login', params: {}} 77 | ]; 78 | 79 | out.log = function(type, req) { 80 | type.should.equal('run'); 81 | req.should.deep.equal(requests.shift()); 82 | if (requests.length === 0) { 83 | out.log = null; 84 | done(); 85 | } 86 | }; 87 | 88 | ways.go('/pages/33'); 89 | }); 90 | 91 | }); -------------------------------------------------------------------------------- /test/flow-run-destroy.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'), 2 | should = require('chai').should(); 3 | 4 | describe('[flow-mode] run+destroy', function() { 5 | var out = null, 6 | run = null, 7 | destroy = null; 8 | 9 | before(function() { 10 | out = {}; 11 | run = function(req, done) { 12 | if (out != null) { 13 | out.log('run', req); 14 | } 15 | done(); 16 | }; 17 | destroy = function(req, done) { 18 | if (out != null) { 19 | out.log('destroy', req); 20 | } 21 | done(); 22 | }; 23 | 24 | ways.reset(); 25 | ways.flow('run+destroy'); 26 | ways('/', run, destroy); 27 | ways('/pages', run, destroy, '/'); 28 | ways('/pages/:id', run, destroy, '/pages'); 29 | ways('/pages/:id/edit', run, destroy, '/pages/:id'); 30 | ways('/no/dep', run, destroy, '/this/does/not/exist'); 31 | 32 | try { 33 | ways('/null', function() {}); 34 | } catch (err) { 35 | var error_msg = "In `flow` mode you must to pass at least 3 args."; 36 | err.message.should.equal(error_msg); 37 | } 38 | }); 39 | 40 | 41 | it('should run route with param from scratch', function(done) { 42 | var requests = [ 43 | {url: '/', pattern: '/', params: {} }, 44 | {url: '/pages', pattern: '/pages', params: {}}, 45 | {url: '/pages/33', pattern: '/pages/:id', params: {id: '33'}}, 46 | {url: '/pages/33/edit',pattern: '/pages/:id/edit',params: {id: '33'}} 47 | ]; 48 | 49 | out.log = function(type, req) { 50 | type.should.equal('run'); 51 | req.should.deep.equal(requests.shift()); 52 | if (requests.length === 0) { 53 | out.log = null; 54 | done(); 55 | } 56 | }; 57 | 58 | ways.go('/pages/33/edit'); 59 | }); 60 | 61 | 62 | it('should run pendings and destroy deads', function(done) { 63 | var request, types = 'run run destroy destroy'.split(' '); 64 | 65 | requests = [ 66 | {url: '/pages/22', pattern: '/pages/:id', params: {id: '22'}}, 67 | {url: '/pages/22/edit', pattern: '/pages/:id/edit', params: {id: '22'}}, 68 | {url: '/pages/33/edit', pattern: '/pages/:id/edit', params: {id: '33'}}, 69 | {url: '/pages/33', pattern: '/pages/:id', params: {id: '33'}} 70 | ]; 71 | 72 | out.log = function(type, req) { 73 | type.should.equal(types.shift()); 74 | req.should.deep.equal(requests.shift()); 75 | if (requests.length === 0) { 76 | out.log = null; 77 | done(); 78 | } 79 | }; 80 | 81 | ways.go('/pages/22/edit'); 82 | }); 83 | 84 | 85 | it('should error on route not found', function() { 86 | var msg = "Route not found for url '/this/route/does/not/exist'"; 87 | try { 88 | ways.go('/this/route/does/not/exist'); 89 | } catch (err) { 90 | err.message.should.equal(msg); 91 | } 92 | }); 93 | 94 | 95 | it('should error on dependency not found', function() { 96 | var msg = "Dependency '/this/does/not/exist' not found for route '/no/dep'"; 97 | try { 98 | ways.go('/no/dep'); 99 | } catch (err) { 100 | err.message.should.equal(msg); 101 | } 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/matchall.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'), 2 | should = require('chai').should(); 3 | 4 | describe('[match-all]', function() { 5 | var out = null, 6 | run = null, 7 | destroy = null; 8 | 9 | before(function() { 10 | out = {}; 11 | run = function(req, done) { 12 | if (out != null) { 13 | out.log('run', req); 14 | } 15 | done(); 16 | }; 17 | destroy = function(req, done) { 18 | if (out != null) { 19 | out.log('destroy', req); 20 | } 21 | done(); 22 | }; 23 | 24 | ways.reset(); 25 | ways.flow('destroy+run'); 26 | ways('/', run, destroy); 27 | ways('*', run, destroy, '/'); 28 | }); 29 | 30 | 31 | it('should run match-all route', function(done) { 32 | var requests = [ 33 | {url: '/', pattern: '/', params: {}}, 34 | {url: '/anything', pattern: '*', params: {}} 35 | ]; 36 | 37 | out.log = function(type, req) { 38 | type.should.equal('run'); 39 | req.should.deep.equal(requests.shift()); 40 | if (requests.length === 0) { 41 | out.log = null; 42 | done(); 43 | } 44 | }; 45 | 46 | ways.go('/anything'); 47 | }); 48 | }); -------------------------------------------------------------------------------- /test/meteor.js: -------------------------------------------------------------------------------- 1 | if('undefined' !== typeof Tinytest){ 2 | 3 | Tinytest.add('Ways', function (test) { 4 | test.isNotNull(Ways, { 5 | message: 'Expect `Ways` to be defined' 6 | }); 7 | }); 8 | } -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui bdd 2 | --reporter spec -------------------------------------------------------------------------------- /test/no-flow.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'), 2 | should = require('chai').should(); 3 | 4 | describe('[no-flow-mode]', function(){ 5 | 6 | var out = null, 7 | run = null, 8 | destroy = null; 9 | 10 | before(function(){ 11 | 12 | out = {} 13 | run = function(req) { 14 | out.log('run', req); 15 | } 16 | 17 | ways.reset() 18 | ways('/', run); 19 | ways('/pages', run); 20 | ways('/pages/:id', run); 21 | ways('/pages/:id/edit', run); 22 | ways('/no/dep', run); 23 | }); 24 | 25 | it('should execute routes in the order they are called', function(done) { 26 | 27 | var requests = [ 28 | {url: '/pages/33/edit', pattern: '/pages/:id/edit', params: {id:'33'}}, 29 | {url: '/pages', pattern: '/pages', params: {}}, 30 | {url: '/pages/33', pattern: '/pages/:id', params: {id:'33'}}, 31 | {url: '/', pattern: '/', params: {}} 32 | ]; 33 | 34 | out.log = function(type, req) { 35 | type.should.equal('run'); 36 | req.should.deep.equal(requests.shift()); 37 | if(requests.length === 0) { 38 | out.log = null; 39 | done(); 40 | } 41 | }; 42 | 43 | // replace shouldn't do anything 44 | ways.go.silent('/pages/33/edit'); 45 | 46 | ways.go('/pages/33/edit'); 47 | ways.go('/pages'); 48 | ways.go('/pages/33'); 49 | ways.go('/'); 50 | }); 51 | }); -------------------------------------------------------------------------------- /test/pathname.js: -------------------------------------------------------------------------------- 1 | var ways = require('../lib/ways'); 2 | var should = require('chai').should(); 3 | 4 | describe('[pathname]', function(){ 5 | 6 | before(function(){ 7 | var fn = function(){}; 8 | 9 | ways.reset(); 10 | ways.flow('destroy+run'); 11 | 12 | ways('/', fn, fn); 13 | ways('/a', fn, fn, '/'); 14 | ways('/b', fn, fn, '/a'); 15 | }); 16 | 17 | it('pathname should be null at startup', function(done) { 18 | should.not.exist(ways.pathname()); 19 | done(); 20 | }); 21 | 22 | it('pathname should match the current url after startup', function(done) { 23 | ways.go('/b'); 24 | should.exist(ways.pathname()); 25 | ways.pathname().should.equal('/b'); 26 | done(); 27 | }); 28 | }); --------------------------------------------------------------------------------