├── .npmignore ├── .coveralls.yml ├── .jshintignore ├── .gitignore ├── .travis.yml ├── .jshintrc ├── index.js ├── package.json ├── LICENSE.md ├── .jscsrc ├── README.md ├── lib └── routr-plugin.js └── tests └── unit └── lib └── routr-plugin.js /.npmignore: -------------------------------------------------------------------------------- 1 | /artifacts/ 2 | /tests/ 3 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /artifacts 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | after_success: 5 | - "npm run cover" 6 | - "cat artifacts/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | 4 | "curly": true, 5 | "latedef": true, 6 | "quotmark": true, 7 | "undef": true, 8 | "unused": true, 9 | "trailing": true 10 | } 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014, Yahoo! Inc. 3 | * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | module.exports = require('./lib/routr-plugin'); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluxible-plugin-dynamic-routr", 3 | "version": "0.2.1", 4 | "description": "A deprecated plugin for Fluxible applications to provide dynamic routing methods", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/localnerve/fluxible-plugin-dynamic-routr.git" 9 | }, 10 | "scripts": { 11 | "cover": "istanbul cover --dir artifacts -- _mocha tests/unit/ --recursive --reporter spec", 12 | "lint": "jshint .", 13 | "style": "jscs .", 14 | "test": "mocha tests/unit/ --recursive --reporter spec", 15 | "validate": "npm run style" 16 | }, 17 | "pre-commit": [ 18 | "lint", 19 | "validate", 20 | "test" 21 | ], 22 | "dependencies": { 23 | "debug": "^2.0.0", 24 | "routr": "^0.1.3" 25 | }, 26 | "devDependencies": { 27 | "chai": "^3.0.0", 28 | "coveralls": "^2.11.2", 29 | "fluxible": "^0.4.10", 30 | "istanbul": "^0.3.17", 31 | "jscs": "^1.10.0", 32 | "jshint": "^2.8.0", 33 | "mocha": "^2.2.5", 34 | "precommit-hook": "^3.0.0" 35 | }, 36 | "author": "Alex Grant ", 37 | "contributors": [], 38 | "licenses": [ 39 | { 40 | "type": "BSD", 41 | "url": "https://github.com/localnerve/fluxible-plugin-dynamic-routr/blob/master/LICENSE.md" 42 | } 43 | ], 44 | "keywords": [ 45 | "yahoo", 46 | "flux", 47 | "react", 48 | "fluxible", 49 | "routr", 50 | "dynamic" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Alex Grant (@localnerve), LocalNerve LLC 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | * Neither the name of the Yahoo! Inc. nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": [ 3 | "./node_modules/**", 4 | "./artifacts/**" 5 | ], 6 | "disallowMixedSpacesAndTabs": true, 7 | "disallowNewlineBeforeBlockStatements": true, 8 | "disallowPaddingNewlinesInBlocks": true, 9 | "disallowSpaceAfterObjectKeys": true, 10 | "disallowTrailingComma": true, 11 | 12 | "maximumLineLength": { 13 | "value": 120, 14 | "allowComments": true, 15 | "allowUrlComments": true, 16 | "allowRegex": true 17 | }, 18 | 19 | "requireBlocksOnNewline": true, 20 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 21 | "requireCapitalizedConstructors": true, 22 | "requireCommaBeforeLineBreak": true, 23 | "requireSpaceAfterKeywords": [ 24 | "if", 25 | "else", 26 | "for", 27 | "while", 28 | "do", 29 | "switch", 30 | "return", 31 | "try", 32 | "catch" 33 | ], 34 | "requireSpaceAfterLineComment": true, 35 | "requireSpaceBeforeBlockStatements": true, 36 | "requireSpacesInConditionalExpression": { 37 | "afterTest": true, 38 | "beforeConsequent": true, 39 | "afterConsequent": true, 40 | "beforeAlternate": true 41 | }, 42 | "requireSpacesInAnonymousFunctionExpression": { 43 | "beforeOpeningCurlyBrace": true 44 | }, 45 | "requireSpacesInFunctionExpression": { 46 | "beforeOpeningCurlyBrace": true 47 | }, 48 | "requireSpacesInNamedFunctionExpression": { 49 | "beforeOpeningCurlyBrace": true 50 | }, 51 | 52 | "safeContextKeyword": ["self"], 53 | 54 | "validateIndentation": 4, 55 | "validateJSDoc": { 56 | "checkParamNames": true, 57 | "checkRedundantParams": true, 58 | "requireParamTypes": true 59 | }, 60 | "validateQuoteMarks": { "mark": "'", "escape": true } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Dynamic Routr Plugin for Fluxible 2 | 3 | [![npm version](https://badge.fury.io/js/fluxible-plugin-dynamic-routr.svg)](http://badge.fury.io/js/fluxible-plugin-dynamic-routr) 4 | [![Build Status](https://travis-ci.org/localnerve/fluxible-plugin-dynamic-routr.svg?branch=master)](https://travis-ci.org/localnerve/fluxible-plugin-dynamic-routr) 5 | [![Dependency Status](https://david-dm.org/localnerve/fluxible-plugin-dynamic-routr.svg)](https://david-dm.org/localnerve/fluxible-plugin-dynamic-routr) 6 | [![devDependency Status](https://david-dm.org/localnerve/fluxible-plugin-dynamic-routr/dev-status.svg)](https://david-dm.org/localnerve/fluxible-plugin-dynamic-routr#info=devDependencies) 7 | [![Coverage Status](https://coveralls.io/repos/localnerve/fluxible-plugin-dynamic-routr/badge.svg?branch=master)](https://coveralls.io/r/localnerve/fluxible-plugin-dynamic-routr?branch=master) 8 | 9 | ## Deprecated 10 | This package has been deprecated in favor of [fluxible-router](https://github.com/yahoo/fluxible-router). It's a major change, but fluxible-router comes with a great RouteStore and supportive components that get even more routing code out of your project. You also get to leave mixins behind, which aligns you with future React. 11 | 12 | For help: 13 | 1. Here is the main [diff](https://github.com/localnerve/flux-react-example/commit/851bad848cd21c8ebecefa098d6b42e42f86ea3c) of my conversion to fluxible-router. 14 | 2. If you have dynamic routing actions that transform to/from functions, this still works great in fluxible-router, but you have to handle re/dehydration yourself. I chose extending the fluxible-router RouteStore as the least impactful change that also aligns with Yahoo. Here my RouteStore [extension](https://github.com/localnerve/flux-react-example/blob/9e6d56f4ab0af2791d76d6e7cb4f84a6ae03b2e0/stores/RouteStore.js), which follows the pattern Yahoo uses for [static routes](https://github.com/yahoo/fluxible-router/blob/4b6f086cf964f28aee5f658bcb60f14b8a2c90e0/lib/RouteStore.js#L134). 15 | 3. Here is the yahoo guide on [upgrading](https://github.com/yahoo/fluxible-router/blob/master/UPGRADE.md) to fluxible-router. 16 | 17 | ## Usage 18 | Provides dynamic routing methods to your [Fluxible](https://github.com/yahoo/fluxible) application using [routr](https://github.com/yahoo/routr). 19 | 20 | ```js 21 | var Fluxible = require('fluxible'); 22 | var routrPlugin = require('fluxible-plugin-routr'); 23 | var app = new Fluxible(); 24 | 25 | var pluginInstance = routrPlugin({ 26 | storeName: 'RoutesStore', // storeName of the Store event source 27 | storeEvent: 'change' // Any event from that Store 28 | }); 29 | 30 | app.plug(pluginInstance); 31 | ``` 32 | 33 | A more complete example usage in an isomorphic fluxible application is found [here](https://github.com/localnerve/flux-react-example) 34 | 35 | ## Routr Plugin API 36 | ### Constructor(options) 37 | 38 | Creates a new routr plugin instance with the following parameters: 39 | 40 | * `options`: An object containing the plugin settings 41 | * `options.storeName` (required): The storeName of the Store 42 | * `options.storeEvent` (required): The name of the event from the Store 43 | * `options.dehydrateRoutes` (optional): A function to transform from fluxible routes to a flat format 44 | * `options.rehydrateRoutes` (optional): A function to transform from a flat format to fluxible routes 45 | 46 | ### Instance Methods 47 | 48 | #### getRoutes 49 | 50 | getter for the `routes` option passed into the constructor. 51 | 52 | ``` 53 | // returns the full routes object 54 | // undefined before updated or rehydrated 55 | pluginInstance.getRoutes(); 56 | ``` 57 | 58 | ### actionContext Methods 59 | 60 | Provides full access to the routr instance. See [routr docs](https://github.com/yahoo/routr) for more information. 61 | 62 | * `actionContext.router.makePath(routeName, routeParams)`: Create a URL based on route name and params 63 | * `actionContext.router.getRoute(path)`: Returns matched route 64 | 65 | ## License 66 | 67 | This software is free to use under the LocalNerve BSD license. 68 | See the [LICENSE file][] for license text and copyright information. 69 | 70 | [LICENSE file]: https://github.com/localnerve/fluxible-plugin-dynamic-routr/blob/master/LICENSE.md 71 | -------------------------------------------------------------------------------- /lib/routr-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | 'use strict'; 6 | 7 | var debug = require('debug')('DynRoutrPlugin'); 8 | var Router = require('routr'); 9 | 10 | var passThru = function(routes) { 11 | return routes; 12 | }; 13 | 14 | module.exports = function routrPlugin(options) { 15 | options = options || {}; 16 | 17 | var storeName = options.storeName; 18 | var storeEvent = options.storeEvent; 19 | var dehydrateRoutes = options.dehydrateRoutes || passThru; 20 | var rehydrateRoutes = options.rehydrateRoutes || passThru; 21 | 22 | // Undefined until updated. 23 | var routes; 24 | 25 | /** 26 | * @class DynRoutrPlugin 27 | */ 28 | return { 29 | name: 'DynRoutrPlugin', 30 | /** 31 | * Called to plug the FluxContext 32 | * @method plugContext 33 | * @returns {Object} 34 | */ 35 | plugContext: function plugContext() { 36 | debug('new plug context'); 37 | var router, actionContext, componentContext; 38 | 39 | /** 40 | * Dynamically update the routes, make a new Router, fix references. 41 | * @param {Object} params 42 | * @param {Object} params.routes The new routes to supply Routr 43 | */ 44 | var updateRoutes = function updateRoutes(params) { 45 | debug('updating routes'); 46 | routes = params.routes; 47 | router = new Router(routes); 48 | if (actionContext) { 49 | actionContext.router = router; 50 | } 51 | if (componentContext) { 52 | componentContext.makePath = router.makePath.bind(router); 53 | } 54 | }; 55 | 56 | return { 57 | /** 58 | * Provides full access to the router in the action context 59 | * @param {Object} context The Fluxible action context 60 | */ 61 | plugActionContext: function plugActionContext(context) { 62 | debug('plug action context, router = '+router); 63 | actionContext = context; 64 | actionContext.router = router; 65 | 66 | // Update on a store event within the single round. 67 | // Could be 'change' from dedicated route store, or 68 | // a dedicated change event within an application store. 69 | actionContext.getStore(storeName) 70 | .removeListener(storeEvent, updateRoutes) 71 | .on(storeEvent, updateRoutes); 72 | }, 73 | /** 74 | * Provides access to create paths by name 75 | * @param {Object} context The Fluxible component context 76 | */ 77 | plugComponentContext: function plugComponentContext(context) { 78 | componentContext = context; 79 | if (router) { 80 | componentContext.makePath = router.makePath.bind(router); 81 | } 82 | }, 83 | /** 84 | * Allows context plugin settings to be persisted between server and client. 85 | * Called on server to send data down to the client. 86 | * @returns {Object} state 87 | */ 88 | dehydrate: function dehydrate() { 89 | return { routes: dehydrateRoutes(routes) }; 90 | }, 91 | /** 92 | * Called on client to rehydrate the context plugin settings. 93 | * @param {Object} state 94 | */ 95 | rehydrate: function rehydrate(state) { 96 | updateRoutes({ routes: rehydrateRoutes(state.routes) }); 97 | } 98 | }; 99 | }, 100 | /** 101 | * @method getRoutes 102 | * @returns {Object} Route specs 103 | */ 104 | getRoutes: function getRoutes() { 105 | return routes; 106 | } 107 | }; 108 | }; -------------------------------------------------------------------------------- /tests/unit/lib/routr-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015 Alex Grant (@localnerve), LocalNerve LLC 3 | * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms. 4 | */ 5 | /*globals describe, it, after, beforeEach */ 6 | 'use strict'; 7 | 8 | var expect = require('chai').expect; 9 | var routrPlugin = require('../../../lib/routr-plugin'); 10 | var createStore = require('fluxible/addons/createStore'); 11 | var Fluxible = require('fluxible'); 12 | 13 | var RoutesStore = createStore({ 14 | storeName: 'RoutesStore', 15 | handlers: { 16 | 'RECEIVE_ROUTES': 'receiveRoutes' 17 | }, 18 | initialize: function() { 19 | this.routes = {}; 20 | }, 21 | receiveRoutes: function(routes) { 22 | this.routes = routes; 23 | this.emitChange(); 24 | } 25 | }); 26 | 27 | describe('DynRoutrPlugin', function() { 28 | var app, 29 | pluginInstance, 30 | pluginName, 31 | context, 32 | routes1 = { 33 | view_user: { 34 | path: '/user/:id', 35 | method: 'get', 36 | foo: { 37 | bar: 'baz' 38 | } 39 | }, 40 | view_user_post: { 41 | path: '/user/:id/post/:post', 42 | method: 'get' 43 | } 44 | }, 45 | routes2 = { 46 | view_second: { 47 | path: '/second/:id', 48 | method: 'get', 49 | foo: { 50 | bar: 'all' 51 | } 52 | } 53 | }, 54 | passThru = function(routes) { 55 | return routes; 56 | }, 57 | dehydrateRoutes = function(routes) { 58 | routes.test = 'test'; 59 | return routes; 60 | }, 61 | rehydrateRoutes = function(routes) { 62 | expect(routes.test).to.equal('test'); 63 | delete routes.test; 64 | return routes; 65 | }, 66 | pluginOptions = { 67 | storeName: RoutesStore.storeName, 68 | storeEvent: 'change' 69 | }; 70 | 71 | function actionContextRoutes(actionContext) { 72 | expect(actionContext.router).to.be.an('object'); 73 | expect(actionContext.router.makePath).to.be.a('function'); 74 | expect(actionContext.router.getRoute).to.be.a('function'); 75 | } 76 | function actionContextRoutes1(actionContext) { 77 | expect(actionContext.router.makePath('view_user', {id: 1})).to.equal('/user/1'); 78 | } 79 | function actionContextRoutes2(actionContext) { 80 | expect(actionContext.router.makePath('view_second', {id: 2})).to.equal('/second/2'); 81 | } 82 | function componentContextRoutes1(componentContext) { 83 | expect(componentContext.makePath).to.be.a('function'); 84 | expect(componentContext.makePath('view_user', {id: 1})).to.equal('/user/1'); 85 | } 86 | function componentContextRoutes2(componentContext) { 87 | expect(componentContext.makePath).to.be.a('function'); 88 | expect(componentContext.makePath('view_second', {id: 2})).to.equal('/second/2'); 89 | } 90 | 91 | beforeEach(function() { 92 | app = new Fluxible(); 93 | pluginInstance = routrPlugin(pluginOptions); 94 | pluginName = pluginInstance.name; 95 | app.plug(pluginInstance); 96 | app.registerStore(RoutesStore); 97 | context = app.createContext(); 98 | }); 99 | 100 | describe('factory', function() { 101 | it('should not have any routes defined', function() { 102 | expect(pluginInstance.getRoutes()).to.be.an('undefined'); 103 | }); 104 | }); 105 | 106 | describe('dehydrate/rehydrate', function() { 107 | var dehydratedState = { 108 | routes: passThru(routes1) 109 | }; 110 | it('should have context rehydrate/dehydrate', function() { 111 | var contextPlug = pluginInstance.plugContext(); 112 | expect(contextPlug.dehydrate).to.be.a('function'); 113 | expect(contextPlug.rehydrate).to.be.a('function'); 114 | }); 115 | describe('rehydrate', function() { 116 | beforeEach(function() { 117 | var plugins = {}; 118 | plugins[pluginName] = dehydratedState; 119 | 120 | context.rehydrate({ 121 | plugins: plugins 122 | }); 123 | }); 124 | it('should have routes', function() { 125 | expect(pluginInstance.getRoutes()).to.eql(routes1); 126 | }); 127 | describe('actionContext', function() { 128 | var actionContext; 129 | beforeEach(function() { 130 | actionContext = context.getActionContext(); 131 | }); 132 | it('should have a router access', function() { 133 | actionContextRoutes(actionContext); 134 | actionContextRoutes1(actionContext); 135 | }); 136 | }); 137 | describe('componentContext', function() { 138 | var componentContext; 139 | beforeEach(function() { 140 | componentContext = context.getComponentContext(); 141 | }); 142 | it('should have a router access', function() { 143 | componentContextRoutes1(componentContext); 144 | }); 145 | }); 146 | }); 147 | describe('dehydrate', function() { 148 | var contextPlug; 149 | beforeEach(function() { 150 | contextPlug = pluginInstance.plugContext(); 151 | contextPlug.rehydrate(dehydratedState); 152 | }); 153 | it('should dehydrate using dehydrateRoutes', function() { 154 | expect(contextPlug.dehydrate(routes1)).to.eql(dehydratedState); 155 | }); 156 | }); 157 | describe('route transformers', function() { 158 | // Just redefine everything 159 | beforeEach(function() { 160 | app = new Fluxible(); 161 | pluginOptions.dehydrateRoutes = dehydrateRoutes; 162 | pluginOptions.rehydrateRoutes = rehydrateRoutes; 163 | pluginInstance = routrPlugin(pluginOptions); 164 | pluginName = pluginInstance.name; 165 | app.plug(pluginInstance); 166 | app.registerStore(RoutesStore); 167 | context = app.createContext(); 168 | }); 169 | after(function() { 170 | delete pluginOptions.dehydrateRoutes; 171 | delete pluginOptions.rehydrateRoutes; 172 | }); 173 | it('should use dehydrateRoutes if defined', function() { 174 | var actionContext = context.getActionContext(); 175 | actionContext.dispatch('RECEIVE_ROUTES', routes1); 176 | dehydratedState = context.dehydrate().plugins[pluginName]; 177 | expect(dehydratedState.routes.test).to.equal('test'); 178 | }); 179 | it('should use rehydrateRoutes if defined', function() { 180 | var plugins = {}; 181 | plugins[pluginName] = dehydratedState; 182 | context.rehydrate({ 183 | plugins: plugins 184 | }); 185 | expect(pluginInstance.getRoutes()).to.eql(routes1); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('updateRoutes', function() { 191 | var actionContext; 192 | beforeEach(function() { 193 | actionContext = context.getActionContext(); 194 | }); 195 | it('should update with routes1 when store updates', function() { 196 | actionContext.dispatch('RECEIVE_ROUTES', routes1); 197 | expect(pluginInstance.getRoutes()).to.eql(routes1); 198 | }); 199 | it('should update with routes2 when store updates', function() { 200 | actionContext.dispatch('RECEIVE_ROUTES', routes2); 201 | expect(pluginInstance.getRoutes()).to.eql(routes2); 202 | }); 203 | describe('context updates', function() { 204 | describe('routes1', function() { 205 | describe('actionContext', function() { 206 | it('should have a router access', function() { 207 | actionContext.dispatch('RECEIVE_ROUTES', routes1); 208 | actionContextRoutes(actionContext); 209 | actionContextRoutes1(actionContext); 210 | }); 211 | }); 212 | describe('componentContext', function() { 213 | var componentContext; 214 | beforeEach(function() { 215 | componentContext = context.getComponentContext(); 216 | }); 217 | it('should have a router access', function() { 218 | actionContext.dispatch('RECEIVE_ROUTES', routes1); 219 | componentContextRoutes1(componentContext); 220 | }); 221 | }); 222 | }); 223 | describe('routes2', function() { 224 | describe('actionContext', function() { 225 | it('should have a router access', function() { 226 | actionContext.dispatch('RECEIVE_ROUTES', routes2); 227 | actionContextRoutes(actionContext); 228 | actionContextRoutes2(actionContext); 229 | }); 230 | }); 231 | describe('componentContext', function() { 232 | var componentContext; 233 | beforeEach(function() { 234 | componentContext = context.getComponentContext(); 235 | }); 236 | it('should have a router access', function() { 237 | actionContext.dispatch('RECEIVE_ROUTES', routes2); 238 | componentContextRoutes2(componentContext); 239 | }); 240 | }); 241 | }); 242 | }); 243 | }); 244 | }); 245 | --------------------------------------------------------------------------------