Skip to content

Commit a4ce7c8

Browse files
mjacksonryanflorence
authored andcommittedSep 2, 2014
[changed] isActive is an instance method
[removed] <Routes onActiveStateChange> This commit removes ActiveStore (yay!). Instead, <Routes> components now store their own active state and emit active state change events to ActiveState descendants that are interested.
1 parent af1fb6e commit a4ce7c8

10 files changed

+236
-208
lines changed
 

‎modules/components/Link.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ var Link = React.createClass({
119119
var params = Link.getParams(nextProps);
120120

121121
this.setState({
122-
isActive: Link.isActive(nextProps.to, params, nextProps.query)
122+
isActive: this.isActive(nextProps.to, params, nextProps.query)
123123
});
124124
},
125125

126126
updateActiveState: function () {
127127
this.setState({
128-
isActive: Link.isActive(this.props.to, Link.getParams(this.props), this.props.query)
128+
isActive: this.isActive(this.props.to, Link.getParams(this.props), this.props.query)
129129
});
130130
},
131131

‎modules/components/Routes.js

+4-12
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ var DefaultLocation = require('../locations/DefaultLocation');
1111
var HashLocation = require('../locations/HashLocation');
1212
var HistoryLocation = require('../locations/HistoryLocation');
1313
var RefreshLocation = require('../locations/RefreshLocation');
14-
var ActiveStore = require('../stores/ActiveStore');
14+
var ActiveDelegate = require('../mixins/ActiveDelegate');
1515
var PathStore = require('../stores/PathStore');
1616
var RouteStore = require('../stores/RouteStore');
1717

@@ -43,13 +43,6 @@ function defaultAbortedTransitionHandler(transition) {
4343
}
4444
}
4545

46-
/**
47-
* The default handler for active state updates.
48-
*/
49-
function defaultActiveStateChangeHandler(state) {
50-
ActiveStore.updateState(state);
51-
}
52-
5346
/**
5447
* The default handler for errors that were thrown asynchronously
5548
* while transitioning. The default behavior is to re-throw the
@@ -74,9 +67,10 @@ var Routes = React.createClass({
7467

7568
displayName: 'Routes',
7669

70+
mixins: [ ActiveDelegate ],
71+
7772
propTypes: {
7873
onAbortedTransition: React.PropTypes.func.isRequired,
79-
onActiveStateChange: React.PropTypes.func.isRequired,
8074
onTransitionError: React.PropTypes.func.isRequired,
8175
preserveScrollPosition: React.PropTypes.bool,
8276
location: function (props, propName, componentName) {
@@ -90,7 +84,6 @@ var Routes = React.createClass({
9084
getDefaultProps: function () {
9185
return {
9286
onAbortedTransition: defaultAbortedTransitionHandler,
93-
onActiveStateChange: defaultActiveStateChangeHandler,
9487
onTransitionError: defaultTransitionErrorHandler,
9588
preserveScrollPosition: false,
9689
location: DefaultLocation
@@ -182,8 +175,7 @@ var Routes = React.createClass({
182175
if (transition.isAborted) {
183176
routes.props.onAbortedTransition(transition);
184177
} else if (nextState) {
185-
routes.setState(nextState);
186-
routes.props.onActiveStateChange(nextState);
178+
routes.setState(nextState, routes.emitChange);
187179

188180
// TODO: add functional test
189181
var rootMatch = getRootMatch(nextState.matches);

‎modules/mixins/ActiveDelegate.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
var React = require('react');
2+
var ChangeEmitter = require('./ChangeEmitter');
3+
4+
function routeIsActive(activeRoutes, routeName) {
5+
return activeRoutes.some(function (route) {
6+
return route.props.name === routeName;
7+
});
8+
}
9+
10+
function paramsAreActive(activeParams, params) {
11+
for (var property in params) {
12+
if (activeParams[property] !== String(params[property]))
13+
return false;
14+
}
15+
16+
return true;
17+
}
18+
19+
function queryIsActive(activeQuery, query) {
20+
for (var property in query) {
21+
if (activeQuery[property] !== String(query[property]))
22+
return false;
23+
}
24+
25+
return true;
26+
}
27+
28+
/**
29+
* A mixin for components that store the active state of routes, URL
30+
* parameters, and query.
31+
*/
32+
var ActiveDelegate = {
33+
34+
mixins: [ ChangeEmitter ],
35+
36+
childContextTypes: {
37+
activeDelegate: React.PropTypes.any.isRequired
38+
},
39+
40+
getChildContext: function () {
41+
return {
42+
activeDelegate: this
43+
};
44+
},
45+
46+
/**
47+
* Returns true if the route with the given name, URL parameters, and
48+
* query are all currently active.
49+
*/
50+
isActive: function (routeName, params, query) {
51+
var activeRoutes = this.state.activeRoutes || [];
52+
var activeParams = this.state.activeParams || {};
53+
var activeQuery = this.state.activeQuery || {};
54+
55+
var isActive = routeIsActive(activeRoutes, routeName) && paramsAreActive(activeParams, params);
56+
57+
if (query)
58+
return isActive && queryIsActive(activeQuery, query);
59+
60+
return isActive;
61+
}
62+
63+
};
64+
65+
module.exports = ActiveDelegate;

‎modules/mixins/ActiveState.js

+25-18
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
var ActiveStore = require('../stores/ActiveStore');
1+
var React = require('react');
2+
var ActiveDelegate = require('./ActiveDelegate');
23

34
/**
45
* A mixin for components that need to know about the routes, params,
56
* and query that are currently active. Components that use it get two
67
* things:
78
*
8-
* 1. An `isActive` static method they can use to check if a route,
9-
* params, and query are active.
10-
* 2. An `updateActiveState` instance method that is called when the
9+
* 1. An `updateActiveState` method that is called when the
1110
* active state changes.
11+
* 2. An `isActive` method they can use to check if a route,
12+
* params, and query are active.
13+
*
1214
*
1315
* Example:
1416
*
@@ -24,40 +26,45 @@ var ActiveStore = require('../stores/ActiveStore');
2426
*
2527
* updateActiveState: function () {
2628
* this.setState({
27-
* isActive: Tab.isActive(routeName, params, query)
29+
* isActive: this.isActive(routeName, params, query)
2830
* })
2931
* }
3032
*
3133
* });
3234
*/
3335
var ActiveState = {
3436

35-
statics: {
36-
37-
/**
38-
* Returns true if the route with the given name, URL parameters, and query
39-
* are all currently active.
40-
*/
41-
isActive: ActiveStore.isActive
42-
37+
contextTypes: {
38+
activeDelegate: React.PropTypes.any.isRequired
4339
},
4440

45-
componentWillMount: function () {
46-
ActiveStore.addChangeListener(this.handleActiveStateChange);
41+
/**
42+
* Returns this component's ActiveDelegate component.
43+
*/
44+
getActiveDelegate: function () {
45+
return this.context.activeDelegate;
4746
},
4847

4948
componentDidMount: function () {
50-
if (this.updateActiveState)
51-
this.updateActiveState();
49+
this.getActiveDelegate().addChangeListener(this.handleActiveStateChange);
50+
this.handleActiveStateChange();
5251
},
5352

5453
componentWillUnmount: function () {
55-
ActiveStore.removeChangeListener(this.handleActiveStateChange);
54+
this.getActiveDelegate().removeChangeListener(this.handleActiveStateChange);
5655
},
5756

5857
handleActiveStateChange: function () {
5958
if (this.isMounted() && typeof this.updateActiveState === 'function')
6059
this.updateActiveState();
60+
},
61+
62+
/**
63+
* Returns true if the route with the given name, URL parameters, and
64+
* query are all currently active.
65+
*/
66+
isActive: function (routeName, params, query) {
67+
return this.getActiveDelegate().isActive(routeName, params, query);
6168
}
6269

6370
};

‎modules/mixins/ChangeEmitter.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
var React = require('react');
2+
var EventEmitter = require('events').EventEmitter;
3+
4+
var CHANGE_EVENT = 'change';
5+
6+
/**
7+
* A mixin for components that emit change events. ActiveDelegate uses
8+
* this mixin to notify descendant ActiveState components when the
9+
* active state changes.
10+
*/
11+
var ChangeEmitter = {
12+
13+
propTypes: {
14+
maxChangeListeners: React.PropTypes.number.isRequired
15+
},
16+
17+
getDefaultProps: function () {
18+
return {
19+
maxChangeListeners: 0
20+
};
21+
},
22+
23+
componentWillMount: function () {
24+
this._events = new EventEmitter;
25+
this._events.setMaxListeners(this.props.maxChangeListeners);
26+
},
27+
28+
componentWillReceiveProps: function (nextProps) {
29+
this._events.setMaxListeners(nextProps.maxChangeListeners);
30+
},
31+
32+
componentWillUnmount: function () {
33+
this._events.removeAllListeners();
34+
},
35+
36+
addChangeListener: function (listener) {
37+
this._events.addListener(CHANGE_EVENT, listener);
38+
},
39+
40+
removeChangeListener: function (listener) {
41+
this._events.removeListener(CHANGE_EVENT, listener);
42+
},
43+
44+
emitChange: function () {
45+
this._events.emit(CHANGE_EVENT);
46+
}
47+
48+
};
49+
50+
module.exports = ChangeEmitter;

‎modules/stores/ActiveStore.js

-84
This file was deleted.

‎specs/ActiveDelegate.spec.js

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
require('./helper');
2+
var Route = require('../modules/components/Route');
3+
var ActiveDelegate = require('../modules/mixins/ActiveDelegate');
4+
5+
var App = React.createClass({
6+
displayName: 'App',
7+
mixins: [ ActiveDelegate ],
8+
getInitialState: function () {
9+
return this.props.initialState;
10+
},
11+
render: function () {
12+
return React.DOM.div();
13+
}
14+
});
15+
16+
describe('when a Route is active', function () {
17+
var route;
18+
beforeEach(function () {
19+
route = Route({ name: 'products', handler: App });
20+
});
21+
22+
describe('and it has no params', function () {
23+
var app;
24+
beforeEach(function () {
25+
app = ReactTestUtils.renderIntoDocument(
26+
App({
27+
initialState: {
28+
activeRoutes: [ route ]
29+
}
30+
})
31+
);
32+
});
33+
34+
it('is active', function () {
35+
assert(app.isActive('products'));
36+
});
37+
});
38+
39+
describe('and the right params are given', function () {
40+
var app;
41+
beforeEach(function () {
42+
app = ReactTestUtils.renderIntoDocument(
43+
App({
44+
initialState: {
45+
activeRoutes: [ route ],
46+
activeParams: { id: '123', show: 'true' },
47+
activeQuery: { search: 'abc' }
48+
}
49+
})
50+
);
51+
});
52+
53+
describe('and no query is used', function () {
54+
it('is active', function () {
55+
assert(app.isActive('products', { id: 123 }));
56+
});
57+
});
58+
59+
describe('and a matching query is used', function () {
60+
it('is active', function () {
61+
assert(app.isActive('products', { id: 123 }, { search: 'abc' }));
62+
});
63+
});
64+
65+
describe('but the query does not match', function () {
66+
it('is not active', function () {
67+
refute(app.isActive('products', { id: 123 }, { search: 'def' }));
68+
});
69+
});
70+
});
71+
72+
describe('and the wrong params are given', function () {
73+
var app;
74+
beforeEach(function () {
75+
app = ReactTestUtils.renderIntoDocument(
76+
App({
77+
initialState: {
78+
activeRoutes: [ route ],
79+
activeParams: { id: 123 }
80+
}
81+
})
82+
);
83+
});
84+
85+
it('is not active', function () {
86+
refute(app.isActive('products', { id: 345 }));
87+
});
88+
});
89+
});

‎specs/ActiveStore.spec.js

-70
This file was deleted.

‎specs/Routes.spec.js

-21
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,6 @@ describe('a Routes', function () {
3030
});
3131
});
3232

33-
describe('when there is a change in active state', function () {
34-
it('triggers onActiveStateChange', function (done) {
35-
var App = React.createClass({
36-
render: function () {
37-
return React.DOM.div();
38-
}
39-
});
40-
41-
function handleActiveStateChange(state) {
42-
assert(state);
43-
done();
44-
}
45-
46-
var routes = ReactTestUtils.renderIntoDocument(
47-
Routes({ onActiveStateChange: handleActiveStateChange },
48-
Route({ handler: App })
49-
)
50-
);
51-
});
52-
});
53-
5433
describe('when there is an error in a transition hook', function () {
5534
it('triggers onTransitionError', function (done) {
5635
var App = React.createClass({

‎specs/main.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require('./ActiveStore.spec.js');
1+
require('./ActiveDelegate.spec.js');
22
require('./AsyncState.spec.js');
33
require('./DefaultRoute.spec.js');
44
require('./NotFoundRoute.spec.js');

0 commit comments

Comments
 (0)
Please sign in to comment.