Skip to content

Commit

Permalink
[fixed] Window scrolling
Browse files Browse the repository at this point in the history
The router now remembers the last window scroll position at various
paths and automatically scrolls the window to match after transitions
complete unless preserveScrollPosition=true is used.

This commit also introduces a flux-style architecture to the high-level
transitionTo/replaceWith/goBack methods.

Fixes #189
Fixes #186
  • Loading branch information
mjackson committed Aug 29, 2014
1 parent 94c7a35 commit b7e21bb
Show file tree
Hide file tree
Showing 15 changed files with 162 additions and 80 deletions.
2 changes: 1 addition & 1 deletion goBack.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = require('./modules/helpers/goBack');
module.exports = require('./modules/actions/LocationActions').goBack;
57 changes: 57 additions & 0 deletions modules/actions/LocationActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
var LocationDispatcher = require('../dispatchers/LocationDispatcher');
var makePath = require('../helpers/makePath');

/**
* Actions that modify the URL.
*/
var LocationActions = {

PUSH: 'push',
REPLACE: 'replace',
POP: 'pop',
UPDATE_SCROLL: 'update-scroll',

/**
* Transitions to the URL specified in the arguments by pushing
* a new URL onto the history stack.
*/
transitionTo: function (to, params, query) {
LocationDispatcher.handleViewAction({
type: LocationActions.PUSH,
path: makePath(to, params, query)
});
},

/**
* Transitions to the URL specified in the arguments by replacing
* the current URL in the history stack.
*/
replaceWith: function (to, params, query) {
LocationDispatcher.handleViewAction({
type: LocationActions.REPLACE,
path: makePath(to, params, query)
});
},

/**
* Transitions to the previous URL.
*/
goBack: function () {
LocationDispatcher.handleViewAction({
type: LocationActions.POP
});
},

/**
* Updates the window's scroll position to the last known position
* for the current URL path.
*/
updateScroll: function () {
LocationDispatcher.handleViewAction({
type: LocationActions.UPDATE_SCROLL
});
}

};

module.exports = LocationActions;
2 changes: 1 addition & 1 deletion modules/components/Link.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
var React = require('react');
var ActiveState = require('../mixins/ActiveState');
var transitionTo = require('../actions/LocationActions').transitionTo;
var withoutProperties = require('../helpers/withoutProperties');
var transitionTo = require('../helpers/transitionTo');
var hasOwnProperty = require('../helpers/hasOwnProperty');
var makeHref = require('../helpers/makeHref');
var warning = require('react/lib/warning');
Expand Down
22 changes: 9 additions & 13 deletions modules/components/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ var React = require('react');
var warning = require('react/lib/warning');
var copyProperties = require('react/lib/copyProperties');
var Promise = require('es6-promise').Promise;
var LocationActions = require('../actions/LocationActions');
var Route = require('../components/Route');
var goBack = require('../helpers/goBack');
var replaceWith = require('../helpers/replaceWith');
var Path = require('../helpers/Path');
var Redirect = require('../helpers/Redirect');
var Transition = require('../helpers/Transition');
Expand Down Expand Up @@ -37,9 +36,9 @@ function defaultAbortedTransitionHandler(transition) {
var reason = transition.abortReason;

if (reason instanceof Redirect) {
replaceWith(reason.to, reason.params, reason.query);
LocationActions.replaceWith(reason.to, reason.params, reason.query);
} else {
goBack();
LocationActions.goBack();
}
}

Expand All @@ -59,6 +58,11 @@ function defaultTransitionErrorHandler(error) {
throw error; // This error probably originated in a transition hook.
}

function maybeUpdateScroll(routes, rootRoute) {
if (!routes.props.preserveScrollPosition && !rootRoute.props.preserveScrollPosition)
LocationActions.updateScroll();
}

/**
* The <Routes> component configures the route hierarchy and renders the
* route matching the current location when rendered into a document.
Expand Down Expand Up @@ -98,7 +102,6 @@ var Routes = React.createClass({
};
},


getLocation: function () {
var location = this.props.location;

Expand Down Expand Up @@ -185,7 +188,7 @@ var Routes = React.createClass({
var rootMatch = getRootMatch(nextState.matches);

if (rootMatch)
maybeScrollWindow(routes, rootMatch.route);
maybeUpdateScroll(routes, rootMatch.route);
}

return transition;
Expand Down Expand Up @@ -443,11 +446,4 @@ function reversedArray(array) {
return array.slice(0).reverse();
}

function maybeScrollWindow(routes, rootRoute) {
if (routes.props.preserveScrollPosition || rootRoute.props.preserveScrollPosition)
return;

window.scrollTo(0, 0);
}

module.exports = Routes;
18 changes: 18 additions & 0 deletions modules/dispatchers/LocationDispatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
var copyProperties = require('react/lib/copyProperties');
var Dispatcher = require('react-dispatcher');

/**
* Dispatches actions that modify the URL.
*/
var LocationDispatcher = copyProperties(new Dispatcher, {

handleViewAction: function (action) {
this.dispatch({
source: 'VIEW_ACTION',
action: action
});
}

});

module.exports = LocationDispatcher;
2 changes: 1 addition & 1 deletion modules/helpers/Transition.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var mixInto = require('react/lib/mixInto');
var transitionTo = require('./transitionTo');
var transitionTo = require('../actions/LocationActions').transitionTo;
var Redirect = require('./Redirect');

/**
Expand Down
10 changes: 0 additions & 10 deletions modules/helpers/goBack.js

This file was deleted.

12 changes: 0 additions & 12 deletions modules/helpers/replaceWith.js

This file was deleted.

12 changes: 0 additions & 12 deletions modules/helpers/transitionTo.js

This file was deleted.

67 changes: 54 additions & 13 deletions modules/stores/PathStore.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var warning = require('react/lib/warning');
var EventEmitter = require('events').EventEmitter;
var LocationActions = require('../actions/LocationActions');
var LocationDispatcher = require('../dispatchers/LocationDispatcher');
var supportsHistory = require('../helpers/supportsHistory');
var HistoryLocation = require('../locations/HistoryLocation');
var RefreshLocation = require('../locations/RefreshLocation');
Expand All @@ -11,6 +13,15 @@ function notifyChange() {
_events.emit(CHANGE_EVENT);
}

var _scrollPositions = {};

function recordScrollPosition(path) {
_scrollPositions[path] = {
x: window.scrollX,
y: window.scrollY
};
}

var _location;

/**
Expand Down Expand Up @@ -58,27 +69,57 @@ var PathStore = {
_location = null;
},

/**
* Returns the location object currently in use.
*/
getLocation: function () {
return _location;
},

push: function (path) {
if (_location.getCurrentPath() !== path)
_location.push(path);
},

replace: function (path) {
if (_location.getCurrentPath() !== path)
_location.replace(path);
/**
* Returns the current URL path.
*/
getCurrentPath: function () {
return _location.getCurrentPath();
},

pop: function () {
_location.pop();
/**
* Returns the last known scroll position for the given path.
*/
getScrollPosition: function (path) {
return _scrollPositions[path] || { x: 0, y: 0 };
},

getCurrentPath: function () {
return _location.getCurrentPath();
}
dispatchToken: LocationDispatcher.register(function (payload) {
var action = payload.action;
var currentPath = _location.getCurrentPath();

switch (action.type) {
case LocationActions.PUSH:
if (currentPath !== action.path) {
recordScrollPosition(currentPath);
_location.push(action.path);
}
break;

case LocationActions.REPLACE:
if (currentPath !== action.path) {
recordScrollPosition(currentPath);
_location.replace(action.path);
}
break;

case LocationActions.POP:
recordScrollPosition(currentPath);
_location.pop();
break;

case LocationActions.UPDATE_SCROLL:
var p = PathStore.getScrollPosition(currentPath);
window.scrollTo(p.x, p.y);
break;
}
})

};

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"dependencies": {
"es6-promise": "^1.0.0",
"events": "^1.0.1",
"qs": "^1.2.2"
"qs": "^1.2.2",
"react-dispatcher": "^0.2.1"
},
"keywords": [
"react",
Expand Down
2 changes: 1 addition & 1 deletion replaceWith.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = require('./modules/helpers/replaceWith');
module.exports = require('./modules/actions/LocationActions').replaceWith;
28 changes: 15 additions & 13 deletions specs/PathStore.spec.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,54 @@
require('./helper');
var MemoryLocation = require('../modules/locations/MemoryLocation');
var PathStore = require('../modules/stores/PathStore');
var transitionTo = require('../modules/actions/LocationActions').transitionTo;
var replaceWith = require('../modules/actions/LocationActions').replaceWith;
var goBack = require('../modules/actions/LocationActions').goBack;
var getCurrentPath = require('../modules/stores/PathStore').getCurrentPath;

describe('PathStore', function () {

beforeEach(function () {
PathStore.push('/one');
transitionTo('/one');
});

describe('when a new path is pushed to the URL', function () {
beforeEach(function () {
PathStore.push('/two');
transitionTo('/two');
});

it('has the correct path', function () {
expect(PathStore.getCurrentPath()).toEqual('/two');
expect(getCurrentPath()).toEqual('/two');
});
});

describe('when a new path is used to replace the URL', function () {
beforeEach(function () {
PathStore.push('/two');
PathStore.replace('/three');
transitionTo('/two');
replaceWith('/three');
});

it('has the correct path', function () {
expect(PathStore.getCurrentPath()).toEqual('/three');
expect(getCurrentPath()).toEqual('/three');
});

describe('going back in history', function () {
beforeEach(function () {
PathStore.pop();
goBack();
});

it('has the path before the one that was replaced', function () {
expect(PathStore.getCurrentPath()).toEqual('/one');
expect(getCurrentPath()).toEqual('/one');
});
});
});

describe('when going back in history', function () {
beforeEach(function () {
PathStore.push('/two');
PathStore.pop();
transitionTo('/two');
goBack();
});

it('has the correct path', function () {
expect(PathStore.getCurrentPath()).toEqual('/one');
expect(getCurrentPath()).toEqual('/one');
});
});

Expand Down
3 changes: 2 additions & 1 deletion specs/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ beforeEach(function () {
RouteStore.unregisterAllRoutes();
});

var transitionTo = require('../modules/actions/LocationActions').transitionTo;
var MemoryLocation = require('../modules/locations/MemoryLocation');
var PathStore = require('../modules/stores/PathStore');

beforeEach(function () {
PathStore.setup(MemoryLocation);
PathStore.push('/');
transitionTo('/');
});

afterEach(function () {
Expand Down
2 changes: 1 addition & 1 deletion transitionTo.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = require('./modules/helpers/transitionTo');
module.exports = require('./modules/actions/LocationActions').transitionTo;

0 comments on commit b7e21bb

Please sign in to comment.