diff --git a/modules/index.js b/modules/index.js index c6bcc8c028..cd5b258998 100644 --- a/modules/index.js +++ b/modules/index.js @@ -17,3 +17,5 @@ exports.State = require('./mixins/State'); exports.create = require('./utils/createRouter'); exports.run = require('./utils/runRouter'); + +exports.History = require('./utils/History'); diff --git a/modules/locations/HashLocation.js b/modules/locations/HashLocation.js index a7b21f2d12..e23cee039d 100644 --- a/modules/locations/HashLocation.js +++ b/modules/locations/HashLocation.js @@ -1,4 +1,5 @@ var LocationActions = require('../actions/LocationActions'); +var History = require('../utils/History'); var Path = require('../utils/Path'); /** @@ -29,6 +30,9 @@ function ensureSlash() { var _changeListeners = []; function notifyChange(type) { + if (type === LocationActions.PUSH) + History.length += 1; + var change = { path: getHashPath(), type: type @@ -87,7 +91,7 @@ var HashLocation = { pop: function () { _actionType = LocationActions.POP; - window.history.back(); + History.back(); }, getCurrentPath: getHashPath, diff --git a/modules/locations/HistoryLocation.js b/modules/locations/HistoryLocation.js index 55ededf9e4..7d0cf3d31b 100644 --- a/modules/locations/HistoryLocation.js +++ b/modules/locations/HistoryLocation.js @@ -1,4 +1,5 @@ var LocationActions = require('../actions/LocationActions'); +var History = require('../utils/History'); var Path = require('../utils/Path'); /** @@ -51,6 +52,7 @@ var HistoryLocation = { push: function (path) { window.history.pushState({ path: path }, '', Path.encode(path)); + History.length += 1; notifyChange(LocationActions.PUSH); }, @@ -60,7 +62,7 @@ var HistoryLocation = { }, pop: function () { - window.history.back(); + History.back(); }, getCurrentPath: getWindowPath, diff --git a/modules/locations/RefreshLocation.js b/modules/locations/RefreshLocation.js index 3b352d6e17..2ab361310b 100644 --- a/modules/locations/RefreshLocation.js +++ b/modules/locations/RefreshLocation.js @@ -17,6 +17,8 @@ var RefreshLocation = { }, pop: function () { + // History will always have length 1 when using full + // page refreshes, so use window.history directly. window.history.back(); }, diff --git a/modules/locations/TestLocation.js b/modules/locations/TestLocation.js index 02af7ec565..149c22ad08 100644 --- a/modules/locations/TestLocation.js +++ b/modules/locations/TestLocation.js @@ -1,4 +1,6 @@ +var invariant = require('react/lib/invariant'); var LocationActions = require('../actions/LocationActions'); +var History = require('../utils/History'); var _listener; @@ -7,6 +9,10 @@ function notifyChange(type) { _listener({ path: TestLocation.getCurrentPath(), type: type }); } +function updateHistoryLength() { + History.length = TestLocation.history.length; +} + /** * A location that is convenient for testing and does not * require a DOM. You should manually setup TestLocation.history @@ -19,20 +25,28 @@ var TestLocation = { addChangeListener: function (listener) { // TestLocation only ever supports a single listener at a time. _listener = listener; + updateHistoryLength(); }, push: function (path) { TestLocation.history.push(path); + updateHistoryLength(); notifyChange(LocationActions.PUSH); }, replace: function (path) { + invariant( + History.length, + 'You cannot replace the current path with no history' + ); + TestLocation.history[TestLocation.history.length - 1] = path; notifyChange(LocationActions.REPLACE); }, pop: function () { TestLocation.history.pop(); + updateHistoryLength(); notifyChange(LocationActions.POP); }, diff --git a/modules/utils/History.js b/modules/utils/History.js new file mode 100644 index 0000000000..9b9c149306 --- /dev/null +++ b/modules/utils/History.js @@ -0,0 +1,29 @@ +var invariant = require('react/lib/invariant'); +var canUseDOM = require('react/lib/ExecutionEnvironment').canUseDOM; + +var History = { + + /** + * Sends the browser back one entry in the history, if one is available. + */ + back: function () { + invariant( + canUseDOM, + 'Cannot use History.back without a DOM' + ); + + // Do this first so that History.length will + // be accurate in location change listeners. + History.length -= 1; + + window.history.back(); + }, + + /** + * The current number of entries in the history. + */ + length: 1 + +}; + +module.exports = History; diff --git a/modules/utils/__tests__/History-test.js b/modules/utils/__tests__/History-test.js new file mode 100644 index 0000000000..0ca81c142a --- /dev/null +++ b/modules/utils/__tests__/History-test.js @@ -0,0 +1,63 @@ +var expect = require('expect'); +var React = require('react'); +var { Foo, RedirectToFoo } = require('../../__tests__/TestHandlers'); +var TestLocation = require('../../locations/TestLocation'); +var Route = require('../../components/Route'); +var Router = require('../../index'); +var History = require('../History'); + +describe('History', function () { + describe('on the initial page load', function () { + it('has length 1', function () { + expect(History.length).toEqual(1); + }); + }); + + describe('after navigating to a route', function () { + beforeEach(function () { + TestLocation.history = [ '/foo' ]; + }); + + it('has length 2', function (done) { + var routes = [ + , + + ]; + + var count = 0; + + var router = Router.run(routes, TestLocation, function (Handler) { + count += 1; + + if (count === 2) { + expect(History.length).toEqual(2); + done(); + } + }); + + router.transitionTo('about'); + }); + + describe('that redirects to another route', function () { + it('has length 2', function (done) { + var routes = [ + , + + ]; + + var count = 0; + + var router = Router.run(routes, TestLocation, function (Handler) { + count += 1; + + if (count === 2) { + expect(History.length).toEqual(2); + done(); + } + }); + + router.transitionTo('about'); + }); + }); + }); +}); diff --git a/modules/utils/createRouter.js b/modules/utils/createRouter.js index 4682f62e6b..898eeb7f2d 100644 --- a/modules/utils/createRouter.js +++ b/modules/utils/createRouter.js @@ -18,6 +18,7 @@ var supportsHistory = require('./supportsHistory'); var Transition = require('./Transition'); var PropTypes = require('./PropTypes'); var Redirect = require('./Redirect'); +var History = require('./History'); var Cancellation = require('./Cancellation'); var Path = require('./Path'); @@ -242,7 +243,8 @@ function createRouter(options) { }, /** - * Transitions to the previous URL. + * Transitions to the previous URL. Returns true if the router + * was able to go back, false otherwise. */ goBack: function () { invariant( @@ -250,7 +252,14 @@ function createRouter(options) { 'You cannot use goBack with a static location' ); - location.pop(); + if (History.length > 1) { + location.pop(); + return true; + } + + warning(false, 'goBack() was ignored because there is no router history'); + + return false; }, /**