From 9c7c4dbad2df75b9c16456cec852eadb1a871825 Mon Sep 17 00:00:00 2001 From: lingyan Date: Thu, 14 May 2015 10:00:57 -0700 Subject: [PATCH] Merge pull request #97 from gfranko/add-onbeforeunload-support Add onbeforeunload support Conflicts: lib/NavLink.js --- README.md | 16 +++++++++++++++ lib/NavLink.js | 19 +++++++++++++----- lib/RouterMixin.js | 32 +++++++++++++++++++++++++++--- tests/unit/lib/NavLink-test.js | 20 +++++++++++++++++++ tests/unit/lib/RouterMixin-test.js | 25 +++++++++++++++++++++++ 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6e51ab2..050e790 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,22 @@ var appComponent = Application({ ``` +## onbeforeunload Support + +The `History` API does not allow `popstate` events to be cancelled, which results in `window.onbeforeunload()` methods not being triggered. This is problematic for users, since application state could be lost when they navigate to a certain page without knowing the consequences. + +Our solution is to check for a `window.onbeforeunload()` method, prompt the user with `window.confirm()`, and then navigate to the correct route based on the confirmation. If a route is cancelled by the user, we reset the page URL back to the original URL by using the `History` `pushState()` method. + +To implement the `window.onbeforeunload()` method, you need to set it within the components that need user verification before leaving a page. Here is an example: + +```javascript +componentDidMount: function() { + window.onbeforeunload = function () { + return 'Make sure to save your changes before leaving this page!'; + } +} +``` + ## Polyfills `addEventListener` and `removeEventListener` polyfills are provided by: diff --git a/lib/NavLink.js b/lib/NavLink.js index db65d19..e29e822 100644 --- a/lib/NavLink.js +++ b/lib/NavLink.js @@ -97,11 +97,20 @@ NavLink = React.createClass({ e.preventDefault(); e.stopPropagation(); - context.executeAction(navigateAction, { - type: 'click', - url: href, - params: this.props.navParams - }); + + var onBeforeUnloadText = typeof window.onbeforeunload === 'function' ? window.onbeforeunload() : ''; + var confirmResult = onBeforeUnloadText ? window.confirm(onBeforeUnloadText) : true; + + if (confirmResult) { + // Removes the window.onbeforeunload method so that the next page will not be affected + window.onbeforeunload = null; + + context.executeAction(navigateAction, { + type: 'click', + url: href, + params: this.props.navParams + }); + } }, render: function() { return React.createElement( diff --git a/lib/RouterMixin.js b/lib/RouterMixin.js index 539a7f1..a88221e 100644 --- a/lib/RouterMixin.js +++ b/lib/RouterMixin.js @@ -82,11 +82,37 @@ RouterMixin = { } self._historyListener = function (e) { + debug('history listener invoked', e, url, self.state.route.url); + if (context) { + var state = self.state || {}; var url = self._history.getUrl(); - debug('history listener invoked', e, url, self.state.route.url); - if (url !== self.state.route.url) { - context.executeAction(navigateAction, {type: TYPE_POPSTATE, url: url, params: (e.state && e.state.params)}); + var currentUrl = state.route.url; + var route = state.route || {}; + var onBeforeUnloadText = typeof window.onbeforeunload === 'function' ? window.onbeforeunload() : ''; + var confirmResult = onBeforeUnloadText ? window.confirm(onBeforeUnloadText) : true; + var nav = route.navigate || {}; + var navParams = nav.params || {}; + var enableScroll = self._enableScroll && nav.preserveScrollPosition; + var historyState = { + params: (nav.params || {}), + scroll: { + x: (enableScroll ? window.scrollX : 0), + y: (enableScroll ? window.scrollY : 0) + } + }; + var pageTitle = navParams.pageTitle || null; + + if (!confirmResult) { + // Pushes the previous history state back on top to set the correct url + self._history.pushState(historyState, pageTitle, currentUrl); + } else { + if (url !== currentUrl) { + // Removes the window.onbeforeunload method so that the next page will not be affected + window.onbeforeunload = null; + + context.executeAction(navigateAction, {type: TYPE_POPSTATE, url: url, params: (e.state && e.state.params)}); + } } } }; diff --git a/tests/unit/lib/NavLink-test.js b/tests/unit/lib/NavLink-test.js index 23b3680..485dd7a 100644 --- a/tests/unit/lib/NavLink-test.js +++ b/tests/unit/lib/NavLink-test.js @@ -191,6 +191,26 @@ describe('NavLink', function () { done(); }, 10); }); + + describe('window.onbeforeunload', function () { + beforeEach(function () { + global.window.confirm = function () { return false; }; + global.window.onbeforeunload = function () { + return 'this is a test'; + }; + }); + + it ('should not call context.executeAction when a user does not confirm the onbeforeunload method', function (done) { + var navParams = {a: 1, b: true}; + var link = ReactTestUtils.renderIntoDocument(NavLink( {href:"/foo", navParams:navParams}, React.DOM.span(null, "bar"))); + link.context = contextMock; + ReactTestUtils.Simulate.click(link.getDOMNode(), {button: 0}); + window.setTimeout(function () { + expect(testResult).to.deep.equal({}); + done(); + }, 10); + }); + }); }); describe('click type', function () { diff --git a/tests/unit/lib/RouterMixin-test.js b/tests/unit/lib/RouterMixin-test.js index f15fe9a..7484a93 100644 --- a/tests/unit/lib/RouterMixin-test.js +++ b/tests/unit/lib/RouterMixin-test.js @@ -163,6 +163,31 @@ describe ('RouterMixin', function () { done(); }, 10); }); + + describe('window.onbeforeunload', function () { + beforeEach(function () { + global.window.confirm = function () { return false; }; + global.window.onbeforeunload = function () { + return 'this is a test'; + }; + }); + + it ('should change the url back to the oldRoute if there is a window.onbeforeunload method', function (done) { + routerMixin.props = {context: contextMock, historyCreator: function() { return historyMock(); }}; + var origPushState = window.history.pushState; + routerMixin.state = { + route: { + url: '/the_path_from_history' + } + }; + routerMixin.componentDidMount(); + window.setTimeout(function() { + expect(testResult.dispatch).to.equal(undefined, JSON.stringify(testResult.dispatch)); + done(); + }, 10); + }); + }); + }); describe('componentWillUnmount()', function () {