Skip to content

Commit

Permalink
[fixed] Add .active class to <Link>s with absolute hrefs
Browse files Browse the repository at this point in the history
  • Loading branch information
mjackson committed Oct 11, 2014
1 parent f64d1b0 commit e571c27
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 70 deletions.
40 changes: 38 additions & 2 deletions modules/components/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ function returnNull() {
return null;
}

function routeIsActive(activeRoutes, routeName) {
return activeRoutes.some(function (route) {
return route.props.name === routeName;
});
}

function paramsAreActive(activeParams, params) {
for (var property in params)
if (String(activeParams[property]) !== String(params[property]))
return false;

return true;
}

function queryIsActive(activeQuery, query) {
for (var property in query)
if (String(activeQuery[property]) !== String(query[property]))
return false;

return true;
}

/**
* The <Routes> component configures the route hierarchy and renders the
* route matching the current location when rendered into a document.
Expand Down Expand Up @@ -476,6 +498,18 @@ var Routes = React.createClass({
location.pop();
},

/**
* Returns true if the given route, params, and query are active.
*/
isActive: function (to, params, query) {
if (Path.isAbsolute(to))
return to === this.getCurrentPath();

return routeIsActive(this.getActiveRoutes(), to) &&
paramsAreActive(this.getActiveParams(), params) &&
(query == null || queryIsActive(this.getActiveQuery(), query));
},

render: function () {
var match = this.state.matches[0];

Expand All @@ -493,7 +527,8 @@ var Routes = React.createClass({
makeHref: React.PropTypes.func.isRequired,
transitionTo: React.PropTypes.func.isRequired,
replaceWith: React.PropTypes.func.isRequired,
goBack: React.PropTypes.func.isRequired
goBack: React.PropTypes.func.isRequired,
isActive: React.PropTypes.func.isRequired
},

getChildContext: function () {
Expand All @@ -503,7 +538,8 @@ var Routes = React.createClass({
makeHref: this.makeHref,
transitionTo: this.transitionTo,
replaceWith: this.replaceWith,
goBack: this.goBack
goBack: this.goBack,
isActive: this.isActive
};
}

Expand Down
34 changes: 33 additions & 1 deletion modules/components/__tests__/Link-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,39 @@ describe('A Link', function () {
React.unmountComponentAtNode(component.getDOMNode());
});

it('has its active class name', function () {
it('is active', function () {
var linkComponent = component.getActiveComponent().refs.link;
expect(linkComponent.getClassName()).toEqual('a-link highlight');
});
});

describe('when the path it links to is active', function () {
var HomeHandler = React.createClass({
render: function () {
return Link({ ref: 'link', to: '/home', className: 'a-link', activeClassName: 'highlight' });
}
});

var component;
beforeEach(function (done) {
component = ReactTestUtils.renderIntoDocument(
Routes({ location: 'none' },
Route({ path: '/home', handler: HomeHandler })
)
);

component.dispatch('/home', function (error, abortReason, nextState) {
expect(error).toBe(null);
expect(abortReason).toBe(null);
component.setState(nextState, done);
});
});

afterEach(function () {
React.unmountComponentAtNode(component.getDOMNode());
});

it('is active', function () {
var linkComponent = component.getActiveComponent().refs.link;
expect(linkComponent.getClassName()).toEqual('a-link highlight');
});
Expand Down
46 changes: 4 additions & 42 deletions modules/mixins/ActiveContext.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,9 @@
var React = require('react');
var copyProperties = require('react/lib/copyProperties');

function routeIsActive(activeRoutes, routeName) {
return activeRoutes.some(function (route) {
return route.props.name === routeName;
});
}

function paramsAreActive(activeParams, params) {
for (var property in params)
if (String(activeParams[property]) !== String(params[property]))
return false;

return true;
}

function queryIsActive(activeQuery, query) {
for (var property in query)
if (String(activeQuery[property]) !== String(query[property]))
return false;

return true;
}

/**
* A mixin for components that store the active state of routes, URL
* parameters, and query.
* A mixin for components that store the active state of routes,
* URL parameters, and query.
*/
var ActiveContext = {

Expand Down Expand Up @@ -72,33 +50,17 @@ var ActiveContext = {
return copyProperties({}, this.state.activeQuery);
},

/**
* 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(this.state.activeRoutes, routeName) &&
paramsAreActive(this.state.activeParams, params);

if (query)
return isActive && queryIsActive(this.state.activeQuery, query);

return isActive;
},

childContextTypes: {
activeRoutes: React.PropTypes.array.isRequired,
activeParams: React.PropTypes.object.isRequired,
activeQuery: React.PropTypes.object.isRequired,
isActive: React.PropTypes.func.isRequired
activeQuery: React.PropTypes.object.isRequired
},

getChildContext: function () {
return {
activeRoutes: this.getActiveRoutes(),
activeParams: this.getActiveParams(),
activeQuery: this.getActiveQuery(),
isActive: this.isActive
activeQuery: this.getActiveQuery()
};
}

Expand Down
60 changes: 35 additions & 25 deletions modules/mixins/__tests__/ActiveContext-test.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,58 @@
var assert = require('assert');
var expect = require('expect');
var React = require('react/addons');
var ReactTestUtils = React.addons.TestUtils;
var Routes = require('../../components/Routes');
var Route = require('../../components/Route');
var ActiveContext = require('../ActiveContext');

describe('ActiveContext', function () {

var App = React.createClass({
mixins: [ ActiveContext ],
render: function () {
return null;
}
});

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 component;
beforeEach(function () {
beforeEach(function (done) {
component = ReactTestUtils.renderIntoDocument(
App({
initialActiveRoutes: [ route ]
})
Routes({ location: 'none' },
Route({ name: 'home', handler: App })
)
);

component.dispatch('/home', function (error, abortReason, nextState) {
expect(error).toBe(null);
expect(abortReason).toBe(null);
component.setState(nextState, done);
});
});

afterEach(function () {
React.unmountComponentAtNode(component.getDOMNode());
});

it('is active', function () {
assert(component.isActive('products'));
assert(component.isActive('home'));
});
});

describe('and the right params are given', function () {
var component;
beforeEach(function () {
beforeEach(function (done) {
component = ReactTestUtils.renderIntoDocument(
App({
initialActiveRoutes: [ route ],
initialActiveParams: { id: '123', show: 'true', variant: 456 },
initialActiveQuery: { search: 'abc', limit: 789 }
})
Routes({ location: 'none' },
Route({ name: 'products', path: '/products/:id/:variant', handler: App })
)
);

component.dispatch('/products/123/456?search=abc&limit=789', function (error, abortReason, nextState) {
expect(error).toBe(null);
expect(abortReason).toBe(null);
component.setState(nextState, done);
});
});

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

describe('and a matching query is used', function () {
it('is active', function () {
assert(component.isActive('products', { id: 123 }, { search: 'abc', limit: '789' }));
assert(component.isActive('products', { id: 123 }, { search: 'abc' }));
});
});

describe('but the query does not match', function () {
it('is not active', function () {
assert(component.isActive('products', { id: 123 }, { search: 'def', limit: '123' }) === false);
assert(component.isActive('products', { id: 123 }, { search: 'def' }) === false);
});
});
});

describe('and the wrong params are given', function () {
var component;
beforeEach(function () {
beforeEach(function (done) {
component = ReactTestUtils.renderIntoDocument(
App({
initialActiveRoutes: [ route ],
initialActiveParams: { id: 123 }
})
Routes({ location: 'none' },
Route({ name: 'products', path: '/products/:id', handler: App })
)
);

component.dispatch('/products/123', function (error, abortReason, nextState) {
expect(error).toBe(null);
expect(abortReason).toBe(null);
component.setState(nextState, done);
});
});

afterEach(function () {
Expand Down

0 comments on commit e571c27

Please sign in to comment.