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

Add replacestate link type #91

Merged
merged 5 commits into from
Apr 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/navlink.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ in navigation events.
| routeName | String | Not used if `href` is specified. This is the name of the target route, which should be defined in your app's routes. |
| navParams | Object | If `href` prop is not available, `navParams` object will be used together with `routeName` to generate the href for the link. This object needs to contain route params the route path needs. Eg. for a route path `/article/:id`, `navParams.id` will be the article ID. |
| followLink | boolean, default to false | If set to true, client side navigation will be disabled. NavLink will just act like a regular anchor link. |
| replaceState | boolean, default to false | If set to true, replaceState is being used instead of pushState |
| preserveScrollPosition | boolean, default to false | If set to true, the page will maintain its scroll position on route change. |


## Example Usage
Expand Down
8 changes: 6 additions & 2 deletions lib/NavLink.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ NavLink = React.createClass({
href: React.PropTypes.string,
routeName: React.PropTypes.string,
navParams: React.PropTypes.object,
followLink: React.PropTypes.bool
followLink: React.PropTypes.bool,
preserveScrollPosition: React.PropTypes.bool,
replaceState: React.PropTypes.bool
},
getInitialState: function () {
return {
Expand All @@ -55,6 +57,7 @@ NavLink = React.createClass({
return href;
},
dispatchNavAction: function (e) {
var navType = this.props.replaceState ? 'replacestate' : 'click';
debug('dispatchNavAction: action=NAVIGATE', this.props.href, this.props.followLink, this.props.navParams);

if (this.props.followLink) {
Expand Down Expand Up @@ -98,8 +101,9 @@ NavLink = React.createClass({
e.preventDefault();
e.stopPropagation();
context.executeAction(navigateAction, {
type: 'click',
type: navType,
url: href,
preserveScrollPosition: this.props.preserveScrollPosition,
params: this.props.navParams
});
},
Expand Down
25 changes: 20 additions & 5 deletions lib/RouterMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var History = require('./History');
var React = require('react');
var TYPE_CLICK = 'click';
var TYPE_PAGELOAD = 'pageload';
var TYPE_REPLACESTATE = 'replacestate';
var TYPE_POPSTATE = 'popstate';
var TYPE_DEFAULT = 'default'; // default value if navigation type is missing, for programmatic navigation
var RouterMixin;
Expand Down Expand Up @@ -125,19 +126,33 @@ RouterMixin = {
var nav = newState.route.navigate || {};
var navType = nav.type || TYPE_DEFAULT;
var historyState;
var prevHistoryState;
var pageTitle;

function resetScrollPosition() {
window.scrollTo(0, 0);
historyState.scroll = {x: 0, y: 0};
debug('on click navigation, reset scroll position to (0, 0)');
}

switch (navType) {
case TYPE_CLICK:
case TYPE_DEFAULT:
case TYPE_REPLACESTATE:
historyState = {params: (nav.params || {})};
if (this._enableScroll) {
window.scrollTo(0, 0);
historyState.scroll = {x: 0, y: 0};
debug('on click navigation, reset scroll position to (0, 0)');
if(this._enableScroll) {
if (!nav.preserveScrollPosition) {
resetScrollPosition();
} else {
historyState.scroll = {x: window.scrollX, y: window.scrollY};
}
}
pageTitle = nav.params && nav.params.pageTitle || null;
this._history.pushState(historyState, pageTitle, newState.route.url);
if(navType == TYPE_REPLACESTATE) {
this._history.replaceState(historyState, pageTitle, newState.route.url);
} else {
this._history.pushState(historyState, pageTitle, newState.route.url);
}
break;
case TYPE_POPSTATE:
if (this._enableScroll) {
Expand Down
23 changes: 22 additions & 1 deletion tests/unit/lib/NavLink-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,18 @@ describe('NavLink', function () {
describe('dispatchNavAction()', function () {
it ('use react context', function (done) {
var navParams = {a: 1, b: true};
var link = ReactTestUtils.renderIntoDocument(NavLink( {href:"/foo", navParams:navParams}, React.DOM.span(null, "bar")));
var link = ReactTestUtils.renderIntoDocument(NavLink({
href:"/foo",
preserveScrollPosition: true,
navParams: navParams
}, React.DOM.span(null, "bar")));
link.context = contextMock;
ReactTestUtils.Simulate.click(link.getDOMNode(), {button: 0});
window.setTimeout(function () {
expect(testResult.dispatch.action).to.equal('NAVIGATE');
expect(testResult.dispatch.payload.type).to.equal('click');
expect(testResult.dispatch.payload.url).to.equal('/foo');
expect(testResult.dispatch.payload.preserveScrollPosition).to.equal(true);
expect(testResult.dispatch.payload.params).to.eql({a: 1, b: true});
done();
}, 10);
Expand Down Expand Up @@ -205,6 +210,22 @@ describe('NavLink', function () {
}, 10);
});

it('navigates on regular click using replaceState', function (done) {
var origin = window.location.origin;
var link = ReactTestUtils.renderIntoDocument(
NavLink(
{href: origin, replaceState: true, context:contextMock},
React.DOM.span(null, "bar")
)
);
ReactTestUtils.Simulate.click(link.getDOMNode(), {button: 0});
window.setTimeout(function () {
expect(testResult.dispatch.action).to.equal('NAVIGATE');
expect(testResult.dispatch.payload.type).to.equal('replacestate');
done();
}, 10);
});

['metaKey', 'altKey', 'ctrlKey', 'shiftKey'].map(function (key) {
it('does not navigate on modified ' + key, function (done) {
var eventData = {button: 0};
Expand Down
56 changes: 56 additions & 0 deletions tests/unit/lib/RouterMixin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,22 @@ describe ('RouterMixin', function () {
expect(testResult.pushState).to.eql({state: {params: {}}, title: null, url: '/bar'});
expect(testResult.scrollTo).to.equal(undefined);
});
it ('update with different route, navigate.type=replacestate, enableScroll=false, do not reset scroll position', function () {
var oldRoute = {url: '/foo'},
newRoute = {url: '/bar', navigate: {type: 'replacestate'}};
routerMixin.props = {
context: contextMock,
enableScroll: false,
historyCreator: function() {
return historyMock('/foo');
}
};
routerMixin.state = {route: newRoute};
routerMixin.componentDidMount();
routerMixin.componentDidUpdate({}, {route: oldRoute});
expect(testResult.replaceState).to.eql({state: {params: {}}, title: null, url: '/bar'});
expect(testResult.scrollTo).to.equal(undefined);
});
it ('update with different route, navigate.type=default, reset scroll position', function () {
var oldRoute = {url: '/foo'},
newRoute = {url: '/bar'};
Expand Down Expand Up @@ -306,6 +322,46 @@ describe ('RouterMixin', function () {
routerMixin.componentDidUpdate({}, {route: oldRoute});
expect(testResult.pushState).to.eql({state: {params: {foo: 'bar'}, scroll: {x: 0, y:0}}, title: null, url: '/foo#hash2'});
});
it ('update with different route, navigate.type=replacestate, with params', function () {
var oldRoute = {url: '/foo'},
newRoute = {url: '/bar', navigate: {type: 'replacestate', params: {foo: 'bar'}}};
routerMixin.props = {context: contextMock, historyCreator: function() { return historyMock('/foo'); }};
routerMixin.state = {route: newRoute};
routerMixin.componentDidMount();
routerMixin.componentDidUpdate({}, {route: oldRoute});
expect(testResult.replaceState).to.eql({state: {params: {foo: 'bar'}, scroll: {x: 0, y: 0}}, title: null, url: '/bar'});
});
it ('update with different route, navigate.type=replacestate', function () {
var oldRoute = {url: '/foo'},
newRoute = {url: '/bar', navigate: {type: 'replacestate'}};
routerMixin.props = {context: contextMock, historyCreator: function() { return historyMock('/foo', {scroll: {x: 42, y: 3}}); }};
routerMixin.state = {route: newRoute};
routerMixin.componentDidMount();
routerMixin.componentDidUpdate({}, {route: oldRoute});
expect(testResult.replaceState).to.eql({state: {params: {}, scroll: {x: 0, y: 0}}, title: null, url: '/bar'});
});
it ('update with different route, navigate.type=pushstate, preserve scroll state', function () {
var oldRoute = {url: '/foo'},
newRoute = {url: '/bar', navigate: {type: 'click', preserveScrollPosition: true}};
global.window.scrollX = 42;
global.window.scrollY = 3;
routerMixin.props = {context: contextMock, historyCreator: function() { return historyMock('/foo'); }};
routerMixin.state = {route: newRoute};
routerMixin.componentDidMount();
routerMixin.componentDidUpdate({}, {route: oldRoute});
expect(testResult.pushState).to.eql({state: {params: {}, scroll: {x: 42, y: 3}}, title: null, url: '/bar'});
});
it ('update with different route, navigate.type=replacestate, preserve scroll state', function () {
var oldRoute = {url: '/foo'},
newRoute = {url: '/bar', navigate: {type: 'replacestate', preserveScrollPosition: true}};
global.window.scrollX = 42;
global.window.scrollY = 3;
routerMixin.props = {context: contextMock, historyCreator: function() { return historyMock('/foo'); }};
routerMixin.state = {route: newRoute};
routerMixin.componentDidMount();
routerMixin.componentDidUpdate({}, {route: oldRoute});
expect(testResult.replaceState).to.eql({state: {params: {}, scroll: {x: 42, y: 3}}, title: null, url: '/bar'});
});
});

});