From a4ce7c8272163191dfcd986a25e99bffe7cfbb54 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 29 Aug 2014 10:57:21 -0700 Subject: [PATCH] [changed] isActive is an instance method [removed] This commit removes ActiveStore (yay!). Instead, components now store their own active state and emit active state change events to ActiveState descendants that are interested. --- modules/components/Link.js | 4 +- modules/components/Routes.js | 16 ++---- modules/mixins/ActiveDelegate.js | 65 +++++++++++++++++++++++ modules/mixins/ActiveState.js | 43 ++++++++------- modules/mixins/ChangeEmitter.js | 50 ++++++++++++++++++ modules/stores/ActiveStore.js | 84 ------------------------------ specs/ActiveDelegate.spec.js | 89 ++++++++++++++++++++++++++++++++ specs/ActiveStore.spec.js | 70 ------------------------- specs/Routes.spec.js | 21 -------- specs/main.js | 2 +- 10 files changed, 236 insertions(+), 208 deletions(-) create mode 100644 modules/mixins/ActiveDelegate.js create mode 100644 modules/mixins/ChangeEmitter.js delete mode 100644 modules/stores/ActiveStore.js create mode 100644 specs/ActiveDelegate.spec.js delete mode 100644 specs/ActiveStore.spec.js diff --git a/modules/components/Link.js b/modules/components/Link.js index 5786e2374f..fe30013958 100644 --- a/modules/components/Link.js +++ b/modules/components/Link.js @@ -119,13 +119,13 @@ var Link = React.createClass({ var params = Link.getParams(nextProps); this.setState({ - isActive: Link.isActive(nextProps.to, params, nextProps.query) + isActive: this.isActive(nextProps.to, params, nextProps.query) }); }, updateActiveState: function () { this.setState({ - isActive: Link.isActive(this.props.to, Link.getParams(this.props), this.props.query) + isActive: this.isActive(this.props.to, Link.getParams(this.props), this.props.query) }); }, diff --git a/modules/components/Routes.js b/modules/components/Routes.js index c96e39af4a..893e24964f 100644 --- a/modules/components/Routes.js +++ b/modules/components/Routes.js @@ -11,7 +11,7 @@ var DefaultLocation = require('../locations/DefaultLocation'); var HashLocation = require('../locations/HashLocation'); var HistoryLocation = require('../locations/HistoryLocation'); var RefreshLocation = require('../locations/RefreshLocation'); -var ActiveStore = require('../stores/ActiveStore'); +var ActiveDelegate = require('../mixins/ActiveDelegate'); var PathStore = require('../stores/PathStore'); var RouteStore = require('../stores/RouteStore'); @@ -43,13 +43,6 @@ function defaultAbortedTransitionHandler(transition) { } } -/** - * The default handler for active state updates. - */ -function defaultActiveStateChangeHandler(state) { - ActiveStore.updateState(state); -} - /** * The default handler for errors that were thrown asynchronously * while transitioning. The default behavior is to re-throw the @@ -74,9 +67,10 @@ var Routes = React.createClass({ displayName: 'Routes', + mixins: [ ActiveDelegate ], + propTypes: { onAbortedTransition: React.PropTypes.func.isRequired, - onActiveStateChange: React.PropTypes.func.isRequired, onTransitionError: React.PropTypes.func.isRequired, preserveScrollPosition: React.PropTypes.bool, location: function (props, propName, componentName) { @@ -90,7 +84,6 @@ var Routes = React.createClass({ getDefaultProps: function () { return { onAbortedTransition: defaultAbortedTransitionHandler, - onActiveStateChange: defaultActiveStateChangeHandler, onTransitionError: defaultTransitionErrorHandler, preserveScrollPosition: false, location: DefaultLocation @@ -182,8 +175,7 @@ var Routes = React.createClass({ if (transition.isAborted) { routes.props.onAbortedTransition(transition); } else if (nextState) { - routes.setState(nextState); - routes.props.onActiveStateChange(nextState); + routes.setState(nextState, routes.emitChange); // TODO: add functional test var rootMatch = getRootMatch(nextState.matches); diff --git a/modules/mixins/ActiveDelegate.js b/modules/mixins/ActiveDelegate.js new file mode 100644 index 0000000000..b2ae3a5c44 --- /dev/null +++ b/modules/mixins/ActiveDelegate.js @@ -0,0 +1,65 @@ +var React = require('react'); +var ChangeEmitter = require('./ChangeEmitter'); + +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 store the active state of routes, URL + * parameters, and query. + */ +var ActiveDelegate = { + + mixins: [ ChangeEmitter ], + + childContextTypes: { + activeDelegate: React.PropTypes.any.isRequired + }, + + getChildContext: function () { + return { + activeDelegate: this + }; + }, + + /** + * Returns true if the route with the given name, URL parameters, and + * query are all currently active. + */ + isActive: function (routeName, params, query) { + var activeRoutes = this.state.activeRoutes || []; + var activeParams = this.state.activeParams || {}; + var activeQuery = this.state.activeQuery || {}; + + var isActive = routeIsActive(activeRoutes, routeName) && paramsAreActive(activeParams, params); + + if (query) + return isActive && queryIsActive(activeQuery, query); + + return isActive; + } + +}; + +module.exports = ActiveDelegate; diff --git a/modules/mixins/ActiveState.js b/modules/mixins/ActiveState.js index 7e890388f0..ff5f7eb877 100644 --- a/modules/mixins/ActiveState.js +++ b/modules/mixins/ActiveState.js @@ -1,14 +1,16 @@ -var ActiveStore = require('../stores/ActiveStore'); +var React = require('react'); +var ActiveDelegate = require('./ActiveDelegate'); /** * 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 + * 1. An `updateActiveState` method that is called when the * active state changes. + * 2. An `isActive` method they can use to check if a route, + * params, and query are active. + * * * Example: * @@ -24,7 +26,7 @@ var ActiveStore = require('../stores/ActiveStore'); * * updateActiveState: function () { * this.setState({ - * isActive: Tab.isActive(routeName, params, query) + * isActive: this.isActive(routeName, params, query) * }) * } * @@ -32,32 +34,37 @@ var ActiveStore = require('../stores/ActiveStore'); */ var ActiveState = { - statics: { - - /** - * Returns true if the route with the given name, URL parameters, and query - * are all currently active. - */ - isActive: ActiveStore.isActive - + contextTypes: { + activeDelegate: React.PropTypes.any.isRequired }, - componentWillMount: function () { - ActiveStore.addChangeListener(this.handleActiveStateChange); + /** + * Returns this component's ActiveDelegate component. + */ + getActiveDelegate: function () { + return this.context.activeDelegate; }, componentDidMount: function () { - if (this.updateActiveState) - this.updateActiveState(); + this.getActiveDelegate().addChangeListener(this.handleActiveStateChange); + this.handleActiveStateChange(); }, componentWillUnmount: function () { - ActiveStore.removeChangeListener(this.handleActiveStateChange); + this.getActiveDelegate().removeChangeListener(this.handleActiveStateChange); }, handleActiveStateChange: function () { if (this.isMounted() && typeof this.updateActiveState === 'function') this.updateActiveState(); + }, + + /** + * Returns true if the route with the given name, URL parameters, and + * query are all currently active. + */ + isActive: function (routeName, params, query) { + return this.getActiveDelegate().isActive(routeName, params, query); } }; diff --git a/modules/mixins/ChangeEmitter.js b/modules/mixins/ChangeEmitter.js new file mode 100644 index 0000000000..106ed9f3b9 --- /dev/null +++ b/modules/mixins/ChangeEmitter.js @@ -0,0 +1,50 @@ +var React = require('react'); +var EventEmitter = require('events').EventEmitter; + +var CHANGE_EVENT = 'change'; + +/** + * A mixin for components that emit change events. ActiveDelegate uses + * this mixin to notify descendant ActiveState components when the + * active state changes. + */ +var ChangeEmitter = { + + propTypes: { + maxChangeListeners: React.PropTypes.number.isRequired + }, + + getDefaultProps: function () { + return { + maxChangeListeners: 0 + }; + }, + + componentWillMount: function () { + this._events = new EventEmitter; + this._events.setMaxListeners(this.props.maxChangeListeners); + }, + + componentWillReceiveProps: function (nextProps) { + this._events.setMaxListeners(nextProps.maxChangeListeners); + }, + + componentWillUnmount: function () { + this._events.removeAllListeners(); + }, + + addChangeListener: function (listener) { + this._events.addListener(CHANGE_EVENT, listener); + }, + + removeChangeListener: function (listener) { + this._events.removeListener(CHANGE_EVENT, listener); + }, + + emitChange: function () { + this._events.emit(CHANGE_EVENT); + } + +}; + +module.exports = ChangeEmitter; diff --git a/modules/stores/ActiveStore.js b/modules/stores/ActiveStore.js deleted file mode 100644 index f56db8c296..0000000000 --- a/modules/stores/ActiveStore.js +++ /dev/null @@ -1,84 +0,0 @@ -var EventEmitter = require('events').EventEmitter; - -var CHANGE_EVENT = 'change'; -var _events = new EventEmitter; - -_events.setMaxListeners(0); - -function notifyChange() { - _events.emit(CHANGE_EVENT); -} - -var _activeRoutes = []; -var _activeParams = {}; -var _activeQuery = {}; - -function routeIsActive(routeName) { - return _activeRoutes.some(function (route) { - return route.props.name === routeName; - }); -} - -function paramsAreActive(params) { - for (var property in params) { - if (_activeParams[property] !== String(params[property])) - return false; - } - - return true; -} - -function queryIsActive(query) { - for (var property in query) { - if (_activeQuery[property] !== String(query[property])) - return false; - } - - return true; -} - -/** - * The ActiveStore keeps track of which routes, URL and query parameters are - * currently active on a page. s subscribe to the ActiveStore to know - * whether or not they are active. - */ -var ActiveStore = { - - addChangeListener: function (listener) { - _events.on(CHANGE_EVENT, listener); - }, - - removeChangeListener: function (listener) { - _events.removeListener(CHANGE_EVENT, 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 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) - return isActive && queryIsActive(query); - - return isActive; - } - -}; - -module.exports = ActiveStore; diff --git a/specs/ActiveDelegate.spec.js b/specs/ActiveDelegate.spec.js new file mode 100644 index 0000000000..5b690ab1f6 --- /dev/null +++ b/specs/ActiveDelegate.spec.js @@ -0,0 +1,89 @@ +require('./helper'); +var Route = require('../modules/components/Route'); +var ActiveDelegate = require('../modules/mixins/ActiveDelegate'); + +var App = React.createClass({ + displayName: 'App', + mixins: [ ActiveDelegate ], + getInitialState: function () { + return this.props.initialState; + }, + render: function () { + return React.DOM.div(); + } +}); + +describe('when a Route is active', function () { + var route; + beforeEach(function () { + route = Route({ name: 'products', handler: App }); + }); + + describe('and it has no params', function () { + var app; + beforeEach(function () { + app = ReactTestUtils.renderIntoDocument( + App({ + initialState: { + activeRoutes: [ route ] + } + }) + ); + }); + + it('is active', function () { + assert(app.isActive('products')); + }); + }); + + describe('and the right params are given', function () { + var app; + beforeEach(function () { + app = ReactTestUtils.renderIntoDocument( + App({ + initialState: { + activeRoutes: [ route ], + activeParams: { id: '123', show: 'true' }, + activeQuery: { search: 'abc' } + } + }) + ); + }); + + describe('and no query is used', function () { + it('is active', function () { + assert(app.isActive('products', { id: 123 })); + }); + }); + + describe('and a matching query is used', function () { + it('is active', function () { + assert(app.isActive('products', { id: 123 }, { search: 'abc' })); + }); + }); + + describe('but the query does not match', function () { + it('is not active', function () { + refute(app.isActive('products', { id: 123 }, { search: 'def' })); + }); + }); + }); + + describe('and the wrong params are given', function () { + var app; + beforeEach(function () { + app = ReactTestUtils.renderIntoDocument( + App({ + initialState: { + activeRoutes: [ route ], + activeParams: { id: 123 } + } + }) + ); + }); + + it('is not active', function () { + refute(app.isActive('products', { id: 345 })); + }); + }); +}); diff --git a/specs/ActiveStore.spec.js b/specs/ActiveStore.spec.js deleted file mode 100644 index 61bb854a69..0000000000 --- a/specs/ActiveStore.spec.js +++ /dev/null @@ -1,70 +0,0 @@ -require('./helper'); -var Route = require('../modules/components/Route'); -var ActiveStore = require('../modules/stores/ActiveStore'); - -var App = React.createClass({ - displayName: 'App', - render: function () { - return React.DOM.div(); - } -}); - -describe('when a Route is active', function () { - var route; - beforeEach(function () { - route = Route({ name: 'products', handler: App }); - }); - - describe('and it has no params', function () { - beforeEach(function () { - ActiveStore.updateState({ - activeRoutes: [ route ] - }); - }); - - it('is active', function () { - assert(ActiveStore.isActive('products')); - }); - }); - - describe('and the right params are given', function () { - beforeEach(function () { - ActiveStore.updateState({ - activeRoutes: [ route ], - activeParams: { id: '123', show: 'true' }, - activeQuery: { search: 'abc' } - }); - }); - - describe('and no query is used', function () { - it('is active', function () { - assert(ActiveStore.isActive('products', { id: 123 })); - }); - }); - - describe('and a matching query is used', function () { - it('is active', function () { - assert(ActiveStore.isActive('products', { id: 123 }, { search: 'abc' })); - }); - }); - - describe('but the query does not match', function () { - it('is not active', function () { - refute(ActiveStore.isActive('products', { id: 123 }, { search: 'def' })); - }); - }); - }); - - describe('and the wrong params are given', function () { - beforeEach(function () { - ActiveStore.updateState({ - activeRoutes: [ route ], - activeParams: { id: 123 } - }); - }); - - it('is not active', function () { - refute(ActiveStore.isActive('products', { id: 345 })); - }); - }); -}); diff --git a/specs/Routes.spec.js b/specs/Routes.spec.js index c95c6cd782..bd2ce08be8 100644 --- a/specs/Routes.spec.js +++ b/specs/Routes.spec.js @@ -30,27 +30,6 @@ describe('a Routes', function () { }); }); - describe('when there is a change in active state', function () { - it('triggers onActiveStateChange', function (done) { - var App = React.createClass({ - render: function () { - return React.DOM.div(); - } - }); - - function handleActiveStateChange(state) { - assert(state); - done(); - } - - var routes = ReactTestUtils.renderIntoDocument( - Routes({ onActiveStateChange: handleActiveStateChange }, - Route({ handler: App }) - ) - ); - }); - }); - describe('when there is an error in a transition hook', function () { it('triggers onTransitionError', function (done) { var App = React.createClass({ diff --git a/specs/main.js b/specs/main.js index 9fbdd8970a..c5865e73c4 100644 --- a/specs/main.js +++ b/specs/main.js @@ -1,4 +1,4 @@ -require('./ActiveStore.spec.js'); +require('./ActiveDelegate.spec.js'); require('./AsyncState.spec.js'); require('./DefaultRoute.spec.js'); require('./NotFoundRoute.spec.js');