Skip to content

Commit

Permalink
[added] ActiveState mixin
Browse files Browse the repository at this point in the history
Components can mixin Router.ActiveState to receive notifications when
the active routes, params, and query change.

Fixes #85
  • Loading branch information
mjackson committed Jul 17, 2014
1 parent 616f8bf commit 8562482
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 71 deletions.
32 changes: 9 additions & 23 deletions modules/components/Link.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -31,8 +31,11 @@ var RESERVED_PROPS = {
* <Link to="showPost" postId="123" query={{show:true}}/>
*/
var Link = React.createClass({

displayName: 'Link',

mixins: [ ActiveState ],

statics: {

getUnreservedProps: function (props) {
Expand Down Expand Up @@ -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)
});
},

Expand Down
2 changes: 1 addition & 1 deletion modules/components/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions modules/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
97 changes: 97 additions & 0 deletions modules/mixins/ActiveState.js
Original file line number Diff line number Diff line change
@@ -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);

This comment has been minimized.

Copy link
@ryanflorence

ryanflorence Jul 17, 2014

Member

I don't think we want this do we? If I'm at /users?sortBy=name and /users?sortBy=age I probably still want <Link to="users"/> to be active.

But ... I'd probably also want <Link to="users" query={{sortBy: 'status'}}/> to not be active ... hmmmmmmmmmm

This comment has been minimized.

Copy link
@ryanflorence

ryanflorence Jul 17, 2014

Member

actually, as I look at the code, it appears this works how I'd like

Given: /users?sortBy=name

<Link to="users" query={{sortBy: 'name'}}/>: active
<Link to="users" query={{sortBy: 'age'}}/>: not active
<Link to="users"/>: active

Am I reading the code correctly?

This comment has been minimized.

Copy link
@mjackson

mjackson Jul 17, 2014

Author Member

Yeah, that's how it works.

This comment has been minimized.

Copy link
@ryanflorence

ryanflorence Jul 17, 2014

Member

🚀


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;
73 changes: 26 additions & 47 deletions modules/stores/ActiveStore.js
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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.
*/
Expand All @@ -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
};
}

};
Expand Down

1 comment on commit 8562482

@KyleAMathews
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Please sign in to comment.