diff --git a/modules/components/Routes.js b/modules/components/Routes.js index ddbc1ca6fe..311c62bb06 100644 --- a/modules/components/Routes.js +++ b/modules/components/Routes.js @@ -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 component configures the route hierarchy and renders the * route matching the current location when rendered into a document. @@ -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]; @@ -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 () { @@ -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 }; } diff --git a/modules/components/__tests__/Link-test.js b/modules/components/__tests__/Link-test.js index fbfb072c22..517eb97850 100644 --- a/modules/components/__tests__/Link-test.js +++ b/modules/components/__tests__/Link-test.js @@ -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'); }); diff --git a/modules/mixins/ActiveContext.js b/modules/mixins/ActiveContext.js index 5c1247cad0..842d78cef5 100644 --- a/modules/mixins/ActiveContext.js +++ b/modules/mixins/ActiveContext.js @@ -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 = { @@ -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() }; } diff --git a/modules/mixins/__tests__/ActiveContext-test.js b/modules/mixins/__tests__/ActiveContext-test.js index c55512fd2e..81b989e8e6 100644 --- a/modules/mixins/__tests__/ActiveContext-test.js +++ b/modules/mixins/__tests__/ActiveContext-test.js @@ -1,32 +1,33 @@ 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 () { @@ -34,20 +35,24 @@ describe('ActiveContext', function () { }); 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 () { @@ -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 () {