diff --git a/CHANGES.md b/CHANGES.md index 821262f245..cc7caa4ddb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +## [HEAD] +> Unreleased + +- **Feature:** Added `applyRouterMiddleware` ([#3327]) + +[HEAD]: https://github.com/reactjs/react-router/compare/v2.2.4...master +[#3327]: https://github.com/reactjs/react-router/issues/3327 + ## [v2.2.4] > April 15, 2016 @@ -13,8 +21,8 @@ - **Minor:** Speed up checking index path active status ([#3313]) [v2.2.3]: https://github.com/reactjs/react-router/compare/v2.2.2...v2.2.3 -[#3313]: https://github.com/reactjs/react-router/pull/3313 [#3331]: https://github.com/reactjs/react-router/pull/3331 +[#3313]: https://github.com/reactjs/react-router/pull/3313 ## [v2.2.2] diff --git a/modules/__tests__/applyRouterMiddleware-test.js b/modules/__tests__/applyRouterMiddleware-test.js new file mode 100644 index 0000000000..b3cec2ed5d --- /dev/null +++ b/modules/__tests__/applyRouterMiddleware-test.js @@ -0,0 +1,144 @@ +import expect from 'expect' +import React, { cloneElement } from 'react' +import { render } from 'react-dom' +import Router from '../Router' +import Route from '../Route' +import createMemoryHistory from '../createMemoryHistory' +import applyMiddleware from '../applyRouterMiddleware' + +const FOO_ROOT_CONTAINER_TEXT = 'FOO ROOT CONTAINER' +const BAR_ROOT_CONTAINER_TEXT = 'BAR ROOT CONTAINER' +const BAZ_CONTAINER_TEXT = 'BAZ INJECTED' + +const FooRootContainer = React.createClass({ + propTypes: { children: React.PropTypes.node.isRequired }, + childContextTypes: { foo: React.PropTypes.string }, + getChildContext() { return { foo: FOO_ROOT_CONTAINER_TEXT } }, + render() { + return this.props.children + } +}) + +const FooContainer = React.createClass({ + propTypes: { children: React.PropTypes.node.isRequired }, + contextTypes: { foo: React.PropTypes.string.isRequired }, + render() { + const { children, ...props } = this.props + const fooFromContext = this.context.foo + return cloneElement(children, { ...props, fooFromContext }) + } +}) + +const useFoo = () => ({ + renderRouterContext: (child) => ( + {child} + ), + renderRouteComponent: (child) => ( + {child} + ) +}) + +const BarRootContainer = React.createClass({ + propTypes: { children: React.PropTypes.node.isRequired }, + childContextTypes: { bar: React.PropTypes.string }, + getChildContext() { return { bar: BAR_ROOT_CONTAINER_TEXT } }, + render() { + return this.props.children + } +}) + +const BarContainer = React.createClass({ + propTypes: { children: React.PropTypes.node.isRequired }, + contextTypes: { bar: React.PropTypes.string.isRequired }, + render() { + const { children, ...props } = this.props + const barFromContext = this.context.bar + return cloneElement(children, { props, barFromContext }) + } +}) + +const useBar = () => ({ + renderRouterContext: (child) => ( + {child} + ), + renderRouteComponent: (child) => ( + {child} + ) +}) + +const useBaz = (bazInjected) => ({ + renderRouteComponent: (child) => ( + cloneElement(child, { bazInjected }) + ) +}) + +const run = ({ renderWithMiddleware, Component }, assertion) => { + const div = document.createElement('div') + const routes = + render(, div, () => assertion(div.innerHTML)) +} + +describe('applyMiddleware', () => { + + it('applies one middleware', (done) => { + run({ + renderWithMiddleware: applyMiddleware(useFoo()), + Component: (props) =>
{props.fooFromContext}
+ }, (html) => { + expect(html).toContain(FOO_ROOT_CONTAINER_TEXT) + done() + }) + }) + + it('applies more than one middleware', (done) => { + run({ + renderWithMiddleware: applyMiddleware(useBar(), useFoo()), + Component: (props) =>
{props.fooFromContext} {props.barFromContext}
+ }, (html) => { + expect(html).toContain(FOO_ROOT_CONTAINER_TEXT) + expect(html).toContain(BAR_ROOT_CONTAINER_TEXT) + done() + }) + }) + + it('applies more middleware with only `getContainer`', (done) => { + run({ + renderWithMiddleware: applyMiddleware( + useBar(), + useFoo(), + useBaz(BAZ_CONTAINER_TEXT) + ), + Component: (props) => ( +
+ {props.fooFromContext} + {props.barFromContext} + {props.bazInjected} +
+ ) + }, (html) => { + expect(html).toContain(FOO_ROOT_CONTAINER_TEXT) + expect(html).toContain(BAR_ROOT_CONTAINER_TEXT) + expect(html).toContain(BAZ_CONTAINER_TEXT) + done() + }) + }) + + it('applies middleware that only has `getContainer`', (done) => { + run({ + renderWithMiddleware: applyMiddleware( + useBaz(BAZ_CONTAINER_TEXT) + ), + Component: (props) => ( +
{props.bazInjected}
+ ) + }, (html) => { + expect(html).toContain(BAZ_CONTAINER_TEXT) + done() + }) + }) + +}) diff --git a/modules/applyRouterMiddleware.js b/modules/applyRouterMiddleware.js new file mode 100644 index 0000000000..ab0001837d --- /dev/null +++ b/modules/applyRouterMiddleware.js @@ -0,0 +1,30 @@ +import React, { createElement } from 'react' +import RouterContext from './RouterContext' + +export default (...middlewares) => { + const withContext = middlewares.map(m => m.renderRouterContext).filter(f => f) + const withComponent = middlewares.map(m => m.renderRouteComponent).filter(f => f) + const makeCreateElement = (baseCreateElement = createElement) => ( + (Component, props) => ( + withComponent.reduceRight( + (previous, renderRouteComponent) => ( + renderRouteComponent(previous, props) + ), baseCreateElement(Component, props) + ) + ) + ) + + return (renderProps) => ( + withContext.reduceRight( + (previous, renderRouterContext) => ( + renderRouterContext(previous, renderProps) + ), ( + + ) + ) + ) +} + diff --git a/modules/index.js b/modules/index.js index f3f10f1fae..b82e692dcb 100644 --- a/modules/index.js +++ b/modules/index.js @@ -23,6 +23,7 @@ export PropTypes from './PropTypes' export match from './match' export useRouterHistory from './useRouterHistory' export { formatPattern } from './PatternUtils' +export applyRouterMiddleware from './applyRouterMiddleware' /* histories */ export browserHistory from './browserHistory'