├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── examples ├── README.md ├── global.css ├── index.html ├── passing-props-to-children │ ├── app.css │ ├── app.js │ └── index.html ├── server.js ├── transitions │ ├── app.js │ └── index.html └── webpack.config.js ├── index.js ├── package.json └── src ├── connectHistory.js ├── connectLifecycle.js ├── connectRouteContext.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .babelrc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-router-ad-hocs 2 | 3 | This is a set of three higher-order components that do (more-or-less) the same things as react-router's mixins. You can use these with React components that you've defined as ES6 classes or with those fancy, stateless, functional components that all the kids are talking about. 4 | 5 | Here's how they work: 6 | 7 | ```javascript 8 | import React, { Component } from 'react' 9 | import { connectHistory } from 'react-router-ad-hocs' 10 | 11 | class MySpecialLink extends Component { 12 | handleClick () { 13 | this.props.history.pushState(null, '/foo') 14 | } 15 | render () { 16 | return ( 17 |

this.handleClick()}> 18 | Click Me! 19 |

20 | ) 21 | } 22 | } 23 | 24 | export default connectHistory(MySpecialLink) 25 | ``` 26 | ## API 27 | 28 | ### `connectHistory(ComponentToBeWrapped)` 29 | 30 | #### Parameters: 31 | ComponentToBeWrapped: Any React component 32 | #### Returns: 33 | A wrapped version of ComponentToBeWrapped that will receive react-router's `history` object as a prop. See the [History mixin](https://github.com/rackt/react-router/blob/master/docs/API.md#history-mixin) documentation for more information. If you are already passing a prop called `history` yourself, it will be overwritten by `connectHistory()`, so don't do that. 34 | 35 | ### `connectLifecycle(ComponentToBeWrapped)` 36 | 37 | #### Parameters: 38 | ComponentToBeWrapped: Any React component that defines a `routerWillLeave` method 39 | #### Returns: 40 | A wrapped version of ComponentToBeWrapped. ComponentToBeWrapped's `routerWillLeave` method will be called immediately before the router leaves the current route, with the ability to cancel the navigation. See the [Lifecycle mixin](https://github.com/rackt/react-router/blob/master/docs/API.md#lifecycle-mixin) documentation for more information. 41 | 42 | ### `connectRouteContext(ComponentToBeWrapped)` 43 | #### Parameters: 44 | ComponentToBeWrapped: Any React component that receives `route` as a prop (i.e. a Route component) 45 | 46 | #### Returns: 47 | A wrapped version of ComponentToBeWrapped that provides `route` on context for all its children. You'd use this on Route components whose children want to use `connectLifecycle()` because `connectLifecycle()` needs to know about the current `route`. See the [RouteContext mixin](https://github.com/rackt/react-router/blob/master/docs/API.md#routecontext-mixin) documentation for more information. 48 | 49 | ## Caveats and differences from react-router mixins: 50 | 51 | 1. `connectHistory()` passes history as a prop, whereas the `History` mixin adds it directly to your component definition and makes it accessible through `this.history`. I decided to pass it as a prop because props are more easily composable. It's probably not a big deal either way, so I guess let me know if this is a problem for you. 52 | 53 | 2. If you're using `connectHistory()` and `connectLifecycle()` together on the same component, it's important that `connectLifecycle()` be applied first (i.e. receive the component first in the composition chain) because it needs to directly access the unwrapped component's `.routerWillLeave()` method, which will otherwise be obscured by `connectHistory()`. So do this: 54 | 55 | ```javascript 56 | const WrappedComponent = connectHistory(connectLifecycle(UnwrappedComponent)) 57 | ``` 58 | 59 | not this: 60 | 61 | ```javascript 62 | const WrappedComponent = connectLifecycle(connectHistory(UnwrappedComponent)) 63 | ``` 64 | 65 | That should cover everything. Have fun! 66 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | React Router Examples 2 | ===================== 3 | 4 | To run the examples in your development environment: 5 | 6 | 1. Clone this repo 7 | 2. Run `npm install` 8 | 3. Start the development server with `npm start` 9 | 4. Point your browser to http://localhost:8080 10 | -------------------------------------------------------------------------------- /examples/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | h1, h2, h3 { 7 | font-weight: 100; 8 | } 9 | 10 | a { 11 | color: hsl(200, 50%, 50%); 12 | } 13 | 14 | a.active { 15 | color: hsl(20, 50%, 50%); 16 | } 17 | 18 | .breadcrumbs a { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | React Router Examples 3 | 4 | 5 |

React Router Examples

6 | 10 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Arial; 3 | font-weight: 200; 4 | } 5 | 6 | a { 7 | color: hsl(200, 50%, 50%); 8 | } 9 | 10 | a.active { 11 | color: hsl(20, 50%, 50%); 12 | } 13 | 14 | #example { 15 | position: absolute; 16 | } 17 | 18 | .App { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | width: 500px; 25 | height: 500px; 26 | } 27 | 28 | .Master { 29 | position: absolute; 30 | left: 0; 31 | top: 0; 32 | bottom: 0; 33 | width: 300px; 34 | overflow: auto; 35 | padding: 10px 40px; 36 | } 37 | 38 | .Detail { 39 | position: absolute; 40 | left: 300px; 41 | top: 0; 42 | bottom: 0; 43 | right: 0; 44 | border-left: 1px solid #ccc; 45 | overflow: auto; 46 | padding: 40px; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createHistory, useBasename } from 'history' 4 | import { Router, Route, Link } from 'react-router' 5 | import connectHistory from '../../src/connectHistory' 6 | 7 | require('./app.css') 8 | 9 | const history = useBasename(createHistory)({ 10 | basename: '/passing-props-to-children' 11 | }) 12 | 13 | const AppContainer = props =>
{props.children}
14 | 15 | const App = connectHistory(React.createClass({ 16 | displayName: 'App', 17 | 18 | getInitialState() { 19 | return { 20 | tacos: [ 21 | { name: 'duck confit' }, 22 | { name: 'carne asada' }, 23 | { name: 'shrimp' } 24 | ] 25 | } 26 | }, 27 | 28 | addTaco() { 29 | let name = prompt('taco name?') 30 | 31 | this.setState({ 32 | tacos: this.state.tacos.concat({ name }) 33 | }) 34 | }, 35 | 36 | handleRemoveTaco(removedTaco) { 37 | this.setState({ 38 | tacos: this.state.tacos.filter(function (taco) { 39 | return taco.name != removedTaco 40 | }) 41 | }) 42 | 43 | this.props.history.pushState(null, '/') 44 | }, 45 | 46 | render() { 47 | let links = this.state.tacos.map(function (taco, i) { 48 | return ( 49 |
  • 50 | {taco.name} 51 |
  • 52 | ) 53 | }) 54 | return ( 55 |
    56 | 57 | 60 |
    61 | {this.props.children && React.cloneElement(this.props.children, { 62 | onRemoveTaco: this.handleRemoveTaco 63 | })} 64 |
    65 |
    66 | ) 67 | } 68 | })) 69 | 70 | const Taco = React.createClass({ 71 | remove() { 72 | this.props.onRemoveTaco(this.props.params.name) 73 | }, 74 | 75 | render() { 76 | return ( 77 |
    78 |

    {this.props.params.name}

    79 | 80 |
    81 | ) 82 | } 83 | }) 84 | 85 | render(( 86 | 87 | 88 | 89 | 90 | 91 | ), document.getElementById('example')) 92 | -------------------------------------------------------------------------------- /examples/passing-props-to-children/index.html: -------------------------------------------------------------------------------- 1 | 2 | Passing Props to Children Example 3 | 4 | 5 |

    React Router Examples / Passing Props to Children

    6 |
    7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console, no-var */ 2 | var express = require('express') 3 | var rewrite = require('express-urlrewrite') 4 | var webpack = require('webpack') 5 | var webpackDevMiddleware = require('webpack-dev-middleware') 6 | var WebpackConfig = require('./webpack.config') 7 | 8 | var app = express() 9 | 10 | app.use(webpackDevMiddleware(webpack(WebpackConfig), { 11 | publicPath: '/__build__/', 12 | stats: { 13 | colors: true 14 | } 15 | })) 16 | 17 | var fs = require('fs') 18 | var path = require('path') 19 | 20 | fs.readdirSync(__dirname).forEach(function (file) { 21 | if (fs.statSync(path.join(__dirname, file)).isDirectory()) 22 | app.use(rewrite('/' + file + '/*', '/' + file + '/index.html')) 23 | }) 24 | 25 | app.use(express.static(__dirname)) 26 | 27 | app.listen(8080, function () { 28 | console.log('Server listening on http://localhost:8080, Ctrl+C to stop') 29 | }) 30 | -------------------------------------------------------------------------------- /examples/transitions/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createHistory, useBasename } from 'history' 4 | import { Router, Route, Link } from 'react-router' 5 | 6 | import { connectHistory, connectLifecycle } from '../../src' 7 | 8 | const history = useBasename(createHistory)({ 9 | basename: '/transitions' 10 | }) 11 | 12 | const App = React.createClass({ 13 | render() { 14 | return ( 15 |
    16 | 20 | {this.props.children} 21 |
    22 | ) 23 | } 24 | }) 25 | 26 | const Dashboard = React.createClass({ 27 | render() { 28 | return

    Dashboard

    29 | } 30 | }) 31 | 32 | const Form = connectHistory(connectLifecycle(React.createClass({ 33 | getInitialState() { 34 | return { 35 | textValue: 'ohai' 36 | } 37 | }, 38 | 39 | routerWillLeave() { 40 | if (this.state.textValue) 41 | return 'You have unsaved information, are you sure you want to leave this page?' 42 | }, 43 | 44 | handleChange(event) { 45 | this.setState({ 46 | textValue: event.target.value 47 | }) 48 | }, 49 | 50 | handleSubmit(event) { 51 | event.preventDefault() 52 | 53 | this.setState({ 54 | textValue: '' 55 | }, () => { 56 | this.props.history.pushState(null, '/') 57 | }) 58 | }, 59 | 60 | render() { 61 | return ( 62 |
    63 |
    64 |

    Click the dashboard link with text in the input.

    65 | 66 | 67 |
    68 |
    69 | ) 70 | } 71 | }))) 72 | 73 | render(( 74 | 75 | 76 | 77 | 78 | 79 | 80 | ), document.getElementById('example')) 81 | -------------------------------------------------------------------------------- /examples/transitions/index.html: -------------------------------------------------------------------------------- 1 | 2 | Transitions Example 3 | 4 | 5 |

    React Router Examples / Transitions

    6 |
    7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-var */ 2 | var fs = require('fs') 3 | var path = require('path') 4 | var webpack = require('webpack') 5 | 6 | module.exports = { 7 | 8 | devtool: 'inline-source-map', 9 | 10 | entry: fs.readdirSync(__dirname).reduce(function (entries, dir) { 11 | if (fs.statSync(path.join(__dirname, dir)).isDirectory()) 12 | entries[dir] = path.join(__dirname, dir, 'app.js') 13 | 14 | return entries 15 | }, {}), 16 | 17 | output: { 18 | path: __dirname + '/__build__', 19 | filename: '[name].js', 20 | chunkFilename: '[id].chunk.js', 21 | publicPath: '/__build__/' 22 | }, 23 | 24 | module: { 25 | loaders: [ 26 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' }, 27 | { test: /\.css$/, loader: 'style!css' } 28 | ] 29 | }, 30 | 31 | plugins: [ 32 | new webpack.optimize.CommonsChunkPlugin('shared.js'), 33 | new webpack.DefinePlugin({ 34 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 35 | }) 36 | ] 37 | 38 | } 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib') -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-ad-hocs", 3 | "version": "0.9.2", 4 | "description": "Higher-order components that do what react-router's mixins do", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node examples/server", 8 | "build": "babel src -d lib", 9 | "prepublish": "npm run build" 10 | }, 11 | "keywords": [ 12 | "react-router", 13 | "react", 14 | "higher-order", 15 | "components", 16 | "mixins" 17 | ], 18 | "author": "", 19 | "license": "ISC", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/dlmanning/react-router-ad-hocs.git" 23 | }, 24 | "dependencies": { 25 | "invariant": "^2.1.2", 26 | "react-router": "^1.0.0", 27 | "warning": "^2.1.0" 28 | }, 29 | "devDependencies": { 30 | "babel": "^6.0.15", 31 | "babel-cli": "^6.1.4", 32 | "babel-core": "^6.1.4", 33 | "babel-loader": "^6.1.0", 34 | "babel-preset-es2015": "^6.1.4", 35 | "babel-preset-react": "^6.1.4", 36 | "babel-preset-stage-2": "^6.1.2", 37 | "css-loader": "^0.22.0", 38 | "express": "^4.13.3", 39 | "express-urlrewrite": "^1.2.0", 40 | "history": "^1.13.0", 41 | "react": "^0.14.2", 42 | "react-dom": "^0.14.2", 43 | "style-loader": "^0.13.0", 44 | "webpack": "^1.12.4", 45 | "webpack-dev-middleware": "^1.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/connectHistory.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import warning from 'warning' 3 | import { PropTypes } from 'react-router' 4 | 5 | /** 6 | * A higher-order-component that passes "history" as a property to components. 7 | */ 8 | 9 | function connectHistory(WrappedComponent) { 10 | class ConnectHistory extends Component { 11 | render() { 12 | warning( 13 | !(this.props.history != null && this.props.history !== this.context.history), 14 | 'The passed prop "history" will be overwritten by connectHistory()' 15 | ) 16 | return 17 | } 18 | } 19 | 20 | ConnectHistory.contextTypes = { history: PropTypes.history } 21 | ConnectHistory.displayName = `ConnectHistory(${getDisplayName(WrappedComponent)})` 22 | ConnectHistory.WrappedComponent = WrappedComponent 23 | 24 | return ConnectHistory 25 | } 26 | 27 | function getDisplayName(WrappedComponent) { 28 | return WrappedComponent.displayName || WrappedComponent.name || 'Component' 29 | } 30 | 31 | export default connectHistory 32 | -------------------------------------------------------------------------------- /src/connectLifecycle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import warning from 'warning' 3 | import invariant from 'invariant' 4 | import { PropTypes } from 'react-router' 5 | 6 | /** 7 | * A higher-order-component that passes "history" as a property to components. 8 | */ 9 | 10 | function connectLifecycle (WrappedComponent) { 11 | class ConnectLifecycle extends Component { 12 | setupLifecyle (wrappedInstance) { 13 | if (wrappedInstance == null) { 14 | return 15 | } 16 | 17 | invariant( 18 | wrappedInstance.routerWillLeave, 19 | 'The Lifecycle higher-order component requires you to define a routerWillLeave method' 20 | ) 21 | 22 | const route = this.props.route || this.context.route 23 | 24 | invariant( 25 | route, 26 | 'The Lifecycle higher-order component must be used on either a) a ' + 27 | 'or b) a descendant of a that uses the RouteContext mixin' 28 | ) 29 | 30 | this._unlistenBeforeLeavingRoute = this.context.history.listenBeforeLeavingRoute( 31 | route, 32 | wrappedInstance.routerWillLeave 33 | ) 34 | } 35 | 36 | componentWillUnmount () { 37 | if (this._unlistenBeforeLeavingRoute) { 38 | this._unlistenBeforeLeavingRoute() 39 | } 40 | } 41 | 42 | render() { 43 | return this.setupLifecyle(instance)} 44 | {...this.props} /> 45 | } 46 | } 47 | 48 | ConnectLifecycle.contextTypes = { 49 | history: PropTypes.history.isRequired, 50 | // Nested children receive the route as context, either 51 | // set by the route component using the RouteContext mixin 52 | // or by some other ancestor. 53 | route: PropTypes.route 54 | }, 55 | 56 | ConnectLifecycle.propTypes = { 57 | // Route components receive the route object as a prop. 58 | route: PropTypes.route 59 | }, 60 | ConnectLifecycle.displayName = `ConnectLifecycle(${getDisplayName(WrappedComponent)})` 61 | ConnectLifecycle.WrappedComponent = WrappedComponent 62 | 63 | return ConnectLifecycle 64 | } 65 | 66 | function getDisplayName (WrappedComponent) { 67 | return WrappedComponent.displayName || WrappedComponent.name || 'Component' 68 | } 69 | 70 | export default connectLifecycle 71 | -------------------------------------------------------------------------------- /src/connectRouteContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The RouteContext higher-order component provides a convenient way 3 | * for route components to set the route in context. This is needed 4 | * for routes that render elements that want to use the Lifecycle 5 | * higher-order component to prevent transitions. 6 | */ 7 | 8 | import React, { Component } from 'react' 9 | import { PropTypes } from 'react-router' 10 | 11 | function connectRouteContext (WrappedComponent) { 12 | class ConnectRouteContext extends Component { 13 | getChildContext () { 14 | return { 15 | route: this.props.route 16 | } 17 | } 18 | 19 | render() { 20 | return 21 | } 22 | 23 | } 24 | 25 | ConnectRouteContext.propTypes = { route: PropTypes.route.isRequired } 26 | ConnectRouteContext.childContextTypes = { route: PropTypes.route.isRequired } 27 | ConnectRouteContext.displayName = `ConnectRouteContext(${getDisplayName(WrappedComponent)})` 28 | ConnectRouteContext.WrappedComponent = WrappedComponent 29 | 30 | return ConnectRouteContext 31 | } 32 | 33 | function getDisplayName(WrappedComponent) { 34 | return WrappedComponent.displayName || WrappedComponent.name || 'Component' 35 | } 36 | 37 | export default connectRouteContext 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as connectHistory } from './connectHistory' 2 | export { default as connectLifecycle } from './connectLifecycle' 3 | export { default as connectRouteContext } from './connectRouteContext' 4 | --------------------------------------------------------------------------------