diff --git a/modules/components/Link.js b/modules/components/Link.js index bf74e9b079..adaf4243f1 100644 --- a/modules/components/Link.js +++ b/modules/components/Link.js @@ -1,5 +1,5 @@ var React = require('react'); -var ActiveStore = require('../stores/ActiveStore'); +var ActiveState = require('../mixins/ActiveState'); var withoutProperties = require('../helpers/withoutProperties'); var transitionTo = require('../helpers/transitionTo'); var makeHref = require('../helpers/makeHref'); @@ -31,8 +31,11 @@ var RESERVED_PROPS = { * */ var Link = React.createClass({ + displayName: 'Link', + mixins: [ ActiveState ], + statics: { getUnreservedProps: function (props) { @@ -86,34 +89,17 @@ var Link = React.createClass({ return className; }, - componentWillMount: function () { - ActiveStore.addChangeListener(this.handleActiveChange); - }, - - componentDidMount: function () { - this.updateActive(); - }, - - componentWillUnmount: function () { - ActiveStore.removeChangeListener(this.handleActiveChange); - }, - - componentWillReceiveProps: function(props) { - var params = Link.getUnreservedProps(props); + componentWillReceiveProps: function (nextProps) { + var params = Link.getUnreservedProps(nextProps); this.setState({ - isActive: ActiveStore.isActive(props.to, params, props.query) + isActive: Link.isActive(nextProps.to, params, nextProps.query) }); }, - handleActiveChange: function () { - if (this.isMounted()) - this.updateActive(); - }, - - updateActive: function () { + updateActiveState: function () { this.setState({ - isActive: ActiveStore.isActive(this.props.to, this.getParams(), this.props.query) + isActive: Link.isActive(this.props.to, this.getParams(), this.props.query) }); }, diff --git a/modules/components/Route.js b/modules/components/Route.js index a6938b85dc..5d07169b7d 100644 --- a/modules/components/Route.js +++ b/modules/components/Route.js @@ -194,7 +194,7 @@ var Route = React.createClass({ if (transition.isCancelled) { Route.handleCancelledTransition(transition, route); } else if (newState) { - ActiveStore.update(newState); + ActiveStore.updateState(newState); } return transition; diff --git a/modules/main.js b/modules/main.js index d8b4c3587b..71110da104 100644 --- a/modules/main.js +++ b/modules/main.js @@ -5,6 +5,8 @@ exports.goBack = require('./helpers/goBack'); exports.replaceWith = require('./helpers/replaceWith'); exports.transitionTo = require('./helpers/transitionTo'); +exports.ActiveState = require('./mixins/ActiveState'); + // Backwards compat with 0.1. We should // remove this when we ship 1.0. exports.Router = require('./Router'); diff --git a/modules/mixins/ActiveState.js b/modules/mixins/ActiveState.js new file mode 100644 index 0000000000..2e9298d752 --- /dev/null +++ b/modules/mixins/ActiveState.js @@ -0,0 +1,97 @@ +var ActiveStore = require('../stores/ActiveStore'); + +function routeIsActive(activeRoutes, routeName) { + return activeRoutes.some(function (route) { + return route.props.name === routeName; + }); +} + +function paramsAreActive(activeParams, params) { + for (var property in params) { + if (activeParams[property] !== String(params[property])) + return false; + } + + return true; +} + +function queryIsActive(activeQuery, query) { + for (var property in query) { + if (activeQuery[property] !== String(query[property])) + return false; + } + + return true; +} + +/** + * A mixin for components that need to know about the routes, params, + * and query that are currently active. Components that use it get two + * things: + * + * 1. An `isActive` static method they can use to check if a route, + * params, and query are active. + * 2. An `updateActiveState` instance method that is called when the + * active state changes. + * + * Example: + * + * var Tab = React.createClass({ + * + * mixins: [ Router.ActiveState ], + * + * getInitialState: function () { + * return { + * isActive: false + * }; + * }, + * + * updateActiveState: function () { + * this.setState({ + * isActive: Tab.isActive(routeName, params, query) + * }) + * } + * + * }); + */ +var ActiveState = { + + statics: { + + /** + * Returns true if the route with the given name, URL parameters, and query + * are all currently active. + */ + isActive: function (routeName, params, query) { + var state = ActiveStore.getState(); + var isActive = routeIsActive(state.routes, routeName) && paramsAreActive(state.params, params); + + if (query) + return isActive && queryIsActive(state.query, query); + + return isActive; + } + + }, + + componentWillMount: function () { + ActiveStore.addChangeListener(this.handleActiveStateChange); + }, + + componentDidMount: function () { + if (this.updateActiveState) + this.updateActiveState(); + }, + + componentWillUnmount: function () { + ActiveStore.removeChangeListener(this.handleActiveStateChange); + }, + + handleActiveStateChange: function () { + if (this.isMounted() && this.updateActiveState) + this.updateActiveState(); + } + +}; + +module.exports = ActiveState; diff --git a/modules/stores/ActiveStore.js b/modules/stores/ActiveStore.js index 92eb3228fe..9a5b5390b4 100644 --- a/modules/stores/ActiveStore.js +++ b/modules/stores/ActiveStore.js @@ -1,33 +1,7 @@ var _activeRoutes = []; - -function routeIsActive(routeName) { - return _activeRoutes.some(function (route) { - return route.props.name === routeName; - }); -} - var _activeParams = {}; - -function paramsAreActive(params) { - for (var property in params) { - if (_activeParams[property] !== String(params[property])) - return false; - } - - return true; -} - var _activeQuery = {}; -function queryIsActive(query) { - for (var property in query) { - if (_activeQuery[property] !== String(query[property])) - return false; - } - - return true; -} - var EventEmitter = require('event-emitter'); var _events = EventEmitter(); @@ -42,27 +16,6 @@ function notifyChange() { */ var ActiveStore = { - update: function (state) { - state = state || {}; - _activeRoutes = state.activeRoutes || []; - _activeParams = state.activeParams || {}; - _activeQuery = state.activeQuery || {}; - notifyChange(); - }, - - /** - * Returns true if the route with the given name, URL parameters, and query - * are all currently active. - */ - isActive: function (routeName, params, query) { - var isActive = routeIsActive(routeName) && paramsAreActive(params); - - if (query) - isActive = isActive && queryIsActive(query); - - return isActive; - }, - /** * Adds a listener that will be called when this store changes. */ @@ -75,6 +28,32 @@ var ActiveStore = { */ removeChangeListener: function (listener) { _events.off('change', listener); + }, + + /** + * Updates the currently active state and notifies all listeners. + * This is automatically called by routes as they become active. + */ + updateState: function (state) { + state = state || {}; + + _activeRoutes = state.activeRoutes || []; + _activeParams = state.activeParams || {}; + _activeQuery = state.activeQuery || {}; + + notifyChange(); + }, + + /** + * Returns an object with the currently active `routes`, `params`, + * and `query`. + */ + getState: function () { + return { + routes: _activeRoutes, + params: _activeParams, + query: _activeQuery + }; } };