├── .DS_Store ├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── History.md ├── Readme.md ├── package.json └── src ├── index.js ├── link.js ├── types.js └── yarr.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmn/yarr/81ae7dccb371d54226bf5ede304fc202189cfba7/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-flow-strip-types", "transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": ["standard", "standard-react"], 9 | "plugins": [ 10 | "react", 11 | "babel" 12 | ], 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "rules": { 17 | }, 18 | "globals": { 19 | "Object": false, 20 | "Symbol": false, 21 | "Function": false, 22 | "Boolean": false, 23 | "Number": false, 24 | "Array": false, 25 | "String": false, 26 | "RegExp": false, 27 | "Date": false, 28 | "Error": false, 29 | "valError": false, 30 | "RangeError": false, 31 | "ReferenceError": false, 32 | "SyntaxError": false, 33 | "TypeError": false, 34 | "URIError": false, 35 | "JSON": false, 36 | "Map": false, 37 | "WeakMap": false, 38 | "Set": false, 39 | "Promise": false, 40 | "ArrayBuffer": false, 41 | "ArrayBufferView": false, 42 | "Int8Array": false, 43 | "Uint8Array": false, 44 | "Uint8ClampedArray": false, 45 | "Int16Array": false, 46 | "Uint16Array": false, 47 | "Int32Array": false, 48 | "Uint32Array": false, 49 | "Float32Array": false, 50 | "Float64Array": false, 51 | "DataView": false, 52 | "boolean": false, 53 | "string": false, 54 | "void": false, 55 | "any": false, 56 | "mixed": false, 57 | "number": false, 58 | "ReactComponent": false, 59 | "React$Element": false, 60 | "SyntheticEvent": false, 61 | "SyntheticClipboardEvent": false, 62 | "SyntheticCompositionEvent": false, 63 | "SyntheticInputEvent": false, 64 | "SyntheticUIEvent": false, 65 | "SyntheticFocusEvent": false, 66 | "SyntheticKeyboardEvent": false, 67 | "SyntheticMouseEvent": false, 68 | "SyntheticDragEvent": false, 69 | "SyntheticWheelEvent": false, 70 | "SyntheticTouchEvent": false, 71 | "Class": false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/babel.* 3 | /node_modules/fbjs.* 4 | /node_modules/json5.* 5 | 6 | [include] 7 | 8 | [libs] 9 | 10 | [options] 11 | esproposal.class_instance_fields=enable 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | testing 3 | .jshint 4 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .flowconfig 3 | .gitignore 4 | History.md 5 | .eslintrc 6 | .babelrc -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2.0.0 / 2016-04-19 2 | ================== 3 | 4 | * Add Flow types 5 | * Clean up type errors 6 | * Update dependencies 7 | 8 | 1.3.7 / 2013-09-09 9 | ================== 10 | 11 | * fix removal of fragment 12 | 13 | 1.3.6 / 2013-03-12 14 | ================== 15 | 16 | * fix links with target attribute 17 | 18 | 1.3.5 / 2013-02-12 19 | ================== 20 | 21 | * fix ctrl/cmd/shift clicks 22 | 23 | 1.3.4 / 2013-02-04 24 | ================== 25 | 26 | * add tmp .show() dispatch argument 27 | * add keywords to component.json 28 | 29 | 1.3.3 / 2012-12-14 30 | ================== 31 | 32 | * remove + support from path regexps 33 | 34 | 1.3.2 / 2012-11-26 35 | ================== 36 | 37 | * add explicit "#" check 38 | * add `window` to `addEventListener` calls 39 | 40 | 1.3.1 / 2012-09-21 41 | ================== 42 | 43 | * fix: onclick only when e.which == 1 44 | 45 | 1.3.0 / 2012-08-29 46 | ================== 47 | 48 | * add `page(fn)` support. Closes #27 49 | * add component.json 50 | * fix tests 51 | * fix examples 52 | 53 | 1.2.1 / 2012-08-02 54 | ================== 55 | 56 | * add transitions example 57 | * add exposing of `Context` and `Route` constructors 58 | * fix infinite loop issue unhandled paths containing query-strings 59 | 60 | 1.2.0 / 2012-07-05 61 | ================== 62 | 63 | * add `ctx.pathname` 64 | * add `ctx.querystring` 65 | * add support for passing a query-string through the dispatcher [ovaillancourt] 66 | * add `.defaultPrevented` support, ignoring page.js handling [ovaillancourt] 67 | 68 | 1.1.3 / 2012-06-18 69 | ================== 70 | 71 | * Added some basic client-side tests 72 | * Fixed initial dispatch in Firefox 73 | * Changed: no-op on subsequent `page()` calls. Closes #16 74 | 75 | 1.1.2 / 2012-06-13 76 | ================== 77 | 78 | * Fixed origin portno bug preventing :80 and :443 from working properly 79 | * Fixed: prevent cyclic refreshes. Closes #17 80 | 81 | 1.1.1 / 2012-06-11 82 | ================== 83 | 84 | * Added enterprisejs example 85 | * Added: join base for `.canonicalPath`. Closes #12 86 | * Fixed `location.origin` usage [fisch42] 87 | * Fixed `pushState()` when unhandled 88 | 89 | 1.1.0 / 2012-06-06 90 | ================== 91 | 92 | * Added `+` support to pathtoRegexp() 93 | * Added `page.base(path)` support 94 | * Added dispatch option to `page()`. Closes #10 95 | * Added `Context#originalPath` 96 | * Fixed unhandled links when .base is present. Closes #11 97 | * Fixed: `Context#path` to "/" 98 | 99 | 0.0.2 / 2012-06-05 100 | ================== 101 | 102 | * Added `make clean` 103 | * Added some mocha tests 104 | * Fixed: ignore fragments 105 | * Fixed: do not pushState on initial load 106 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![yarr logo](http://naman.s3.amazonaws.com/yarr.png) 2 | 3 | Yet Another React Router. 4 | (forked from Page.js a tiny ~1200 byte Express-inspired client-side router) 5 | 6 | ```js 7 | var yarr = require('yarr.js'); 8 | 9 | yarr('/', index) 10 | yarr('/user/:user', show) 11 | yarr('/user/:user/edit', edit) 12 | yarr('/user/:user/album', album) 13 | yarr('/user/:user/album/sort', sort) 14 | yarr('*', notfound) 15 | yarr() 16 | ``` 17 | 18 | [![Pair on this](https://tf-assets-staging.s3.amazonaws.com/badges/thinkful_repo_badge.svg)](http://start.thinkful.com/react/?utm_source=github&utm_medium=badge&utm_campaign=yarr) 19 | 20 | ## examples 21 | 22 | In order to provide suitable example, the original examples for page.js have been removed. New examples will be added to reflect the slightly modified behaviour. 23 | **Please Note**: Unlike in Page.js, you have to use yarr.Link for all links in React app for routing to work correctly. The Link Component has the same API as a normal tag. 24 | 25 | ## Changelog 26 | 27 | #### v1.2.0 28 | - *Link* now uses React-Tappable behind the scenes. This means that you will get fast click responses on touch screens. The API on the outside remains unchanged. 29 | - *Link* now accepts any props that are accepted by React-Tappable. You can set the `component` attribute to use an element other than an `` element. This includes custom React Classes. I have also submitted a pull request to React-Tappable that adds support for arbitrary props. I will update to the latest React-Tappable when that code is merged. 30 | - The previous version of `Yarr.js` only made the `Link` component available with a lowercase `l`. This is no longer the case. `Link` in capital case is now available as well. This means you can import the `Link` component in one of these ways: 31 | 32 | ``` 33 | var Link = require('yarr.js').Link; 34 | var {Link} = require('yarr.js'); 35 | import {Link} from 'yarr.js'; 36 | ``` 37 | 38 | ## API 39 | 40 | ### yarr(path, callback[, callback ...]) 41 | 42 | Defines a route mapping `path` to the given `callback(s)`. 43 | 44 | ```js 45 | yarr('/', user.list) 46 | yarr('/user/:id', user.load, user.show) 47 | yarr('/user/:id/edit', user.load, user.edit) 48 | yarr('*', notfound) 49 | ``` 50 | 51 | Then with your react code use the yarr.link for any internal routable links. 52 | 53 | ```js 54 | var Link = require('yarr').link; 55 | 56 | //use it as follows: 57 | Link({href:'/my-route'}, "click here"); 58 | 59 | //or in JSX: 60 | click here 61 | ``` 62 | 63 | ### yarr(callback) 64 | 65 | This is equivalent to `yarr('*', callback)` for generic "middleware". 66 | 67 | ### yarr(path) 68 | 69 | Navigate to the given `path`. 70 | 71 | ```js 72 | yarr('/user/12') 73 | ``` 74 | 75 | ### yarr.show(path) 76 | 77 | Identical to `yarr(path)` above. 78 | 79 | ### yarr([options]) 80 | 81 | Register yarr's `popstate` bindings. The following options are available: 82 | 83 | - `popstate` bind to popstate [true] 84 | - `dispatch` perform initial dispatch [true] 85 | 86 | If you wish to load serve initial content 87 | from the server you likely will want to 88 | set `dispatch` to __false__. 89 | 90 | ### yarr.start([options]) 91 | 92 | Identical to `yarr([options])` above. 93 | 94 | ### yarr.stop() 95 | 96 | Unbind both the `popstate` and `click` handlers. 97 | 98 | ### yarr.base([path]) 99 | 100 | Get or set the base `path`. For example if yarr.js 101 | is operating within "/blog/*" set the base path to "/blog". 102 | 103 | ### Context 104 | 105 | Routes are passed `Context` objects, these may 106 | be used to share state, for example `ctx.user =`, 107 | as well as the history "state" `ctx.state` that 108 | the `pushState` API provides. 109 | 110 | #### Context#save() 111 | 112 | Saves the context using `replaceState()`. For example 113 | this is useful for caching HTML or other resources 114 | that were loaded for when a user presses "back". 115 | 116 | #### Context#canonicalPath 117 | 118 | Pathname including the "base" (if any) and query string "/admin/login?foo=bar". 119 | 120 | #### Context#path 121 | 122 | Pathname and query string "/login?foo=bar". 123 | 124 | #### Context#querystring 125 | 126 | Query string void of leading `?` such as "foo=bar", defaults to "". 127 | 128 | #### Context#pathname 129 | 130 | The pathname void of query string "/login". 131 | 132 | #### Context#state 133 | 134 | The `pushState` state object. 135 | 136 | #### Context#title 137 | 138 | The `pushState` title. 139 | 140 | ## Routing 141 | 142 | The router uses the same string-to-regexp conversion 143 | that Express does, so things like ":id", ":id?", and "*" work 144 | as you might expect. 145 | 146 | Another aspect that is much like Express is the ability to 147 | pass multiple callbacks. You can use this to your advantage 148 | to flatten nested callbacks, or simply to abstract components. 149 | 150 | ### Separating concerns 151 | 152 | For example suppose you had a route to _edit_ users, and a 153 | route to _view_ users. In both cases you need to load the user. 154 | One way to achieve this is with several callbacks as shown here: 155 | 156 | ```js 157 | yarr('/user/:user', load, show) 158 | yarr('/user/:user/edit', load, edit) 159 | ``` 160 | 161 | Using the `*` character we could alter this to match all 162 | routes prefixed with "/user" to achieve the same result: 163 | 164 | ```js 165 | yarr('/user/*', load) 166 | yarr('/user/:user', show) 167 | yarr('/user/:user/edit', edit) 168 | ``` 169 | 170 | Likewise `*` may be used as catch-alls after all routes 171 | acting as a 404 handler, before all routes, in-between and 172 | so on. For example: 173 | 174 | ```js 175 | yarr('/user/:user', load, show) 176 | yarr('*', function(){ 177 | // render not found 178 | }) 179 | ``` 180 | 181 | ### Default 404 behaviour 182 | 183 | By default when a route is not matched, 184 | yarr.js will invoke `yarr.stop()` to unbind 185 | itself, and proceed with redirecting to the 186 | location requested. This means you may use 187 | yarr.js with a multi-page application _without_ 188 | explicitly binding to certain links. 189 | 190 | ### Working with parameters and contexts 191 | 192 | Much like `request` and `response` objects are 193 | passed around in Express, yarr.js has a single 194 | "Context" object. Using the previous examples 195 | of `load` and `show` for a user, we can assign 196 | arbitrary properties to `ctx` to maintain state 197 | between callbacks. 198 | 199 | First to build a `load` function that will load 200 | the user for subsequent routes you'll need to 201 | access the ":id" passed. You can do this with 202 | `ctx.params.NAME` much like Express: 203 | 204 | ```js 205 | function load(ctx, next){ 206 | var id = ctx.params.id 207 | } 208 | ``` 209 | 210 | Then perform some kind of action against the server, 211 | assigning the user to `ctx.user` for other routes to 212 | utilize. `next()` is then invoked to pass control to 213 | the following matching route in sequence, if any. 214 | 215 | ```js 216 | function load(ctx, next){ 217 | var id = ctx.params.id 218 | $.getJSON('/user/' + id + '.json', function(user){ 219 | ctx.user = user 220 | next() 221 | }) 222 | } 223 | ``` 224 | 225 | The "show" function might look something like this, 226 | however you may render templates or do anything you 227 | want. Note that here `next()` is _not_ invoked, because 228 | this is considered the "end point", and no routes 229 | will be matched until another link is clicked or 230 | `yarr(path)` is called. 231 | 232 | ```js 233 | function show(ctx){ 234 | React.renderComponent({user:ctx.user.name}, document.body); 235 | } 236 | ``` 237 | 238 | Finally using them like so: 239 | 240 | ```js 241 | yarr('/user/:id', load, show) 242 | ``` 243 | 244 | ### Working with state 245 | 246 | When working with the `pushState` API, 247 | and thus yarr.js you may optionally provide 248 | state objects available when the user navigates 249 | the history. 250 | 251 | For example if you had a photo application 252 | and you performed a relatively expensive 253 | search to populate a list of images, 254 | normally when a user clicks "back" in 255 | the browser the route would be invoked 256 | and the query would be made yet-again. 257 | 258 | Perhaps the route callback looks like this: 259 | 260 | ```js 261 | function show(ctx){ 262 | $.getJSON('/photos', function(images){ 263 | displayImages(images) 264 | }) 265 | } 266 | ``` 267 | 268 | You may utilize the history's state 269 | object to cache this result, or any 270 | other values you wish. This makes it 271 | possible to completely omit the query 272 | when a user presses back, providing 273 | a much nicer experience. 274 | 275 | ```js 276 | function show(ctx){ 277 | if (ctx.state.images) { 278 | displayImages(ctx.state.images) 279 | } else { 280 | $.getJSON('/photos', function(images){ 281 | ctx.state.images = images 282 | ctx.save() 283 | displayImages(images) 284 | }) 285 | } 286 | } 287 | ``` 288 | 289 | __NOTE__: `ctx.save()` must be used 290 | if the state changes _after_ the first 291 | tick (xhr, setTimeout, etc), otherwise 292 | it is optional and the state will be 293 | saved after dispatching. 294 | 295 | ### Matching paths 296 | 297 | Here are some examples of what's possible 298 | with the string to `RegExp` conversion. 299 | 300 | Match an explicit path: 301 | 302 | ```js 303 | yarr('/about', callback) 304 | ``` 305 | 306 | Match with required parameter accessed via `ctx.params.name`: 307 | 308 | ```js 309 | yarr('/user/:name', callback) 310 | ``` 311 | 312 | Match with several params, for example `/user/tj/edit` or 313 | `/user/tj/view`. 314 | 315 | ```js 316 | yarr('/user/:name/:operation', callback) 317 | ``` 318 | 319 | Match with one optional and one required, now `/user/tj` 320 | will match the same route as `/user/tj/show` etc: 321 | 322 | ```js 323 | yarr('/user/:name/:operation?', callback) 324 | ``` 325 | 326 | Use the wildcard char `*` to match across segments, 327 | available via `ctx.params[N]` where __N__ is the 328 | index of `*` since you may use several. For example 329 | the following will match `/user/12/edit`, `/user/12/albums/2/admin` 330 | and so on. 331 | 332 | ```js 333 | yarr('/user/*', loadUser) 334 | ``` 335 | 336 | Named wildcard accessed, for example `/file/javascripts/jquery.js` 337 | would provide "/javascripts/jquery.js" as `ctx.params.file`: 338 | 339 | ```js 340 | yarr('/file/:file(*)', loadUser) 341 | ``` 342 | 343 | And of course `RegExp` literals, where the capture 344 | groups are available via `ctx.params[N]` where __N__ 345 | is the index of the capture group. 346 | 347 | ```js 348 | yarr(/^\/commits\/(\d+)\.\.(\d+)/, loadUser) 349 | ``` 350 | 351 | ### Pull Requests 352 | 353 | * Break commits into a single objective. 354 | * An objective should be a chunk of code that is related but requires explaination. 355 | * Commits should be in the form of what-it-is: how-it-does-it and or why-it's-needed or what-it-is for trivial changes 356 | * Pull requests and commits should be a guide to the code. 357 | 358 | In specific I would love: 359 | * Tests 360 | * Examples 361 | * Bug Fixes 362 | 363 | ## License 364 | 365 | (The MIT License) 366 | 367 | Copyright (c) 2014 Naman Goel <naman34@gmail.com> 368 | 369 | Original Page.js by: 370 | TJ Holowaychuk <tj@vision-media.ca> 371 | 372 | Permission is hereby granted, free of charge, to any person obtaining 373 | a copy of this software and associated documentation files (the 374 | 'Software'), to deal in the Software without restriction, including 375 | without limitation the rights to use, copy, modify, merge, publish, 376 | distribute, sublicense, and/or sell copies of the Software, and to 377 | permit persons to whom the Software is furnished to do so, subject to 378 | the following conditions: 379 | 380 | The above copyright notice and this permission notice shall be 381 | included in all copies or substantial portions of the Software. 382 | 383 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 384 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 385 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 386 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 387 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 388 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 389 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 390 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarr.js", 3 | "description": "Yet Another React Router (~1200 bytes)", 4 | "version": "2.0.1", 5 | "scripts": { 6 | "build": "babel src -d lib", 7 | "cpFlow": "for f in `pwd`/src/*; do cp $f `pwd`/lib/$(basename $f).flow; done", 8 | "prepublish": "npm run build && npm run cpFlow" 9 | }, 10 | "keywords": [ 11 | "page", 12 | "route", 13 | "router", 14 | "routes", 15 | "pushState", 16 | "yarr", 17 | "history", 18 | "react" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/nmn/yarr.git" 23 | }, 24 | "main": "lib/index.js", 25 | "devDependencies": { 26 | "babel-cli": "^6.7.5", 27 | "babel-eslint": "^6.0.3", 28 | "babel-plugin-syntax-class-properties": "^6.5.0", 29 | "babel-plugin-transform-class-properties": "^6.6.0", 30 | "babel-plugin-transform-flow-strip-types": "^6.7.0", 31 | "babel-preset-es2015": "^6.6.0", 32 | "babel-preset-react": "^6.5.0", 33 | "eslint": "^2.8.0", 34 | "eslint-config-standard": "^5.1.0", 35 | "eslint-config-standard-react": "^2.3.0", 36 | "eslint-plugin-promise": "^1.1.0", 37 | "eslint-plugin-react": "^5.0.1", 38 | "eslint-plugin-standard": "^1.3.2" 39 | }, 40 | "peerDependencies": { 41 | "react": "^0.14.0 || ^15.0.0", 42 | "react-dom": "^0.14.0 || ^15.0.0" 43 | }, 44 | "dependencies": { 45 | "react-tappable": "^0.8.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import yarr from './yarr' 2 | import L from './link' 3 | 4 | Object.defineProperty(yarr, '__esModule', { 5 | value: true 6 | }) 7 | 8 | yarr.default = yarr 9 | yarr.Link = L 10 | 11 | module.exports = yarr 12 | -------------------------------------------------------------------------------- /src/link.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, {PropTypes} from 'react' 3 | import yarr from './yarr' 4 | import Tappable from 'react-tappable' 5 | 6 | import type {ReactKeyboardEvent} from './types' 7 | 8 | type Props = { 9 | onClick(evt: Object): boolean; 10 | href: string; 11 | component?: string; 12 | }; 13 | 14 | type RouteFn = (event: ReactKeyboardEvent) => ?boolean; 15 | 16 | const modifierKeyPressed = event => event.getModifierState && ( 17 | event.getModifierState('Shift') || 18 | event.getModifierState('Alt') || 19 | event.getModifierState('Control') || 20 | event.getModifierState('Meta') || 21 | event.button > 1 22 | ) 23 | 24 | export default class Link extends React.Component { 25 | 26 | route: RouteFn = (event) => { 27 | if (modifierKeyPressed(event)) { 28 | return null 29 | } 30 | event.nativeEvent && event.preventDefault && event.preventDefault() 31 | 32 | let shouldRoute = true 33 | let ret = true 34 | 35 | if (this.props.onClick) { 36 | var evt = {} 37 | evt.preventDefault = function () { 38 | shouldRoute = false 39 | } 40 | 41 | ret = this.props.onClick(evt) 42 | } 43 | 44 | shouldRoute = ret === false ? false : shouldRoute 45 | 46 | if (!shouldRoute) { 47 | return null 48 | } 49 | 50 | if (this.props.href) { 51 | yarr.show(this.props.href) 52 | } 53 | 54 | return false 55 | }; 56 | 57 | routeOnEnter: RouteFn = (e) => { 58 | if (e.keyCode === 13) { 59 | this.route(e) 60 | } 61 | }; 62 | 63 | onClick: RouteFn = (event) => { 64 | if (modifierKeyPressed(event)) { 65 | return 66 | } 67 | event.nativeEvent && event.preventDefault && event.preventDefault() 68 | }; 69 | 70 | render (): React$Element { 71 | const props = Object.assign({}, this.props, { 72 | onTap: this.route, 73 | pressDelay: 500, 74 | moveThreshold: 5, 75 | component: this.props.component || 'a', 76 | onClick: this.onClick, 77 | onKeyUp: this.routeOnEnter 78 | }) 79 | 80 | return 81 | } 82 | 83 | } 84 | 85 | Link.displayName = 'Link' 86 | Link.propTypes = { 87 | onClick: PropTypes.func, 88 | href: PropTypes.string, 89 | component: PropTypes.string 90 | } 91 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | type ModifierKeys = 'Shift' 3 | | 'Alt' 4 | | 'Control' 5 | | 'Meta' 6 | 7 | export type ReactKeyboardEvent = { 8 | getModifierState: (key: ModifierKeys) => boolean; 9 | button: number; 10 | nativeEvent: Object; 11 | keyCode: number; 12 | preventDefault(): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/yarr.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /** 3 | * Perform initial dispatch. 4 | */ 5 | 6 | let dispatch = true 7 | 8 | /** 9 | * Base path. 10 | */ 11 | 12 | let base: string = '' 13 | 14 | /** 15 | * Running flag. 16 | */ 17 | 18 | let running 19 | 20 | /** 21 | * Register `path` with callback `fn()`, 22 | * or route `path`, or `yarr.start()`. 23 | * 24 | * yarr(fn); 25 | * yarr('*', fn); 26 | * yarr('/user/:id', load, user); 27 | * yarr('/user/' + user.id, { some: 'thing' }); 28 | * yarr('/user/' + user.id); 29 | * yarr(); 30 | * 31 | * @param {String|Function} path 32 | * @param {Function} fn... 33 | * @api public 34 | */ 35 | 36 | function yarr(path: string | Function, fn?: Function): void { 37 | // 38 | if (typeof path === 'function') { 39 | return yarr('*', path) 40 | } 41 | 42 | // route to 43 | if (typeof path === 'string' && typeof fn === 'function') { 44 | const route = new Route(path) 45 | for (let i = 1; i < arguments.length; ++i) { 46 | yarr.callbacks.push(route.middleware(arguments[i])) 47 | } 48 | // show with [state] 49 | } else if (typeof path === 'string') { 50 | yarr.show(path, fn) 51 | // start [options] 52 | } else { 53 | yarr.start(path) 54 | } 55 | } 56 | 57 | /** 58 | * Callback functions. 59 | */ 60 | 61 | yarr.callbacks = [] 62 | 63 | /** 64 | * Get or set basepath to `path`. 65 | * 66 | * @param {String} path 67 | * @api public 68 | */ 69 | 70 | yarr.base = function(path: string): ?string { 71 | if (arguments.length === 0) return base 72 | base = path 73 | } 74 | 75 | /** 76 | * Bind with the given `options`. 77 | * 78 | * Options: 79 | * 80 | * - `click` bind to click events [true] 81 | * - `popstate` bind to popstate [true] 82 | * - `dispatch` perform initial dispatch [true] 83 | * 84 | * @param {Object} options 85 | * @api public 86 | */ 87 | 88 | type YarrOptions = { 89 | click?: boolean, 90 | popstate?: boolean, 91 | dispatch?: boolean 92 | }; 93 | 94 | yarr.start = function(options: YarrOptions | any): void { 95 | options = options || {} 96 | if (running) return 97 | running = true 98 | if (options.dispatch === false) dispatch = false 99 | if (options.popstate !== false) window.addEventListener('popstate', onpopstate, false) 100 | if (!dispatch) return 101 | const url = location.pathname + location.search + location.hash 102 | yarr.replace(url, null, true, dispatch) 103 | } 104 | 105 | /** 106 | * Unbind click and popstate event handlers. 107 | * 108 | * @api public 109 | */ 110 | 111 | yarr.stop = function(): void { 112 | running = false 113 | global.removeEventListener('popstate', onpopstate, false) 114 | } 115 | 116 | /** 117 | * Show `path` with optional `state` object. 118 | * 119 | * @param {String} path 120 | * @param {Object} state 121 | * @param {Boolean} dispatch 122 | * @return {Context} 123 | * @api public 124 | */ 125 | 126 | yarr.show = function(path: string, state: any, dispatch?: boolean): Context { 127 | const ctx = new Context(path, state) 128 | if (dispatch !== false) yarr.dispatch(ctx) 129 | if (!ctx.unhandled) ctx.pushState() 130 | return ctx 131 | } 132 | 133 | /** 134 | * Replace `path` with optional `state` object. 135 | * 136 | * @param {String} path 137 | * @param {Object} state 138 | * @return {Context} 139 | * @api public 140 | */ 141 | 142 | yarr.replace = function(path: string, state: ?Object, init: ?boolean, dispatch: ?boolean): Context { 143 | const ctx = new Context(path, state) 144 | ctx.init = Boolean(init) 145 | if (dispatch == null) dispatch = true 146 | if (dispatch) yarr.dispatch(ctx) 147 | ctx.save() 148 | return ctx 149 | } 150 | 151 | /** 152 | * Dispatch the given `ctx`. 153 | * 154 | * @param {Object} ctx 155 | * @api private 156 | */ 157 | 158 | yarr.dispatch = function(ctx: Context): void { 159 | var i = 0 160 | 161 | function next() { 162 | var fn = yarr.callbacks[i++] 163 | if (!fn) return unhandled(ctx) 164 | fn(ctx, next) 165 | } 166 | 167 | next() 168 | } 169 | 170 | /** 171 | * Unhandled `ctx`. When it's not the initial 172 | * popstate then redirect. If you wish to handle 173 | * 404s on your own use `yarr('*', callback)`. 174 | * 175 | * @param {Context} ctx 176 | * @api private 177 | */ 178 | 179 | function unhandled(ctx: Context): void { 180 | const current = window.location.pathname + window.location.search 181 | if (current === ctx.canonicalPath) return 182 | yarr.stop() 183 | ctx.unhandled = true 184 | window.location = ctx.canonicalPath 185 | } 186 | 187 | /** 188 | * Initialize a new "request" `Context` 189 | * with the given `path` and optional initial `state`. 190 | * 191 | * @param {String} path 192 | * @param {Object} state 193 | * @api public 194 | */ 195 | class Context { 196 | init: boolean; 197 | canonicalPath: string; 198 | path: string; 199 | title: string; 200 | state: any; 201 | querystring: string; 202 | pathname: string; 203 | params: Array; 204 | hash: string; 205 | constructor(path: string, state: any) { 206 | if (path[0] === '/' && path.indexOf(base) !== 0) { 207 | path = base + path 208 | } 209 | 210 | const i = path.indexOf('?') 211 | 212 | this.canonicalPath = path 213 | this.path = path.replace(base, '') || '/' 214 | 215 | this.title = global.document.title 216 | this.state = state || {} 217 | this.state.path = path 218 | this.querystring = ~i ? path.slice(i + 1) : '' 219 | this.pathname = ~i ? path.slice(0, i) : path 220 | this.params = [] 221 | 222 | // fragment 223 | this.hash = '' 224 | if (~this.path.indexOf('#')) { 225 | const parts = this.path.split('#') 226 | this.path = parts[0] 227 | this.hash = parts[1] || '' 228 | this.querystring = this.querystring.split('#')[0] 229 | } 230 | } 231 | pushState(): void { 232 | history.pushState(this.state, this.title, this.canonicalPath) 233 | } 234 | save(): void { 235 | history.replaceState(this.state, this.title, this.canonicalPath) 236 | } 237 | } 238 | yarr.Context = Context 239 | 240 | /** 241 | * Initialize `Route` with the given HTTP `path`, 242 | * and an array of `callbacks` and `options`. 243 | * 244 | * Options: 245 | * 246 | * - `sensitive` enable case-sensitive routes 247 | * - `strict` enable strict matching for trailing slashes 248 | * 249 | * @param {String} path 250 | * @param {Object} options. 251 | * @api private 252 | */ 253 | 254 | type RouteOptions = { 255 | sensitive?: boolean; 256 | strict?: boolean; 257 | }; 258 | type KeyType = { 259 | name: number; 260 | optional: boolean; 261 | }; 262 | 263 | class Route { 264 | path: string; 265 | method: 'GET'; 266 | keys: Array; 267 | regexp: RegExp; 268 | constructor(path: string, options: RouteOptions = {}) { 269 | const {sensitive, strict} = options 270 | this.path = path 271 | this.method = 'GET' 272 | this.keys = [] 273 | this.regexp = pathtoRegexp(path, this.keys, Boolean(sensitive), Boolean(strict)) 274 | } 275 | 276 | /** 277 | * Return route middleware with 278 | * the given callback `fn()`. 279 | * 280 | * @param {Function} fn 281 | * @return {Function} 282 | * @api public 283 | */ 284 | middleware(fn: (ctx: Context, next: Function) => void): (ctx: Context, next: Function) => void { 285 | return (ctx, next) => { 286 | if (this.match(ctx.path, ctx.params)) { 287 | return fn(ctx, next) 288 | } 289 | next() 290 | } 291 | } 292 | 293 | /** 294 | * Check if this route matches `path`, if so 295 | * populate `params`. 296 | * 297 | * @param {String} path 298 | * @param {Array} params 299 | * @return {Boolean} 300 | * @api private 301 | */ 302 | match(path: string, params: Array): boolean { 303 | const keys = this.keys 304 | const qsIndex = path.indexOf('?') 305 | const pathname = ~qsIndex ? path.slice(0, qsIndex) : path 306 | const m = this.regexp.exec(decodeURIComponent(pathname)) 307 | 308 | if (!m) return false 309 | 310 | for (let i = 1, len = m.length; i < len; ++i) { 311 | const key = keys[i - 1] 312 | 313 | const val = typeof m[i] === 'string' 314 | ? decodeURIComponent(m[i]) 315 | : m[i] 316 | 317 | if (key) { 318 | params[key.name] = undefined !== params[key.name] 319 | ? params[key.name] 320 | : val 321 | } else { 322 | params.push(val) 323 | } 324 | } 325 | 326 | return true 327 | } 328 | } 329 | 330 | yarr.Route = Route 331 | 332 | /** 333 | * Normalize the given path string, 334 | * returning a regular expression. 335 | * 336 | * An empty array should be passed, 337 | * which will contain the placeholder 338 | * key names. For example "/user/:id" will 339 | * then contain ["id"]. 340 | * 341 | * @param {String|RegExp|Array} path 342 | * @param {Array} keys 343 | * @param {Boolean} sensitive 344 | * @param {Boolean} strict 345 | * @return {RegExp} 346 | * @api private 347 | */ 348 | 349 | function pathtoRegexp(path: string | RegExp | Array, keys: Array, sensitive: boolean, strict: boolean): RegExp { 350 | if (path instanceof RegExp) return path 351 | if (path instanceof Array) path = '(' + path.join('|') + ')' 352 | path = path 353 | .concat(strict ? '' : '/?') 354 | .replace(/\/\(/g, '(?:/') 355 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) { 356 | keys.push({ name: key, optional: !!optional }) 357 | slash = slash || '' 358 | return '' + 359 | (optional ? '' : slash) + 360 | '(?:' + 361 | (optional ? slash : '') + 362 | (format || '') + 363 | (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + 364 | (optional || '') 365 | }) 366 | .replace(/([\/.])/g, '\\$1') 367 | .replace(/\*/g, '(.*)') 368 | 369 | return sensitive ? new RegExp(`^${path}$`) : new RegExp(`^${path}$`, 'i') 370 | } 371 | 372 | /** 373 | * Handle "populate" events. 374 | */ 375 | 376 | function onpopstate(e: Object): void { 377 | if (e.state) { 378 | var path = e.state.path 379 | yarr.replace(path, e.state) 380 | } 381 | } 382 | 383 | /** 384 | * Expose `yarr`. 385 | */ 386 | module.exports = yarr 387 | --------------------------------------------------------------------------------