├── .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 |
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 |
--------------------------------------------------------------------------------