diff --git a/modules/History.js b/modules/History.js index 107b4eeae5..4cb3d957cd 100644 --- a/modules/History.js +++ b/modules/History.js @@ -1,13 +1,18 @@ -import { history } from './PropTypes' +import createPropTypes from './PropTypes' -const History = { +export default function createHistory(React) { + const { history } = createPropTypes(React) - contextTypes: { history }, + const History = { + + contextTypes: { history }, + + componentWillMount () { + this.history = this.context.history + } - componentWillMount () { - this.history = this.context.history } -} + return History -export default History +} diff --git a/modules/IndexLink.js b/modules/IndexLink.js index 41971f5962..684b458c7f 100644 --- a/modules/IndexLink.js +++ b/modules/IndexLink.js @@ -1,12 +1,15 @@ -import React from 'react' -import Link from './Link' +import createLink from './Link' -const IndexLink = React.createClass({ +export default function createIndexLink(React) { + const Link = createLink(React) - render() { - return - } + const IndexLink = React.createClass({ -}) + render() { + return + } -export default IndexLink + }) + + return IndexLink +} diff --git a/modules/IndexRoute.js b/modules/IndexRoute.js index a2402d83c6..5d1cbf6a6e 100644 --- a/modules/IndexRoute.js +++ b/modules/IndexRoute.js @@ -1,47 +1,52 @@ -import React from 'react' import invariant from 'invariant' import warning from 'warning' -import { createRouteFromReactElement } from './RouteUtils' -import { component, components, falsy } from './PropTypes' - -const { bool, func } = React.PropTypes - -/** - * An is used to specify its parent's in - * a JSX route config. - */ -const IndexRoute = React.createClass({ - - statics: { - - createRouteFromReactElement(element, parentRoute) { - if (parentRoute) { - parentRoute.indexRoute = createRouteFromReactElement(element) - } else { - warning( - false, - 'An does not make sense at the root of your route config' - ) +import createRouteUtils from './RouteUtils' +import createPropTypes from './PropTypes' + +export default function createIndexRoute(React) { + const { createRouteFromReactElement } = createRouteUtils(React) + const { component, components, falsy } = createPropTypes(React) + + const { bool, func } = React.PropTypes + + /** + * An is used to specify its parent's in + * a JSX route config. + */ + const IndexRoute = React.createClass({ + + statics: { + + createRouteFromReactElement(element, parentRoute) { + if (parentRoute) { + parentRoute.indexRoute = createRouteFromReactElement(element) + } else { + warning( + false, + 'An does not make sense at the root of your route config' + ) + } } - } - - }, - propTypes: { - path: falsy, - ignoreScrollBehavior: bool, - component, - components, - getComponents: func - }, + }, + + propTypes: { + path: falsy, + ignoreScrollBehavior: bool, + component, + components, + getComponents: func + }, + + render() { + invariant( + false, + ' elements are for router configuration only and should not be rendered' + ) + } - render() { - invariant( - false, - ' elements are for router configuration only and should not be rendered' - ) - } + }) -}) + return IndexRoute -export default IndexRoute +} diff --git a/modules/Lifecycle.js b/modules/Lifecycle.js index 4738e3dc23..ac89b256f7 100644 --- a/modules/Lifecycle.js +++ b/modules/Lifecycle.js @@ -1,69 +1,72 @@ -import React from 'react' import invariant from 'invariant' -const { object } = React.PropTypes +export default function createLifecycle(React) { -/** - * The Lifecycle mixin adds the routerWillLeave lifecycle method - * to a component that may be used to cancel a transition or prompt - * the user for confirmation. - * - * On standard transitions, routerWillLeave receives a single argument: the - * location we're transitioning to. To cancel the transition, return false. - * To prompt the user for confirmation, return a prompt message (string). - * - * routerWillLeave does not receive a location object during the beforeunload - * event in web browsers (assuming you're using the useBeforeUnload history - * enhancer). In this case, it is not possible for us to know the location - * we're transitioning to so routerWillLeave must return a prompt message to - * prevent the user from closing the tab. - */ -const Lifecycle = { + const { object } = React.PropTypes - propTypes: { - // Route components receive the route object as a prop. - route: object - }, + /** + * The Lifecycle mixin adds the routerWillLeave lifecycle method + * to a component that may be used to cancel a transition or prompt + * the user for confirmation. + * + * On standard transitions, routerWillLeave receives a single argument: the + * location we're transitioning to. To cancel the transition, return false. + * To prompt the user for confirmation, return a prompt message (string). + * + * routerWillLeave does not receive a location object during the beforeunload + * event in web browsers (assuming you're using the useBeforeUnload history + * enhancer). In this case, it is not possible for us to know the location + * we're transitioning to so routerWillLeave must return a prompt message to + * prevent the user from closing the tab. + */ + const Lifecycle = { - contextTypes: { - history: object.isRequired, - // Nested children receive the route as context, either - // set by the route component using the RouteContext mixin - // or by some other ancestor. - route: object - }, + propTypes: { + // Route components receive the route object as a prop. + route: object + }, - _getRoute() { - const route = this.props.route || this.context.route + contextTypes: { + history: object.isRequired, + // Nested children receive the route as context, either + // set by the route component using the RouteContext mixin + // or by some other ancestor. + route: object + }, - invariant( - route, - 'The Lifecycle mixin needs to be used either on 1) a or ' + - '2) a descendant of a that uses the RouteContext mixin' - ) + _getRoute() { + const route = this.props.route || this.context.route - return route - }, + invariant( + route, + 'The Lifecycle mixin needs to be used either on 1) a or ' + + '2) a descendant of a that uses the RouteContext mixin' + ) - componentWillMount() { - invariant( - this.routerWillLeave, - 'The Lifecycle mixin requires you to define a routerWillLeave method' - ) + return route + }, - this.context.history.registerRouteHook( - this._getRoute(), - this.routerWillLeave - ) - }, + componentWillMount() { + invariant( + this.routerWillLeave, + 'The Lifecycle mixin requires you to define a routerWillLeave method' + ) + + this.context.history.registerRouteHook( + this._getRoute(), + this.routerWillLeave + ) + }, + + componentWillUnmount() { + this.context.history.unregisterRouteHook( + this._getRoute(), + this.routerWillLeave + ) + } - componentWillUnmount() { - this.context.history.unregisterRouteHook( - this._getRoute(), - this.routerWillLeave - ) } -} + return Lifecycle -export default Lifecycle +} diff --git a/modules/Link.js b/modules/Link.js index 06464a3e06..20c207c850 100644 --- a/modules/Link.js +++ b/modules/Link.js @@ -1,8 +1,5 @@ -import React from 'react' import warning from 'warning' -const { bool, object, string, func } = React.PropTypes - function isLeftClickEvent(event) { return event.button === 0 } @@ -19,100 +16,104 @@ function isEmptyObject(object) { return true } -/** - * A is used to create an element that links to a route. - * When that route is active, the link gets an "active" class name - * (or the value of its `activeClassName` prop). - * - * For example, assuming you have the following route: - * - * - * - * You could use the following component to link to that route: - * - * - * - * Links may pass along location state and/or query string parameters - * in the state/query props, respectively. - * - * - */ -const Link = React.createClass({ - - contextTypes: { - history: object - }, - - propTypes: { - activeStyle: object, - activeClassName: string, - onlyActiveOnIndex: bool.isRequired, - to: string.isRequired, - query: object, - state: object, - onClick: func - }, - - getDefaultProps() { - return { - onlyActiveOnIndex: false, - className: '', - style: {} - } - }, +export default function createLink(React) { + const { bool, object, string, func } = React.PropTypes + + /** + * A is used to create an element that links to a route. + * When that route is active, the link gets an "active" class name + * (or the value of its `activeClassName` prop). + * + * For example, assuming you have the following route: + * + * + * + * You could use the following component to link to that route: + * + * + * + * Links may pass along location state and/or query string parameters + * in the state/query props, respectively. + * + * + */ + const Link = React.createClass({ + + contextTypes: { + history: object + }, + + propTypes: { + activeStyle: object, + activeClassName: string, + onlyActiveOnIndex: bool.isRequired, + to: string.isRequired, + query: object, + state: object, + onClick: func + }, + + getDefaultProps() { + return { + onlyActiveOnIndex: false, + className: '', + style: {} + } + }, - handleClick(event) { - let allowTransition = true, clickResult + handleClick(event) { + let allowTransition = true, clickResult - if (this.props.onClick) - clickResult = this.props.onClick(event) + if (this.props.onClick) + clickResult = this.props.onClick(event) - if (isModifiedEvent(event) || !isLeftClickEvent(event)) - return + if (isModifiedEvent(event) || !isLeftClickEvent(event)) + return - if (clickResult === false || event.defaultPrevented === true) - allowTransition = false + if (clickResult === false || event.defaultPrevented === true) + allowTransition = false - event.preventDefault() + event.preventDefault() - if (allowTransition) - this.context.history.pushState(this.props.state, this.props.to, this.props.query) - }, + if (allowTransition) + this.context.history.pushState(this.props.state, this.props.to, this.props.query) + }, - componentWillMount() { - warning( - this.context.history, - 'A should not be rendered outside the context of history ' + - 'some features including real hrefs, active styling, and navigation ' + - 'will not function correctly' - ) - }, + componentWillMount() { + warning( + this.context.history, + 'A should not be rendered outside the context of history ' + + 'some features including real hrefs, active styling, and navigation ' + + 'will not function correctly' + ) + }, - render() { - const { history } = this.context - const { activeClassName, activeStyle, onlyActiveOnIndex, to, query, state, onClick, ...props } = this.props + render() { + const { history } = this.context + const { activeClassName, activeStyle, onlyActiveOnIndex, to, query, state, onClick, ...props } = this.props - props.onClick = this.handleClick + props.onClick = this.handleClick - // Ignore if rendered outside the context - // of history, simplifies unit testing. - if (history) { - props.href = history.createHref(to, query) + // Ignore if rendered outside the context + // of history, simplifies unit testing. + if (history) { + props.href = history.createHref(to, query) - if (activeClassName || (activeStyle != null && !isEmptyObject(activeStyle))) { - if (history.isActive(to, query, onlyActiveOnIndex)) { - if (activeClassName) - props.className += props.className === '' ? activeClassName : ` ${activeClassName}` + if (activeClassName || (activeStyle != null && !isEmptyObject(activeStyle))) { + if (history.isActive(to, query, onlyActiveOnIndex)) { + if (activeClassName) + props.className += props.className === '' ? activeClassName : ` ${activeClassName}` - if (activeStyle) - props.style = { ...props.style, ...activeStyle } + if (activeStyle) + props.style = { ...props.style, ...activeStyle } + } } } - } - return React.createElement('a', props) - } + return React.createElement('a', props) + } -}) + }) -export default Link + return Link +} diff --git a/modules/PropTypes.js b/modules/PropTypes.js index c22a9726b7..b6c5f0ad76 100644 --- a/modules/PropTypes.js +++ b/modules/PropTypes.js @@ -1,37 +1,40 @@ -import { PropTypes } from 'react' +export default function createPropTypes(React) { -const { func, object, arrayOf, oneOfType, element, shape, string } = PropTypes + const { func, object, arrayOf, oneOfType, element, shape, string } = React.PropTypes -export function falsy(props, propName, componentName) { - if (props[propName]) - return new Error(`<${componentName}> should not have a "${propName}" prop`) -} + function falsy(props, propName, componentName) { + if (props[propName]) + return new Error(`<${componentName}> should not have a "${propName}" prop`) + } + + const history = shape({ + listen: func.isRequired, + pushState: func.isRequired, + replaceState: func.isRequired, + go: func.isRequired + }) -export const history = shape({ - listen: func.isRequired, - pushState: func.isRequired, - replaceState: func.isRequired, - go: func.isRequired -}) + const location = shape({ + pathname: string.isRequired, + search: string.isRequired, + state: object, + action: string.isRequired, + key: string + }) -export const location = shape({ - pathname: string.isRequired, - search: string.isRequired, - state: object, - action: string.isRequired, - key: string -}) + const component = oneOfType([ func, string ]) + const components = oneOfType([ component, object ]) + const route = oneOfType([ object, element ]) + const routes = oneOfType([ route, arrayOf(route) ]) -export const component = oneOfType([ func, string ]) -export const components = oneOfType([ component, object ]) -export const route = oneOfType([ object, element ]) -export const routes = oneOfType([ route, arrayOf(route) ]) + return { + falsy, + history, + location, + component, + components, + route, + routes + } -export default { - falsy, - history, - location, - component, - components, - route } diff --git a/modules/Redirect.js b/modules/Redirect.js index e236566d98..2c44e0b3be 100644 --- a/modules/Redirect.js +++ b/modules/Redirect.js @@ -1,67 +1,73 @@ -import React from 'react' import invariant from 'invariant' -import { createRouteFromReactElement } from './RouteUtils' +import createRouteUtils from './RouteUtils' import { formatPattern } from './PatternUtils' -import { falsy } from './PropTypes' +import createPropTypes from './PropTypes' -const { string, object } = React.PropTypes +export default function createRedirect(React) { + const { createRouteFromReactElement } = createRouteUtils(React) -/** - * A is used to declare another URL path a client should be sent - * to when they request a given URL. - * - * Redirects are placed alongside routes in the route configuration and are - * traversed in the same manner. - */ -const Redirect = React.createClass({ + const { falsy } = createPropTypes(React) - statics: { + const { string, object } = React.PropTypes - createRouteFromReactElement(element) { - const route = createRouteFromReactElement(element) + /** + * A is used to declare another URL path a client should be sent + * to when they request a given URL. + * + * Redirects are placed alongside routes in the route configuration and are + * traversed in the same manner. + */ + const Redirect = React.createClass({ - if (route.from) - route.path = route.from + statics: { - // TODO: Handle relative pathnames, see #1658 - invariant( - route.to.charAt(0) === '/', - ' must be an absolute path. This should be fixed in the future' - ) + createRouteFromReactElement(element) { + const route = createRouteFromReactElement(element) - route.onEnter = function (nextState, replaceState) { - const { location, params } = nextState - const pathname = route.to ? formatPattern(route.to, params) : location.pathname + if (route.from) + route.path = route.from - replaceState( - route.state || location.state, - pathname, - route.query || location.query + // TODO: Handle relative pathnames, see #1658 + invariant( + route.to.charAt(0) === '/', + ' must be an absolute path. This should be fixed in the future' ) + + route.onEnter = function (nextState, replaceState) { + const { location, params } = nextState + const pathname = route.to ? formatPattern(route.to, params) : location.pathname + + replaceState( + route.state || location.state, + pathname, + route.query || location.query + ) + } + + return route } - return route - } + }, - }, + propTypes: { + path: string, + from: string, // Alias for path + to: string.isRequired, + query: object, + state: object, + onEnter: falsy, + children: falsy + }, - propTypes: { - path: string, - from: string, // Alias for path - to: string.isRequired, - query: object, - state: object, - onEnter: falsy, - children: falsy - }, + render() { + invariant( + false, + ' elements are for router configuration only and should not be rendered' + ) + } - render() { - invariant( - false, - ' elements are for router configuration only and should not be rendered' - ) - } + }) -}) + return Redirect -export default Redirect +} diff --git a/modules/Route.js b/modules/Route.js index 4eebbda44e..ea4334a3b4 100644 --- a/modules/Route.js +++ b/modules/Route.js @@ -1,59 +1,66 @@ -import React from 'react' import warning from 'warning' import invariant from 'invariant' -import { createRouteFromReactElement } from './RouteUtils' -import { component, components } from './PropTypes' - -const { string, bool, func } = React.PropTypes - -/** - * A is used to declare which components are rendered to the page when - * the URL matches a given pattern. - * - * Routes are arranged in a nested tree structure. When a new URL is requested, - * the tree is searched depth-first to find a route whose path matches the URL. - * When one is found, all routes in the tree that lead to it are considered - * "active" and their components are rendered into the DOM, nested in the same - * order as they are in the tree. - */ -const Route = React.createClass({ - - statics: { - - createRouteFromReactElement(element) { - const route = createRouteFromReactElement(element) - - if (route.handler) { - warning( - false, - ' is deprecated, use instead' - ) - - route.component = route.handler - delete route.handler + +import createRouteUtils from './RouteUtils' +import createPropTypes from './PropTypes' + +export default function createRoute(React) { + + const { createRouteFromReactElement } = createRouteUtils(React) + const { component, components } = createPropTypes(React) + + const { string, bool, func } = React.PropTypes + + /** + * A is used to declare which components are rendered to the page when + * the URL matches a given pattern. + * + * Routes are arranged in a nested tree structure. When a new URL is requested, + * the tree is searched depth-first to find a route whose path matches the URL. + * When one is found, all routes in the tree that lead to it are considered + * "active" and their components are rendered into the DOM, nested in the same + * order as they are in the tree. + */ + const Route = React.createClass({ + + statics: { + + createRouteFromReactElement(element) { + const route = createRouteFromReactElement(element) + + if (route.handler) { + warning( + false, + ' is deprecated, use instead' + ) + + route.component = route.handler + delete route.handler + } + + return route } - return route + }, + + propTypes: { + path: string, + ignoreScrollBehavior: bool, + handler: component, // deprecated + component, + components, + getComponents: func + }, + + render() { + invariant( + false, + ' elements are for router configuration only and should not be rendered' + ) } - - }, - - propTypes: { - path: string, - ignoreScrollBehavior: bool, - handler: component, // deprecated - component, - components, - getComponents: func - }, - - render() { - invariant( - false, - ' elements are for router configuration only and should not be rendered' - ) - } - -}) - -export default Route + + }) + + return Route + +} diff --git a/modules/RouteContext.js b/modules/RouteContext.js index 0483df1769..7f9202a580 100644 --- a/modules/RouteContext.js +++ b/modules/RouteContext.js @@ -1,29 +1,31 @@ -import React from 'react' +export default function createRouteContext(React) { -const { object } = React.PropTypes + const { object } = React.PropTypes -/** - * The RouteContext mixin provides a convenient way for route - * components to set the route in context. This is needed for - * routes that render elements that want to use the Lifecycle - * mixin to prevent transitions. - */ -const RouteContext = { + /** + * The RouteContext mixin provides a convenient way for route + * components to set the route in context. This is needed for + * routes that render elements that want to use the Lifecycle + * mixin to prevent transitions. + */ + const RouteContext = { - propTypes: { - route: object.isRequired - }, + propTypes: { + route: object.isRequired + }, - childContextTypes: { - route: object.isRequired - }, + childContextTypes: { + route: object.isRequired + }, - getChildContext() { - return { - route: this.props.route + getChildContext() { + return { + route: this.props.route + } } + } -} + return RouteContext -export default RouteContext +} diff --git a/modules/RouteUtils.js b/modules/RouteUtils.js index 9b1da7a9ef..b461ba8e81 100644 --- a/modules/RouteUtils.js +++ b/modules/RouteUtils.js @@ -1,97 +1,107 @@ -import React from 'react' import warning from 'warning' -function isValidChild(object) { - return object == null || React.isValidElement(object) -} +export default function createRouteUtils(React) { -export function isReactChildren(object) { - return isValidChild(object) || (Array.isArray(object) && object.every(isValidChild)) -} + function isValidChild(object) { + return object == null || React.isValidElement(object) + } + + function isReactChildren(object) { + return isValidChild(object) || (Array.isArray(object) && object.every(isValidChild)) + } -function checkPropTypes(componentName, propTypes, props) { - componentName = componentName || 'UnknownComponent' + function checkPropTypes(componentName, propTypes, props) { + componentName = componentName || 'UnknownComponent' - for (const propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - const error = propTypes[propName](props, propName, componentName) + for (const propName in propTypes) { + if (propTypes.hasOwnProperty(propName)) { + const error = propTypes[propName](props, propName, componentName) - if (error instanceof Error) - warning(false, error.message) + if (error instanceof Error) + warning(false, error.message) + } } } -} -function createRoute(defaultProps, props) { - return { ...defaultProps, ...props } -} + function createRoute(defaultProps, props) { + return { ...defaultProps, ...props } + } -export function createRouteFromReactElement(element) { - const type = element.type - const route = createRoute(type.defaultProps, element.props) + function createRouteFromReactElement(element) { + const type = element.type + const route = createRoute(type.defaultProps, element.props) - if (type.propTypes) - checkPropTypes(type.displayName || type.name, type.propTypes, route) + if (type.propTypes) + checkPropTypes(type.displayName || type.name, type.propTypes, route) - if (route.children) { - const childRoutes = createRoutesFromReactChildren(route.children, route) + if (route.children) { + const childRoutes = createRoutesFromReactChildren(route.children, route) - if (childRoutes.length) - route.childRoutes = childRoutes + if (childRoutes.length) + route.childRoutes = childRoutes - delete route.children - } + delete route.children + } - return route -} + return route + } -/** - * Creates and returns a routes object from the given ReactChildren. JSX - * provides a convenient way to visualize how routes in the hierarchy are - * nested. - * - * import { Route, createRoutesFromReactChildren } from 'react-router' - * - * const routes = createRoutesFromReactChildren( - * - * - * - * - * ) - * - * Note: This method is automatically used when you provide children - * to a component. - */ -export function createRoutesFromReactChildren(children, parentRoute) { - const routes = [] - - React.Children.forEach(children, function (element) { - if (React.isValidElement(element)) { - // Component classes may have a static create* method. - if (element.type.createRouteFromReactElement) { - const route = element.type.createRouteFromReactElement(element, parentRoute) - - if (route) - routes.push(route) - } else { - routes.push(createRouteFromReactElement(element)) + /** + * Creates and returns a routes object from the given ReactChildren. JSX + * provides a convenient way to visualize how routes in the hierarchy are + * nested. + * + * import { Route, createRoutesFromReactChildren } from 'react-router' + * + * const routes = createRoutesFromReactChildren( + * + * + * + * + * ) + * + * Note: This method is automatically used when you provide children + * to a component. + */ + function createRoutesFromReactChildren(children, parentRoute) { + const routes = [] + + React.Children.forEach(children, function (element) { + if (React.isValidElement(element)) { + // Component classes may have a static create* method. + if (element.type.createRouteFromReactElement) { + const route = element.type.createRouteFromReactElement(element, parentRoute) + + if (route) + routes.push(route) + } else { + routes.push(createRouteFromReactElement(element)) + } } + }) + + return routes + } + + /** + * Creates and returns an array of routes from the given object which + * may be a JSX route, a plain object route, or an array of either. + */ + function createRoutes(routes) { + if (isReactChildren(routes)) { + routes = createRoutesFromReactChildren(routes) + } else if (!Array.isArray(routes)) { + routes = [ routes ] } - }) - return routes -} + return routes + } -/** - * Creates and returns an array of routes from the given object which - * may be a JSX route, a plain object route, or an array of either. - */ -export function createRoutes(routes) { - if (isReactChildren(routes)) { - routes = createRoutesFromReactChildren(routes) - } else if (!Array.isArray(routes)) { - routes = [ routes ] + return { + isReactChildren, + createRouteFromReactElement, + createRoutesFromReactChildren, + createRoutes } - return routes } diff --git a/modules/Router.js b/modules/Router.js index 0598724b24..915d6dcb62 100644 --- a/modules/Router.js +++ b/modules/Router.js @@ -1,97 +1,102 @@ -import React from 'react' import warning from 'warning' import createHashHistory from 'history/lib/createHashHistory' -import { createRoutes } from './RouteUtils' -import RoutingContext from './RoutingContext' -import useRoutes from './useRoutes' -import { routes } from './PropTypes' +import createRouteUtils from './RouteUtils' +import createRoutingContext from './RoutingContext' +import createUseRoutes from './useRoutes' +import createPropTypes from './PropTypes' -const { func, object } = React.PropTypes +export default function createRouter(React) { + const { createRoutes } = createRouteUtils(React) + const RoutingContext = createRoutingContext(React) + const useRoutes = createUseRoutes(React) + const { routes } = createPropTypes(React) + const { func, object } = React.PropTypes -/** - * A is a high-level API for automatically setting up - * a router that renders a with all the props - * it needs each time the URL changes. - */ -const Router = React.createClass({ + /** + * A is a high-level API for automatically setting up + * a router that renders a with all the props + * it needs each time the URL changes. + */ + const Router = React.createClass({ - propTypes: { - history: object, - children: routes, - routes, // alias for children - createElement: func, - onError: func, - onUpdate: func, - parseQueryString: func, - stringifyQuery: func - }, + propTypes: { + history: object, + children: routes, + routes, // alias for children + createElement: func, + onError: func, + onUpdate: func, + parseQueryString: func, + stringifyQuery: func + }, - getInitialState() { - return { - location: null, - routes: null, - params: null, - components: null - } - }, + getInitialState() { + return { + location: null, + routes: null, + params: null, + components: null + } + }, - handleError(error) { - if (this.props.onError) { - this.props.onError.call(this, error) - } else { - // Throw errors by default so we don't silently swallow them! - throw error // This error probably occurred in getChildRoutes or getComponents. - } - }, + handleError(error) { + if (this.props.onError) { + this.props.onError.call(this, error) + } else { + // Throw errors by default so we don't silently swallow them! + throw error // This error probably occurred in getChildRoutes or getComponents. + } + }, - componentWillMount() { - let { history, children, routes, parseQueryString, stringifyQuery } = this.props - let createHistory = history ? () => history : createHashHistory + componentWillMount() { + let { history, children, routes, parseQueryString, stringifyQuery } = this.props + let createHistory = history ? () => history : createHashHistory - this.history = useRoutes(createHistory)({ - routes: createRoutes(routes || children), - parseQueryString, - stringifyQuery - }) + this.history = useRoutes(createHistory)({ + routes: createRoutes(routes || children), + parseQueryString, + stringifyQuery + }) - this._unlisten = this.history.listen((error, state) => { - if (error) { - this.handleError(error) - } else { - this.setState(state, this.props.onUpdate) - } - }) - }, + this._unlisten = this.history.listen((error, state) => { + if (error) { + this.handleError(error) + } else { + this.setState(state, this.props.onUpdate) + } + }) + }, - componentWillReceiveProps(nextProps) { - warning( - nextProps.history === this.props.history, - "The `history` provided to has changed, it will be ignored." - ) - }, + componentWillReceiveProps(nextProps) { + warning( + nextProps.history === this.props.history, + "The `history` provided to has changed, it will be ignored." + ) + }, - componentWillUnmount() { - if (this._unlisten) - this._unlisten() - }, + componentWillUnmount() { + if (this._unlisten) + this._unlisten() + }, - render() { - let { location, routes, params, components } = this.state - let { createElement } = this.props + render() { + let { location, routes, params, components } = this.state + let { createElement } = this.props - if (location == null) - return null // Async match + if (location == null) + return null // Async match - return React.createElement(RoutingContext, { - history: this.history, - createElement, - location, - routes, - params, - components - }) - } + return React.createElement(RoutingContext, { + history: this.history, + createElement, + location, + routes, + params, + components + }) + } -}) + }) -export default Router + return Router +} diff --git a/modules/RoutingContext.js b/modules/RoutingContext.js index fbe9f4cae6..c8b4e5f6e1 100644 --- a/modules/RoutingContext.js +++ b/modules/RoutingContext.js @@ -1,91 +1,94 @@ -import React from 'react' import invariant from 'invariant' import getRouteParams from './getRouteParams' -const { array, func, object } = React.PropTypes - -/** - * A renders the component tree for a given router state - * and sets the history object and the current location in context. - */ -const RoutingContext = React.createClass({ - - propTypes: { - history: object.isRequired, - createElement: func.isRequired, - location: object.isRequired, - routes: array.isRequired, - params: object.isRequired, - components: array.isRequired - }, - - getDefaultProps() { - return { - createElement: React.createElement +export default function createRoutingContext(React) { + + const { array, func, object } = React.PropTypes + + /** + * A renders the component tree for a given router state + * and sets the history object and the current location in context. + */ + const RoutingContext = React.createClass({ + + propTypes: { + history: object.isRequired, + createElement: func.isRequired, + location: object.isRequired, + routes: array.isRequired, + params: object.isRequired, + components: array.isRequired + }, + + getDefaultProps() { + return { + createElement: React.createElement + } + }, + + childContextTypes: { + history: object.isRequired, + location: object.isRequired + }, + + getChildContext() { + return { + history: this.props.history, + location: this.props.location + } + }, + + createElement(component, props) { + return component == null ? null : this.props.createElement(component, props) + }, + + render() { + const { history, location, routes, params, components } = this.props + let element = null + + if (components) { + element = components.reduceRight((element, components, index) => { + if (components == null) + return element // Don't create new children use the grandchildren. + + const route = routes[index] + const routeParams = getRouteParams(route, params) + const props = { + history, + location, + params, + route, + routeParams, + routes + } + + if (element) + props.children = element + + if (typeof components === 'object') { + const elements = {} + + for (const key in components) + if (components.hasOwnProperty(key)) + elements[key] = this.createElement(components[key], props) + + return elements + } + + return this.createElement(components, props) + }, element) + } + + invariant( + element === null || element === false || React.isValidElement(element), + 'The root route must render a single element' + ) + + return element } - }, - childContextTypes: { - history: object.isRequired, - location: object.isRequired - }, + }) - getChildContext() { - return { - history: this.props.history, - location: this.props.location - } - }, - - createElement(component, props) { - return component == null ? null : this.props.createElement(component, props) - }, - - render() { - const { history, location, routes, params, components } = this.props - let element = null - - if (components) { - element = components.reduceRight((element, components, index) => { - if (components == null) - return element // Don't create new children use the grandchildren. - - const route = routes[index] - const routeParams = getRouteParams(route, params) - const props = { - history, - location, - params, - route, - routeParams, - routes - } - - if (element) - props.children = element - - if (typeof components === 'object') { - const elements = {} - - for (const key in components) - if (components.hasOwnProperty(key)) - elements[key] = this.createElement(components[key], props) - - return elements - } - - return this.createElement(components, props) - }, element) - } - - invariant( - element === null || element === false || React.isValidElement(element), - 'The root route must render a single element' - ) - - return element - } - -}) + return RoutingContext -export default RoutingContext +} diff --git a/modules/__tests__/History-test.js b/modules/__tests__/History-test.js index b77d8ebe67..7d996399e3 100644 --- a/modules/__tests__/History-test.js +++ b/modules/__tests__/History-test.js @@ -1,10 +1,15 @@ /*eslint-env mocha */ import expect from 'expect' import React from 'react' -import History from '../History' -import Router from '../Router' -import Route from '../Route' -import createHistory from 'history/lib/createMemoryHistory' +import createHistory from '../History' +import createRouter from '../Router' +import createRoute from '../Route' +import createMemoryHistory from 'history/lib/createMemoryHistory' + +const History = createHistory(React) +const Router = createRouter(React) +const Route = createRoute(React) + describe('History Mixin', function () { @@ -18,7 +23,7 @@ describe('History Mixin', function () { }) it('assigns the history to the component instance', function (done) { - let history = createHistory('/') + let history = createMemoryHistory('/') function assertHistory() { expect(this.history).toExist() diff --git a/modules/__tests__/IndexRoute-test.js b/modules/__tests__/IndexRoute-test.js index 0759f9a7bd..c5669e0620 100644 --- a/modules/__tests__/IndexRoute-test.js +++ b/modules/__tests__/IndexRoute-test.js @@ -3,9 +3,13 @@ import expect from 'expect' import React from 'react' import createHistory from 'history/lib/createMemoryHistory' -import IndexRoute from '../IndexRoute' -import Router from '../Router' -import Route from '../Route' +import createIndexRoute from '../IndexRoute' +import createRouter from '../Router' +import createRoute from '../Route' + +const IndexRoute = createIndexRoute(React) +const Router = createRouter(React) +const Route = createRoute(React) describe('an ', function () { diff --git a/modules/__tests__/Link-test.js b/modules/__tests__/Link-test.js index 2156dc5c9e..4bb4249220 100644 --- a/modules/__tests__/Link-test.js +++ b/modules/__tests__/Link-test.js @@ -5,9 +5,13 @@ import expect from 'expect' import React from 'react/addons' import createHistory from 'history/lib/createMemoryHistory' import execSteps from './execSteps' -import Router from '../Router' -import Route from '../Route' -import Link from '../Link' +import createRouter from '../Router' +import createRoute from '../Route' +import createLink from '../Link' + +const Router = createRouter(React) +const Route = createRoute(React) +const Link = createLink(React) const { click } = React.addons.TestUtils.Simulate diff --git a/modules/__tests__/Redirect-test.js b/modules/__tests__/Redirect-test.js index 0d7f3e5081..d208fee8e6 100644 --- a/modules/__tests__/Redirect-test.js +++ b/modules/__tests__/Redirect-test.js @@ -2,9 +2,13 @@ import expect from 'expect' import React from 'react' import createHistory from 'history/lib/createMemoryHistory' -import Redirect from '../Redirect' -import Router from '../Router' -import Route from '../Route' +import createRedirect from '../Redirect' +import createRouter from '../Router' +import createRoute from '../Route' + +const Redirect = createRedirect(React) +const Router = createRouter(React) +const Route = createRoute(React) describe('A ', function () { diff --git a/modules/__tests__/RouteComponent-test.js b/modules/__tests__/RouteComponent-test.js index 159bd26148..2a323e87d1 100644 --- a/modules/__tests__/RouteComponent-test.js +++ b/modules/__tests__/RouteComponent-test.js @@ -2,7 +2,9 @@ import expect from 'expect' import React from 'react' import createHistory from 'history/lib/createMemoryHistory' -import Router from '../Router' +import createRouter from '../Router' + +const Router = createRouter(React) describe('a Route Component', function () { diff --git a/modules/__tests__/Router-test.js b/modules/__tests__/Router-test.js index d9648c3177..6ec52f81b8 100644 --- a/modules/__tests__/Router-test.js +++ b/modules/__tests__/Router-test.js @@ -3,8 +3,11 @@ import expect from 'expect' import React from 'react' import createHistory from 'history/lib/createMemoryHistory' -import Router from '../Router' -import Route from '../Route' +import createRouter from '../Router' +import createRoute from '../Route' + +const Router = createRouter(React) +const Route = createRoute(React) describe('Router', function () { diff --git a/modules/__tests__/createRoutesFromReactChildren-test.js b/modules/__tests__/createRoutesFromReactChildren-test.js index a4730014c6..719702dc28 100644 --- a/modules/__tests__/createRoutesFromReactChildren-test.js +++ b/modules/__tests__/createRoutesFromReactChildren-test.js @@ -2,9 +2,13 @@ /*eslint react/prop-types: 0*/ import expect from 'expect' import React from 'react' -import { createRoutesFromReactChildren } from '../RouteUtils' -import IndexRoute from '../IndexRoute' -import Route from '../Route' +import createRouteUtils from '../RouteUtils' +import createIndexRoute from '../IndexRoute' +import createRoute from '../Route' + +const Route = createRoute(React) +const IndexRoute = createIndexRoute(React) +const { createRoutesFromReactChildren } = createRouteUtils(React) describe('createRoutesFromReactChildren', function () { diff --git a/modules/__tests__/isActive-test.js b/modules/__tests__/isActive-test.js index c435fae244..d9f2d561be 100644 --- a/modules/__tests__/isActive-test.js +++ b/modules/__tests__/isActive-test.js @@ -2,9 +2,13 @@ import expect from 'expect' import React from 'react' import createHistory from 'history/lib/createMemoryHistory' -import IndexRoute from '../IndexRoute' -import Router from '../Router' -import Route from '../Route' +import createIndexRoute from '../IndexRoute' +import createRouter from '../Router' +import createRoute from '../Route' + +const IndexRoute = createIndexRoute(React) +const Router = createRouter(React) +const Route = createRoute(React) describe('isActive', function () { diff --git a/modules/__tests__/matchRoutes-test.js b/modules/__tests__/matchRoutes-test.js index f318fe672e..396a7cd5da 100644 --- a/modules/__tests__/matchRoutes-test.js +++ b/modules/__tests__/matchRoutes-test.js @@ -1,12 +1,16 @@ /*eslint-env mocha */ import React from 'react' -import Route from '../Route' +import createRoute from '../Route' import assert from 'assert' import expect from 'expect' import { createLocation } from 'history' -import { createRoutes } from '../RouteUtils' -import matchRoutes from '../matchRoutes' +import createRouteUtils from '../RouteUtils' +import createMatchRoutes from '../matchRoutes' + +const Route = createRoute(React) +const { createRoutes } = createRouteUtils(React) +const matchRoutes = createMatchRoutes(React) describe('matchRoutes', function () { diff --git a/modules/__tests__/pushState-test.js b/modules/__tests__/pushState-test.js index 7b311c7c3f..27da7ecc77 100644 --- a/modules/__tests__/pushState-test.js +++ b/modules/__tests__/pushState-test.js @@ -3,8 +3,11 @@ import expect from 'expect' import React from 'react' import resetHash from './resetHash' import execSteps from './execSteps' -import Router from '../Router' -import Route from '../Route' +import createRouter from '../Router' +import createRoute from '../Route' + +const Router = createRouter(React) +const Route = createRoute(React) describe('pushState', function () { beforeEach(resetHash) diff --git a/modules/__tests__/serverRendering-test.js b/modules/__tests__/serverRendering-test.js index 2e74d79cfd..675b70576b 100644 --- a/modules/__tests__/serverRendering-test.js +++ b/modules/__tests__/serverRendering-test.js @@ -3,9 +3,13 @@ import expect from 'expect' import React from 'react' import createLocation from 'history/lib/createLocation' -import RoutingContext from '../RoutingContext' -import match from '../match' -import Link from '../Link' +import createRoutingContext from '../RoutingContext' +import createMatch from '../match' +import createLink from '../Link' + +const RoutingContext = createRoutingContext(React) +const match = createMatch(React) +const Link = createLink(React) describe('server rendering', function () { diff --git a/modules/__tests__/transitionHooks-test.js b/modules/__tests__/transitionHooks-test.js index 6e998ab188..619effdd3c 100644 --- a/modules/__tests__/transitionHooks-test.js +++ b/modules/__tests__/transitionHooks-test.js @@ -4,7 +4,9 @@ import expect, { spyOn } from 'expect' import React from 'react' import createHistory from 'history/lib/createMemoryHistory' import execSteps from './execSteps' -import Router from '../Router' +import createRouter from '../Router' + +const Router = createRouter(React) describe('When a router enters a branch', function () { diff --git a/modules/createAll.js b/modules/createAll.js new file mode 100644 index 0000000000..6dc3980ca7 --- /dev/null +++ b/modules/createAll.js @@ -0,0 +1,54 @@ +import createRouter from './Router' +import createLink from './Link' +import createIndexLink from './IndexLink' +import createIndexRoute from './IndexRoute' +import createRedirect from './Redirect'; +import createRoute from './Route' +import createHistory from './History' +import createLifecycle from './Lifecycle' +import createRouteContext from './RouteContext' +import createUseRoutes from './useRoutes' +import createRouteUtils from './RouteUtils' +import createRoutingContext from './RoutingContext' +import createPropTypes from './PropTypes' +import createMatch from './match' + + +export default function createAll(React) { + const Router = createRouter(React) + const Link = createLink(React) + const IndexLink = createIndexLink(React) + const IndexRoute = createIndexRoute(React) + const Redirect = createRedirect(React) + const Route = createRoute(React) + const History = createHistory(React) + const Lifecycle = createLifecycle(React) + const RouteContext = createRouteContext(React) + const useRoutes = createUseRoutes(React) + const { createRoutes } = createRouteUtils(React) + const RoutingContext = createRoutingContext(React) + const PropTypes = createPropTypes(React) + const match = createMatch(React) + + return { + /* components */ + Router, + Link, + IndexLink, + /* components (configuration) */ + IndexRoute, + Redirect, + Route, + /* mixins */ + History, + Lifecycle, + RouteContext, + + /* utils */ + useRoutes, + createRoutes, + RoutingContext, + PropTypes, + match + } +} diff --git a/modules/index.js b/modules/index.js index a8d7adf8e4..fa382559d6 100644 --- a/modules/index.js +++ b/modules/index.js @@ -1,23 +1,23 @@ -/* components */ -export Router from './Router' -export Link from './Link' -export IndexLink from './IndexLink' +import React from 'react' +import createAll from './createAll' -/* components (configuration) */ -export IndexRoute from './IndexRoute' -export Redirect from './Redirect' -export Route from './Route' +const all = createAll(React) -/* mixins */ -export History from './History' -export Lifecycle from './Lifecycle' -export RouteContext from './RouteContext' +export const { + Router, + Link, + IndexLink, + IndexRoute, + Redirect, + Route, + History, + Lifecycle, + RouteContext, + useRoutes, + createRoutes, + RoutingContext, + PropTypes, + match +} = all -/* utils */ -export useRoutes from './useRoutes' -export { createRoutes } from './RouteUtils' -export RoutingContext from './RoutingContext' -export PropTypes from './PropTypes' -export match from './match' - -export default from './Router' +export default all.Router diff --git a/modules/match.js b/modules/match.js index a6094c6aa5..fe98200df3 100644 --- a/modules/match.js +++ b/modules/match.js @@ -1,25 +1,32 @@ import createMemoryHistory from 'history/lib/createMemoryHistory' -import useRoutes from './useRoutes' -import { createRoutes } from './RouteUtils' +import createUseRoutes from './useRoutes' +import createRouteUtils from './RouteUtils' -export default function match({ - routes, - history, - location, - parseQueryString, - stringifyQuery -}, cb) { - let createHistory = history ? () => history : createMemoryHistory +export default function createMatch(React) { - let staticHistory = useRoutes(createHistory)({ - routes: createRoutes(routes), + const useRoutes = createUseRoutes(React); + const { createRoutes } = createRouteUtils(React) + + function match({ + routes, + history, + location, parseQueryString, stringifyQuery - }) + }, cb) { + let createHistory = history ? () => history : createMemoryHistory - staticHistory.match(location, function (error, nextLocation, nextState) { - let renderProps = nextState ? {...nextState, history: staticHistory} : null - cb(error, nextLocation, renderProps) - }) -} + let staticHistory = useRoutes(createHistory)({ + routes: createRoutes(routes), + parseQueryString, + stringifyQuery + }) + staticHistory.match(location, function (error, nextLocation, nextState) { + let renderProps = nextState ? {...nextState, history: staticHistory} : null + cb(error, nextLocation, renderProps) + }) + } + + return match +} diff --git a/modules/matchRoutes.js b/modules/matchRoutes.js index a1c28c8211..e5e4e612c5 100644 --- a/modules/matchRoutes.js +++ b/modules/matchRoutes.js @@ -1,126 +1,132 @@ import { loopAsync } from './AsyncUtils' import { matchPattern } from './PatternUtils' -import { createRoutes } from './RouteUtils' - -function getChildRoutes(route, location, callback) { - if (route.childRoutes) { - callback(null, route.childRoutes) - } else if (route.getChildRoutes) { - route.getChildRoutes(location, function(error, childRoutes) { - callback(error, !error && createRoutes(childRoutes)) - }) - } else { - callback() - } -} +import createRouteUtils from './RouteUtils' -function getIndexRoute(route, location, callback) { - if (route.indexRoute) { - callback(null, route.indexRoute) - } else if (route.getIndexRoute) { - route.getIndexRoute(location, function(error, indexRoute) { - callback(error, !error && createRoutes(indexRoute)[0]) - }) - } else { - callback() - } -} +export default function createMatchRoutes(React) { -function assignParams(params, paramNames, paramValues) { - return paramNames.reduceRight(function (params, paramName, index) { - const paramValue = paramValues && paramValues[index] + const { createRoutes } = createRouteUtils(React) - if (Array.isArray(params[paramName])) { - params[paramName].unshift(paramValue) - } else if (paramName in params) { - params[paramName] = [ paramValue, params[paramName] ] + function getChildRoutes(route, location, callback) { + if (route.childRoutes) { + callback(null, route.childRoutes) + } else if (route.getChildRoutes) { + route.getChildRoutes(location, function(error, childRoutes) { + callback(error, !error && createRoutes(childRoutes)) + }) } else { - params[paramName] = paramValue + callback() } + } - return params - }, params) -} + function getIndexRoute(route, location, callback) { + if (route.indexRoute) { + callback(null, route.indexRoute) + } else if (route.getIndexRoute) { + route.getIndexRoute(location, function(error, indexRoute) { + callback(error, !error && createRoutes(indexRoute)[0]) + }) + } else { + callback() + } + } -function createParams(paramNames, paramValues) { - return assignParams({}, paramNames, paramValues) -} + function assignParams(params, paramNames, paramValues) { + return paramNames.reduceRight(function (params, paramName, index) { + const paramValue = paramValues && paramValues[index] -function matchRouteDeep(basename, route, location, callback) { - let pattern = route.path || '' + if (Array.isArray(params[paramName])) { + params[paramName].unshift(paramValue) + } else if (paramName in params) { + params[paramName] = [ paramValue, params[paramName] ] + } else { + params[paramName] = paramValue + } - if (pattern.indexOf('/') !== 0) - pattern = basename.replace(/\/*$/, '/') + pattern // Relative paths build on the parent's path. + return params + }, params) + } - const { remainingPathname, paramNames, paramValues } = matchPattern(pattern, location.pathname) - const isExactMatch = remainingPathname === '' + function createParams(paramNames, paramValues) { + return assignParams({}, paramNames, paramValues) + } - if (isExactMatch && route.path) { - const match = { - routes: [ route ], - params: createParams(paramNames, paramValues) - } + function matchRouteDeep(basename, route, location, callback) { + let pattern = route.path || '' - getIndexRoute(route, location, function (error, indexRoute) { - if (error) { - callback(error) - } else { - if (indexRoute) - match.routes.push(indexRoute) + if (pattern.indexOf('/') !== 0) + pattern = basename.replace(/\/*$/, '/') + pattern // Relative paths build on the parent's path. - callback(null, match) - } - }) - } else if (remainingPathname != null || route.childRoutes) { - // Either a) this route matched at least some of the path or b) - // we don't have to load this route's children asynchronously. In - // either case continue checking for matches in the subtree. - getChildRoutes(route, location, function (error, childRoutes) { - if (error) { - callback(error) - } else if (childRoutes) { - // Check the child routes to see if any of them match. - matchRoutes(childRoutes, location, function (error, match) { - if (error) { - callback(error) - } else if (match) { - // A child route matched! Augment the match and pass it up the stack. - match.routes.unshift(route) - callback(null, match) - } else { - callback() - } - }, pattern) - } else { - callback() + const { remainingPathname, paramNames, paramValues } = matchPattern(pattern, location.pathname) + const isExactMatch = remainingPathname === '' + + if (isExactMatch && route.path) { + const match = { + routes: [ route ], + params: createParams(paramNames, paramValues) } - }) - } else { - callback() + + getIndexRoute(route, location, function (error, indexRoute) { + if (error) { + callback(error) + } else { + if (indexRoute) + match.routes.push(indexRoute) + + callback(null, match) + } + }) + } else if (remainingPathname != null || route.childRoutes) { + // Either a) this route matched at least some of the path or b) + // we don't have to load this route's children asynchronously. In + // either case continue checking for matches in the subtree. + getChildRoutes(route, location, function (error, childRoutes) { + if (error) { + callback(error) + } else if (childRoutes) { + // Check the child routes to see if any of them match. + matchRoutes(childRoutes, location, function (error, match) { + if (error) { + callback(error) + } else if (match) { + // A child route matched! Augment the match and pass it up the stack. + match.routes.unshift(route) + callback(null, match) + } else { + callback() + } + }, pattern) + } else { + callback() + } + }) + } else { + callback() + } } -} -/** - * Asynchronously matches the given location to a set of routes and calls - * callback(error, state) when finished. The state object will have the - * following properties: - * - * - routes An array of routes that matched, in hierarchical order - * - params An object of URL parameters - * - * Note: This operation may finish synchronously if no routes have an - * asynchronous getChildRoutes method. - */ -function matchRoutes(routes, location, callback, basename='') { - loopAsync(routes.length, function (index, next, done) { - matchRouteDeep(basename, routes[index], location, function (error, match) { - if (error || match) { - done(error, match) - } else { - next() - } - }) - }, callback) -} + /** + * Asynchronously matches the given location to a set of routes and calls + * callback(error, state) when finished. The state object will have the + * following properties: + * + * - routes An array of routes that matched, in hierarchical order + * - params An object of URL parameters + * + * Note: This operation may finish synchronously if no routes have an + * asynchronous getChildRoutes method. + */ + function matchRoutes(routes, location, callback, basename='') { + loopAsync(routes.length, function (index, next, done) { + matchRouteDeep(basename, routes[index], location, function (error, match) { + if (error || match) { + done(error, match) + } else { + next() + } + }) + }, callback) + } + + return matchRoutes -export default matchRoutes +} diff --git a/modules/native.js b/modules/native.js new file mode 100644 index 0000000000..f1d956271f --- /dev/null +++ b/modules/native.js @@ -0,0 +1,23 @@ +import React from 'react-native' +import createAll from './createAll' + +const all = createAll(React) + +export const { + Router, + Link, + IndexLink, + IndexRoute, + Redirect, + Route, + History, + Lifecycle, + RouteContext, + useRoutes, + createRoutes, + RoutingContext, + PropTypes, + match +} = all + +export default all.Router diff --git a/modules/useRoutes.js b/modules/useRoutes.js index 8fd9ca26d4..8dcec5404b 100644 --- a/modules/useRoutes.js +++ b/modules/useRoutes.js @@ -6,235 +6,241 @@ import computeChangedRoutes from './computeChangedRoutes' import { runEnterHooks, runLeaveHooks } from './TransitionUtils' import { default as _isActive } from './isActive' import getComponents from './getComponents' -import matchRoutes from './matchRoutes' +import createMatchRoutes from './matchRoutes' -function hasAnyProperties(object) { - for (const p in object) - if (object.hasOwnProperty(p)) - return true +export default function createUseRoutes(React) { - return false -} + const matchRoutes = createMatchRoutes(React) -/** - * Returns a new createHistory function that may be used to create - * history objects that know about routing. - * - * - isActive(pathname, query) - * - registerRouteHook(route, (location) => {}) - * - unregisterRouteHook(route, (location) => {}) - * - match(location, (error, nextState, nextLocation) => {}) - * - listen((error, nextState) => {}) - */ -function useRoutes(createHistory) { - return function (options={}) { - let { routes, ...historyOptions } = options - let history = useQueries(createHistory)(historyOptions) - let state = {} - - function isActive(pathname, query, indexOnly=false) { - return _isActive(pathname, query, indexOnly, state.location, state.routes, state.params) - } + function hasAnyProperties(object) { + for (const p in object) + if (object.hasOwnProperty(p)) + return true - let partialNextState + return false + } - function match(location, callback) { - if (partialNextState && partialNextState.location === location) { - // Continue from where we left off. - finishMatch(partialNextState, callback) - } else { - matchRoutes(routes, location, function (error, nextState) { - if (error) { - callback(error, null, null) - } else if (nextState) { - finishMatch({ ...nextState, location }, function (err, nextLocation, nextState) { - if (nextState) - state = nextState - callback(err, nextLocation, nextState) - }) - } else { - callback(null, null, null) - } - }) + /** + * Returns a new createHistory function that may be used to create + * history objects that know about routing. + * + * - isActive(pathname, query) + * - registerRouteHook(route, (location) => {}) + * - unregisterRouteHook(route, (location) => {}) + * - match(location, (error, nextState, nextLocation) => {}) + * - listen((error, nextState) => {}) + */ + function useRoutes(createHistory) { + return function (options={}) { + let { routes, ...historyOptions } = options + let history = useQueries(createHistory)(historyOptions) + let state = {} + + function isActive(pathname, query, indexOnly=false) { + return _isActive(pathname, query, indexOnly, state.location, state.routes, state.params) } - } - - function createLocationFromRedirectInfo({ pathname, query, state }) { - return createLocation( - history.createPath(pathname, query), state, REPLACE, history.createKey() - ) - } - - function finishMatch(nextState, callback) { - let { leaveRoutes, enterRoutes } = computeChangedRoutes(state, nextState) - runLeaveHooks(leaveRoutes) + let partialNextState - runEnterHooks(enterRoutes, nextState, function (error, redirectInfo) { - if (error) { - callback(error) - } else if (redirectInfo) { - callback(null, createLocationFromRedirectInfo(redirectInfo), null) + function match(location, callback) { + if (partialNextState && partialNextState.location === location) { + // Continue from where we left off. + finishMatch(partialNextState, callback) } else { - // TODO: Fetch components after state is updated. - getComponents(nextState, function (error, components) { + matchRoutes(routes, location, function (error, nextState) { if (error) { - callback(error) + callback(error, null, null) + } else if (nextState) { + finishMatch({ ...nextState, location }, function (err, nextLocation, nextState) { + if (nextState) + state = nextState + callback(err, nextLocation, nextState) + }) } else { - callback(null, null, { ...nextState, components }) + callback(null, null, null) } }) } - }) - } - - const RouteHooks = {} + } - let RouteGuid = 1 + function createLocationFromRedirectInfo({ pathname, query, state }) { + return createLocation( + history.createPath(pathname, query), state, REPLACE, history.createKey() + ) + } - function getRouteID(route) { - return route.__id__ || (route.__id__ = RouteGuid++) - } + function finishMatch(nextState, callback) { + let { leaveRoutes, enterRoutes } = computeChangedRoutes(state, nextState) - function getRouteHooksForRoutes(routes) { - return routes.reduce(function (hooks, route) { - hooks.push.apply(hooks, RouteHooks[getRouteID(route)]) - return hooks - }, []) - } + runLeaveHooks(leaveRoutes) - function transitionHook(location, callback) { - matchRoutes(routes, location, function (error, nextState) { - if (nextState == null) { - // TODO: We didn't actually match anything, but hang - // onto error/nextState so we don't have to matchRoutes - // again in the listen callback. - callback() - return - } + runEnterHooks(enterRoutes, nextState, function (error, redirectInfo) { + if (error) { + callback(error) + } else if (redirectInfo) { + callback(null, createLocationFromRedirectInfo(redirectInfo), null) + } else { + // TODO: Fetch components after state is updated. + getComponents(nextState, function (error, components) { + if (error) { + callback(error) + } else { + callback(null, null, { ...nextState, components }) + } + }) + } + }) + } - // Cache some state here so we don't have to - // matchRoutes() again in the listen callback. - partialNextState = { ...nextState, location } + const RouteHooks = {} - let hooks = getRouteHooksForRoutes( - computeChangedRoutes(state, nextState).leaveRoutes - ) + let RouteGuid = 1 - let result - for (let i = 0, len = hooks.length; result == null && i < len; ++i) { - // Passing the location arg here indicates to - // the user that this is a transition hook. - result = hooks[i](location) - } + function getRouteID(route) { + return route.__id__ || (route.__id__ = RouteGuid++) + } - callback(result) - }) - } + function getRouteHooksForRoutes(routes) { + return routes.reduce(function (hooks, route) { + hooks.push.apply(hooks, RouteHooks[getRouteID(route)]) + return hooks + }, []) + } - function beforeUnloadHook() { - // Synchronously check to see if any route hooks want to - // prevent the current window/tab from closing. - if (state.routes) { - let hooks = getRouteHooksForRoutes(state.routes) - - let message - for (let i = 0, len = hooks.length; typeof message !== 'string' && i < len; ++i) { - // Passing no args indicates to the user that this is a - // beforeunload hook. We don't know the next location. - message = hooks[i]() - } + function transitionHook(location, callback) { + matchRoutes(routes, location, function (error, nextState) { + if (nextState == null) { + // TODO: We didn't actually match anything, but hang + // onto error/nextState so we don't have to matchRoutes + // again in the listen callback. + callback() + return + } - return message - } - } + // Cache some state here so we don't have to + // matchRoutes() again in the listen callback. + partialNextState = { ...nextState, location } - function registerRouteHook(route, hook) { - // TODO: Warn if they register for a route that isn't currently - // active. They're probably doing something wrong, like re-creating - // route objects on every location change. - let routeID = getRouteID(route) - let hooks = RouteHooks[routeID] + let hooks = getRouteHooksForRoutes( + computeChangedRoutes(state, nextState).leaveRoutes + ) - if (hooks == null) { - let thereWereNoRouteHooks = !hasAnyProperties(RouteHooks) + let result + for (let i = 0, len = hooks.length; result == null && i < len; ++i) { + // Passing the location arg here indicates to + // the user that this is a transition hook. + result = hooks[i](location) + } - hooks = RouteHooks[routeID] = [ hook ] + callback(result) + }) + } - if (thereWereNoRouteHooks) { - history.registerTransitionHook(transitionHook) + function beforeUnloadHook() { + // Synchronously check to see if any route hooks want to + // prevent the current window/tab from closing. + if (state.routes) { + let hooks = getRouteHooksForRoutes(state.routes) + + let message + for (let i = 0, len = hooks.length; typeof message !== 'string' && i < len; ++i) { + // Passing no args indicates to the user that this is a + // beforeunload hook. We don't know the next location. + message = hooks[i]() + } - if (history.registerBeforeUnloadHook) - history.registerBeforeUnloadHook(beforeUnloadHook) + return message } - } else if (hooks.indexOf(hook) === -1) { - hooks.push(hook) } - } - function unregisterRouteHook(route, hook) { - let routeID = getRouteID(route) - let hooks = RouteHooks[routeID] + function registerRouteHook(route, hook) { + // TODO: Warn if they register for a route that isn't currently + // active. They're probably doing something wrong, like re-creating + // route objects on every location change. + let routeID = getRouteID(route) + let hooks = RouteHooks[routeID] - if (hooks != null) { - let newHooks = hooks.filter(item => item !== hook) + if (hooks == null) { + let thereWereNoRouteHooks = !hasAnyProperties(RouteHooks) - if (newHooks.length === 0) { - delete RouteHooks[routeID] + hooks = RouteHooks[routeID] = [ hook ] - if (!hasAnyProperties(RouteHooks)) { - history.unregisterTransitionHook(transitionHook) + if (thereWereNoRouteHooks) { + history.registerTransitionHook(transitionHook) - if (history.unregisterBeforeUnloadHook) - history.unregisterBeforeUnloadHook(beforeUnloadHook) + if (history.registerBeforeUnloadHook) + history.registerBeforeUnloadHook(beforeUnloadHook) } - } else { - RouteHooks[routeID] = newHooks + } else if (hooks.indexOf(hook) === -1) { + hooks.push(hook) } } - } - /** - * This is the API for stateful environments. As the location changes, - * we update state and call the listener. Benefits of this API are: - * - * - We automatically manage state on the client - * - We automatically handle redirects on the client - * - We warn when the location doesn't match any routes - */ - function listen(listener) { - return history.listen(function (location) { - if (state.location === location) { - listener(null, state) - } else { - match(location, function (error, nextLocation, nextState) { - if (error) { - listener(error) - } else if (nextState) { - listener(null, state) // match mutates state to nextState - } else if (nextLocation) { - history.transitionTo(nextLocation) - } else { - warning( - false, - 'Location "%s" did not match any routes', - location.pathname + location.search - ) + function unregisterRouteHook(route, hook) { + let routeID = getRouteID(route) + let hooks = RouteHooks[routeID] + + if (hooks != null) { + let newHooks = hooks.filter(item => item !== hook) + + if (newHooks.length === 0) { + delete RouteHooks[routeID] + + if (!hasAnyProperties(RouteHooks)) { + history.unregisterTransitionHook(transitionHook) + + if (history.unregisterBeforeUnloadHook) + history.unregisterBeforeUnloadHook(beforeUnloadHook) } - }) + } else { + RouteHooks[routeID] = newHooks + } } - }) - } + } - return { - ...history, - isActive, - registerRouteHook, - unregisterRouteHook, - listen, - match + /** + * This is the API for stateful environments. As the location changes, + * we update state and call the listener. Benefits of this API are: + * + * - We automatically manage state on the client + * - We automatically handle redirects on the client + * - We warn when the location doesn't match any routes + */ + function listen(listener) { + return history.listen(function (location) { + if (state.location === location) { + listener(null, state) + } else { + match(location, function (error, nextLocation, nextState) { + if (error) { + listener(error) + } else if (nextState) { + listener(null, state) // match mutates state to nextState + } else if (nextLocation) { + history.transitionTo(nextLocation) + } else { + warning( + false, + 'Location "%s" did not match any routes', + location.pathname + location.search + ) + } + }) + } + }) + } + + return { + ...history, + isActive, + registerRouteHook, + unregisterRouteHook, + listen, + match + } } } -} -export default useRoutes + return useRoutes + +} diff --git a/native.js b/native.js new file mode 100644 index 0000000000..0c3ed853ba --- /dev/null +++ b/native.js @@ -0,0 +1 @@ +module.exports = require('./lib/native');