Skip to content

Commit e571c27

Browse files
committed
[fixed] Add .active class to <Link>s with absolute hrefs
Fixes #379
1 parent f64d1b0 commit e571c27

File tree

4 files changed

+110
-70
lines changed

4 files changed

+110
-70
lines changed

modules/components/Routes.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,28 @@ function returnNull() {
167167
return null;
168168
}
169169

170+
function routeIsActive(activeRoutes, routeName) {
171+
return activeRoutes.some(function (route) {
172+
return route.props.name === routeName;
173+
});
174+
}
175+
176+
function paramsAreActive(activeParams, params) {
177+
for (var property in params)
178+
if (String(activeParams[property]) !== String(params[property]))
179+
return false;
180+
181+
return true;
182+
}
183+
184+
function queryIsActive(activeQuery, query) {
185+
for (var property in query)
186+
if (String(activeQuery[property]) !== String(query[property]))
187+
return false;
188+
189+
return true;
190+
}
191+
170192
/**
171193
* The <Routes> component configures the route hierarchy and renders the
172194
* route matching the current location when rendered into a document.
@@ -476,6 +498,18 @@ var Routes = React.createClass({
476498
location.pop();
477499
},
478500

501+
/**
502+
* Returns true if the given route, params, and query are active.
503+
*/
504+
isActive: function (to, params, query) {
505+
if (Path.isAbsolute(to))
506+
return to === this.getCurrentPath();
507+
508+
return routeIsActive(this.getActiveRoutes(), to) &&
509+
paramsAreActive(this.getActiveParams(), params) &&
510+
(query == null || queryIsActive(this.getActiveQuery(), query));
511+
},
512+
479513
render: function () {
480514
var match = this.state.matches[0];
481515

@@ -493,7 +527,8 @@ var Routes = React.createClass({
493527
makeHref: React.PropTypes.func.isRequired,
494528
transitionTo: React.PropTypes.func.isRequired,
495529
replaceWith: React.PropTypes.func.isRequired,
496-
goBack: React.PropTypes.func.isRequired
530+
goBack: React.PropTypes.func.isRequired,
531+
isActive: React.PropTypes.func.isRequired
497532
},
498533

499534
getChildContext: function () {
@@ -503,7 +538,8 @@ var Routes = React.createClass({
503538
makeHref: this.makeHref,
504539
transitionTo: this.transitionTo,
505540
replaceWith: this.replaceWith,
506-
goBack: this.goBack
541+
goBack: this.goBack,
542+
isActive: this.isActive
507543
};
508544
}
509545

modules/components/__tests__/Link-test.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,39 @@ describe('A Link', function () {
6767
React.unmountComponentAtNode(component.getDOMNode());
6868
});
6969

70-
it('has its active class name', function () {
70+
it('is active', function () {
71+
var linkComponent = component.getActiveComponent().refs.link;
72+
expect(linkComponent.getClassName()).toEqual('a-link highlight');
73+
});
74+
});
75+
76+
describe('when the path it links to is active', function () {
77+
var HomeHandler = React.createClass({
78+
render: function () {
79+
return Link({ ref: 'link', to: '/home', className: 'a-link', activeClassName: 'highlight' });
80+
}
81+
});
82+
83+
var component;
84+
beforeEach(function (done) {
85+
component = ReactTestUtils.renderIntoDocument(
86+
Routes({ location: 'none' },
87+
Route({ path: '/home', handler: HomeHandler })
88+
)
89+
);
90+
91+
component.dispatch('/home', function (error, abortReason, nextState) {
92+
expect(error).toBe(null);
93+
expect(abortReason).toBe(null);
94+
component.setState(nextState, done);
95+
});
96+
});
97+
98+
afterEach(function () {
99+
React.unmountComponentAtNode(component.getDOMNode());
100+
});
101+
102+
it('is active', function () {
71103
var linkComponent = component.getActiveComponent().refs.link;
72104
expect(linkComponent.getClassName()).toEqual('a-link highlight');
73105
});

modules/mixins/ActiveContext.js

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,9 @@
11
var React = require('react');
22
var copyProperties = require('react/lib/copyProperties');
33

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 (String(activeParams[property]) !== String(params[property]))
13-
return false;
14-
15-
return true;
16-
}
17-
18-
function queryIsActive(activeQuery, query) {
19-
for (var property in query)
20-
if (String(activeQuery[property]) !== String(query[property]))
21-
return false;
22-
23-
return true;
24-
}
25-
264
/**
27-
* A mixin for components that store the active state of routes, URL
28-
* parameters, and query.
5+
* A mixin for components that store the active state of routes,
6+
* URL parameters, and query.
297
*/
308
var ActiveContext = {
319

@@ -72,33 +50,17 @@ var ActiveContext = {
7250
return copyProperties({}, this.state.activeQuery);
7351
},
7452

75-
/**
76-
* Returns true if the route with the given name, URL parameters, and
77-
* query are all currently active.
78-
*/
79-
isActive: function (routeName, params, query) {
80-
var isActive = routeIsActive(this.state.activeRoutes, routeName) &&
81-
paramsAreActive(this.state.activeParams, params);
82-
83-
if (query)
84-
return isActive && queryIsActive(this.state.activeQuery, query);
85-
86-
return isActive;
87-
},
88-
8953
childContextTypes: {
9054
activeRoutes: React.PropTypes.array.isRequired,
9155
activeParams: React.PropTypes.object.isRequired,
92-
activeQuery: React.PropTypes.object.isRequired,
93-
isActive: React.PropTypes.func.isRequired
56+
activeQuery: React.PropTypes.object.isRequired
9457
},
9558

9659
getChildContext: function () {
9760
return {
9861
activeRoutes: this.getActiveRoutes(),
9962
activeParams: this.getActiveParams(),
100-
activeQuery: this.getActiveQuery(),
101-
isActive: this.isActive
63+
activeQuery: this.getActiveQuery()
10264
};
10365
}
10466

modules/mixins/__tests__/ActiveContext-test.js

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,58 @@
11
var assert = require('assert');
2+
var expect = require('expect');
23
var React = require('react/addons');
34
var ReactTestUtils = React.addons.TestUtils;
5+
var Routes = require('../../components/Routes');
46
var Route = require('../../components/Route');
5-
var ActiveContext = require('../ActiveContext');
67

78
describe('ActiveContext', function () {
89

910
var App = React.createClass({
10-
mixins: [ ActiveContext ],
1111
render: function () {
1212
return null;
1313
}
1414
});
1515

1616
describe('when a route is active', function () {
17-
var route;
18-
beforeEach(function () {
19-
route = Route({ name: 'products', handler: App });
20-
});
21-
2217
describe('and it has no params', function () {
2318
var component;
24-
beforeEach(function () {
19+
beforeEach(function (done) {
2520
component = ReactTestUtils.renderIntoDocument(
26-
App({
27-
initialActiveRoutes: [ route ]
28-
})
21+
Routes({ location: 'none' },
22+
Route({ name: 'home', handler: App })
23+
)
2924
);
25+
26+
component.dispatch('/home', function (error, abortReason, nextState) {
27+
expect(error).toBe(null);
28+
expect(abortReason).toBe(null);
29+
component.setState(nextState, done);
30+
});
3031
});
3132

3233
afterEach(function () {
3334
React.unmountComponentAtNode(component.getDOMNode());
3435
});
3536

3637
it('is active', function () {
37-
assert(component.isActive('products'));
38+
assert(component.isActive('home'));
3839
});
3940
});
4041

4142
describe('and the right params are given', function () {
4243
var component;
43-
beforeEach(function () {
44+
beforeEach(function (done) {
4445
component = ReactTestUtils.renderIntoDocument(
45-
App({
46-
initialActiveRoutes: [ route ],
47-
initialActiveParams: { id: '123', show: 'true', variant: 456 },
48-
initialActiveQuery: { search: 'abc', limit: 789 }
49-
})
46+
Routes({ location: 'none' },
47+
Route({ name: 'products', path: '/products/:id/:variant', handler: App })
48+
)
5049
);
50+
51+
component.dispatch('/products/123/456?search=abc&limit=789', function (error, abortReason, nextState) {
52+
expect(error).toBe(null);
53+
expect(abortReason).toBe(null);
54+
component.setState(nextState, done);
55+
});
5156
});
5257

5358
afterEach(function () {
@@ -62,26 +67,31 @@ describe('ActiveContext', function () {
6267

6368
describe('and a matching query is used', function () {
6469
it('is active', function () {
65-
assert(component.isActive('products', { id: 123 }, { search: 'abc', limit: '789' }));
70+
assert(component.isActive('products', { id: 123 }, { search: 'abc' }));
6671
});
6772
});
6873

6974
describe('but the query does not match', function () {
7075
it('is not active', function () {
71-
assert(component.isActive('products', { id: 123 }, { search: 'def', limit: '123' }) === false);
76+
assert(component.isActive('products', { id: 123 }, { search: 'def' }) === false);
7277
});
7378
});
7479
});
7580

7681
describe('and the wrong params are given', function () {
7782
var component;
78-
beforeEach(function () {
83+
beforeEach(function (done) {
7984
component = ReactTestUtils.renderIntoDocument(
80-
App({
81-
initialActiveRoutes: [ route ],
82-
initialActiveParams: { id: 123 }
83-
})
85+
Routes({ location: 'none' },
86+
Route({ name: 'products', path: '/products/:id', handler: App })
87+
)
8488
);
89+
90+
component.dispatch('/products/123', function (error, abortReason, nextState) {
91+
expect(error).toBe(null);
92+
expect(abortReason).toBe(null);
93+
component.setState(nextState, done);
94+
});
8595
});
8696

8797
afterEach(function () {

0 commit comments

Comments
 (0)