Skip to content
This repository has been archived by the owner on Jul 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #97 from gfranko/add-onbeforeunload-support
Browse files Browse the repository at this point in the history
Add onbeforeunload support
Conflicts:
	lib/NavLink.js
  • Loading branch information
lingyan authored and Michael Ridgway committed May 14, 2015
1 parent 2101349 commit 9c7c4db
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 8 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 14 additions & 5 deletions lib/NavLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
32 changes: 29 additions & 3 deletions lib/RouterMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)});
}
}
}
};
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/lib/NavLink-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
25 changes: 25 additions & 0 deletions tests/unit/lib/RouterMixin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down

0 comments on commit 9c7c4db

Please sign in to comment.