├── .gitignore ├── LICENSE ├── README.md ├── app ├── controller.js └── main.js ├── build └── index.html ├── index.js ├── package.json ├── test.js ├── tests └── reactive-router-test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Christian Alfoni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reactive-router 2 | A reactive wrapper around Page JS 3 | 4 | > **The URL is like any other state** 5 | 6 | ## DEPRECATED 7 | I have decided to deprecate this project, the reason being that we have developed [addressbar](https://github.com/christianalfoni/addressbar) and [url-mapper](https://github.com/christianalfoni/url-mapper). These projects encourages to move routing to something else than deciding what views/components to render. Typically this is related to producing state instead, but I think we can do a lot of cool stuff with urls. One example of what these projects has made us able to do is shown in [cerebral-router](https://github.com/cerebral/cerebral-router). 8 | 9 | ## What makes reactive-router different? 10 | When the router moved to the frontend it has been given a lot of different jobs. Keep track of and parse the current url, control the VIEW layer and often do data-fetching and handling transition states. The reactive-router is going back to the roots of what a router does, and that is pass a URL request to the controller layer of your application. 11 | 12 | Some more info 13 | - [Article: Why we are doing MVC and FLUX wrong](http://www.christianalfoni.com/articles/2015_08_02_Why-we-are-doing-MVC-and-FLUX-wrong) 14 | - [Video: reactive-router](https://www.youtube.com/watch?v=6tUbnDHq8xs) 15 | - [Video: Cerebral - A state controller with its own debugger](https://www.youtube.com/watch?v=xCIv4-Q2dtA) 16 | 17 | ## Application state, not URLs 18 | > Your VIEW layer should not care about what the URL is, it should care about what state your application is in 19 | 20 | The way to think about the **reactive-router** is this: 21 | 22 | 1. A url triggers and the reactive-router triggers a related method on your controller layer, it being action creators or some other state changing controller 23 | 2. Your **controller** layer converts this request to application state. An example of this would be `/inbox` which puts your application in `{currentFolder: 'inbox'}` 24 | 3. Your **view** layer does not check the url to figure out what to render, it checks the `currentFolder` state 25 | 26 | What this means is that you stop thinking about your UI as a reflection of the URLs, because it does not matter. What matters is the state you want to put your application in. A URL is just a way to trigger some state, it being setting what components to render, what filters to set, what item in a list to highlight etc. 27 | 28 | First of all this allows you to trigger a url change by just changing the "url" state inside your state store. It also allows you to define what a url-change actually means. It does not have to be changing out components, it could be highlighting something in a list or trigger some animation. 29 | 30 | ## How does it work? 31 | reactive-router is a wrapper around [pagejs](https://visionmedia.github.io/page.js/), a neat little routing library built by **visionmedia**. 32 | 33 | ```js 34 | import ReactiveRouter from 'reactive-router'; 35 | import state from './state.js'; 36 | 37 | // state can be a store, controller, actions or whatever is responsible 38 | // for changing the state of your app. This example is with a state store 39 | 40 | // Actions like these can be a lot more generic, but it is just to show you 41 | const homeRouted = function (context) { 42 | state.set('url', context.path); 43 | state.set('currentPage', 'home'); 44 | }; 45 | 46 | const messageRouted = function (context) { 47 | state.set('url', context.path); 48 | state.set('currentPage', 'messages'); 49 | state.set('currentMessage', context.params.id); 50 | }; 51 | 52 | const errorRouted = function (context) { 53 | state.set('url', context.path); 54 | state.set('currentPage', 'error'); 55 | }; 56 | 57 | /* 58 | ROUTER 59 | The way you define routes is changed. Pass one object to define all routes. 60 | Second argument is any Page JS options 61 | */ 62 | const router = ReactiveRouter({ 63 | '/home': homeRouted, 64 | '/messages/:id': messageRouted, 65 | '/error': errorRouted 66 | }, { 67 | hashbang: true 68 | }); 69 | 70 | // Listen to state changes and set the url 71 | state.on('change', function (state) { 72 | router.set(state.url); 73 | // or silently set, will not trigger the callback 74 | router.setSilent(state.url); 75 | }); 76 | ``` 77 | 78 | ## Why listen to state changes and set the url? 79 | If you are familiar with React, you can compare this to an input. Even though the input/router is what caused the change, we want to store the state (value/url) and bring it right back to the input/router. The reason is that now we can manually change the input/router value/url inside our state store and it will be reflected in the UI, as you can see an examples of with the actions above. To change a url you can trigger your own "change url" signal, or just change the url normally with a hyperlink. 80 | 81 | #### Handle nesting 82 | ```js 83 | @State({currentPage: ['currentPage']) 84 | const Comp = React.createClass({ 85 | render() { 86 | switch (this.props.currentPage) { 87 | case 'home': 88 | return ; 89 | case 'messages': 90 | return 91 | case 'error': 92 | return 93 | } 94 | } 95 | }); 96 | ``` 97 | 98 | #### Trigger a route 99 | ```js 100 | const Comp = React.createClass({ 101 | render() { 102 | return ( 103 | Open message 123 104 | ); 105 | } 106 | }); 107 | ``` 108 | 109 | #### Trigger route with state change 110 | Now you can change the route from within your actions/controller and the router will react to that. 111 | ```js 112 | import ajax from './state.js'; 113 | import state from './state.js'; 114 | 115 | const someAction = function () { 116 | ajax.post('/something') 117 | .resolve(function (data) { 118 | state.set('data', data); 119 | state.set('url', '/data'); 120 | }) 121 | .catch(function (error) { 122 | state.set('error', error); 123 | state.set('url', '/error'); 124 | }); 125 | }; 126 | ``` 127 | -------------------------------------------------------------------------------- /app/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from 'cerebral-react-immutable-store'; 2 | 3 | const state = { 4 | url: '/foo', 5 | messageId: null 6 | }; 7 | 8 | const defaultArgs = { 9 | 10 | }; 11 | 12 | const controller = Controller(state, defaultArgs); 13 | 14 | export default controller; 15 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | import controller from './controller.js'; 2 | import {Mixin} from 'cerebral-react-immutable-store'; 3 | import React from 'react'; 4 | import Router from './../index.js'; 5 | 6 | const Messages = React.createClass({ 7 | mixins: [Mixin], 8 | getStatePaths() { 9 | return { 10 | url: ['url'], 11 | messageId: ['messageId'] 12 | }; 13 | }, 14 | render() { 15 | return ( 16 |
17 |

Messages!

18 | { 19 | this.state.messageId ? 20 | this.state.messageId : 21 | null 22 | } 23 |
24 | ); 25 | } 26 | }); 27 | 28 | const App = React.createClass({ 29 | mixins: [Mixin], 30 | getStatePaths() { 31 | return { 32 | url: ['url'] 33 | }; 34 | }, 35 | render() { 36 | return ( 37 |
38 | Foo 39 | this.signals.urlChanged({path: '/foo/456'})}>Bar 40 |
41 | 42 |
43 |
44 | ); 45 | } 46 | }); 47 | 48 | controller.signal('indexRouted', function fooRouted (args, state) { 49 | state.set('url', args.path); 50 | }); 51 | 52 | controller.signal('fooRouted', function fooRouted (args, state) { 53 | state.set('url', args.path); 54 | state.set('messageId', null); 55 | }); 56 | 57 | controller.signal('barRouted', function barRouted (args, state) { 58 | state.set('url', args.path); 59 | }); 60 | 61 | controller.signal('messageRouted', function messageRoutedd (args, state) { 62 | state.set('url', args.path); 63 | state.set('messageId', args.params.id); 64 | }); 65 | 66 | controller.signal('urlChanged', function urlChanged (args, state) { 67 | state.set('url', args.path); 68 | }); 69 | 70 | const router = Router({ 71 | '/': controller.signals.indexRouted, 72 | '/foo': controller.signals.fooRouted, 73 | '/bar': controller.signals.barRouted, 74 | '/foo/:id': controller.signals.messageRouted 75 | }, { 76 | hashbang: true 77 | }); 78 | 79 | controller.eventEmitter.on('change', function (state) { 80 | router.set(state.url); 81 | }); 82 | 83 | controller.eventEmitter.on('remember', function (state) { 84 | router.setSilent(state.url); 85 | }); 86 | 87 | React.render(controller.injectInto(App), document.body); 88 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var page = require('page'); 3 | 4 | var Router = function (routes, options) { 5 | 6 | var isSilent = false; 7 | 8 | // register the routes 9 | Object.keys(routes).map(function (route) { 10 | page(route, function () { 11 | !isSilent && routes[route].apply(this, arguments); 12 | }); 13 | }); 14 | 15 | // start the router 16 | page(options || {}); 17 | 18 | // export functions 19 | return { 20 | set: function (url) { 21 | if (page.current !== url) { 22 | page(url); 23 | } 24 | }, 25 | setSilent: function (url) { 26 | isSilent = true; 27 | this.set(url); 28 | isSilent = false; 29 | } 30 | }; 31 | 32 | }; 33 | 34 | module.exports = Router; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-router", 3 | "version": "0.4.1", 4 | "description": "A reactive wrapper around Page JS", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "webpack-dev-server --devtool eval --progress --colors --content-base build", 11 | "test": "node test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/christianalfoni/reactive-router.git" 16 | }, 17 | "keywords": [ 18 | "router", 19 | "react", 20 | "state", 21 | "reactive" 22 | ], 23 | "author": "Christian Alfoni", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/christianalfoni/reactive-router/issues" 27 | }, 28 | "homepage": "https://github.com/christianalfoni/reactive-router", 29 | "devDependencies": { 30 | "babel-core": "^5.8.3", 31 | "babel-loader": "^5.3.2", 32 | "cerebral-react-immutable-store": "^0.2.1", 33 | "immutable-store": "^0.5.1", 34 | "nodeunit": "^0.9.1", 35 | "react": "^0.13.3", 36 | "webpack": "^1.10.5", 37 | "webpack-dev-server": "^1.10.1" 38 | }, 39 | "dependencies": { 40 | "page": "^1.6.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var reporter = require('nodeunit').reporters.default; 2 | 3 | process.chdir(__dirname); 4 | 5 | var run_tests = new Array(); 6 | var tests_available = { 7 | 'signals' : 'tests/signals.js', 8 | 'store': 'tests/store.js', 9 | 'recorder': 'tests/recorder.js' 10 | }; 11 | 12 | var test_name; 13 | 14 | // Check if any arguments were provided to the script 15 | if(process.argv.length > 2) { 16 | var i; 17 | // For each argument, treat it as the name of a test 18 | for(i = 2; i < process.argv.length; i++) { 19 | test_name = process.argv[i]; 20 | if(tests_available.hasOwnProperty(test_name)) { 21 | // Add the test to the list of tests to run 22 | run_tests.push(tests_available[test_name]); 23 | } else { 24 | console.log("Invalid test '" + test_name + "'"); 25 | } 26 | } 27 | } else { 28 | // No arguments provided to the script, so we run all the tests 29 | for(test_name in tests_available) { 30 | if(tests_available.hasOwnProperty(test_name)) { 31 | run_tests.push(tests_available[test_name]); 32 | } 33 | } 34 | } 35 | 36 | // Tell the reporter to run the tests 37 | if(run_tests.length > 0) { 38 | reporter.run(run_tests); 39 | } 40 | -------------------------------------------------------------------------------- /tests/reactive-router-test.js: -------------------------------------------------------------------------------- 1 | exports['should '] = function () { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var node_modules = path.resolve(__dirname, 'node_modules'); 3 | 4 | var config = { 5 | entry: path.resolve(__dirname, 'app/main.js'), 6 | devtool: 'eval-source-map', 7 | output: { 8 | filename: 'bundle.js' 9 | }, 10 | resolve: { 11 | alias: { 12 | 'controller': path.resolve(__dirname, 'index.js') 13 | } 14 | }, 15 | module: { 16 | loaders: [{ 17 | test: /\.js$/, 18 | loader: 'babel', 19 | exclude: node_modules 20 | }] 21 | } 22 | }; 23 | 24 | module.exports = config; 25 | --------------------------------------------------------------------------------