Skip to content

Commit b7e21bb

Browse files
committed
[fixed] Window scrolling
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
1 parent 94c7a35 commit b7e21bb

15 files changed

+162
-80
lines changed

goBack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
module.exports = require('./modules/helpers/goBack');
1+
module.exports = require('./modules/actions/LocationActions').goBack;

modules/actions/LocationActions.js

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
var LocationDispatcher = require('../dispatchers/LocationDispatcher');
2+
var makePath = require('../helpers/makePath');
3+
4+
/**
5+
* Actions that modify the URL.
6+
*/
7+
var LocationActions = {
8+
9+
PUSH: 'push',
10+
REPLACE: 'replace',
11+
POP: 'pop',
12+
UPDATE_SCROLL: 'update-scroll',
13+
14+
/**
15+
* Transitions to the URL specified in the arguments by pushing
16+
* a new URL onto the history stack.
17+
*/
18+
transitionTo: function (to, params, query) {
19+
LocationDispatcher.handleViewAction({
20+
type: LocationActions.PUSH,
21+
path: makePath(to, params, query)
22+
});
23+
},
24+
25+
/**
26+
* Transitions to the URL specified in the arguments by replacing
27+
* the current URL in the history stack.
28+
*/
29+
replaceWith: function (to, params, query) {
30+
LocationDispatcher.handleViewAction({
31+
type: LocationActions.REPLACE,
32+
path: makePath(to, params, query)
33+
});
34+
},
35+
36+
/**
37+
* Transitions to the previous URL.
38+
*/
39+
goBack: function () {
40+
LocationDispatcher.handleViewAction({
41+
type: LocationActions.POP
42+
});
43+
},
44+
45+
/**
46+
* Updates the window's scroll position to the last known position
47+
* for the current URL path.
48+
*/
49+
updateScroll: function () {
50+
LocationDispatcher.handleViewAction({
51+
type: LocationActions.UPDATE_SCROLL
52+
});
53+
}
54+
55+
};
56+
57+
module.exports = LocationActions;

modules/components/Link.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var React = require('react');
22
var ActiveState = require('../mixins/ActiveState');
3+
var transitionTo = require('../actions/LocationActions').transitionTo;
34
var withoutProperties = require('../helpers/withoutProperties');
4-
var transitionTo = require('../helpers/transitionTo');
55
var hasOwnProperty = require('../helpers/hasOwnProperty');
66
var makeHref = require('../helpers/makeHref');
77
var warning = require('react/lib/warning');

modules/components/Routes.js

+9-13
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ var React = require('react');
22
var warning = require('react/lib/warning');
33
var copyProperties = require('react/lib/copyProperties');
44
var Promise = require('es6-promise').Promise;
5+
var LocationActions = require('../actions/LocationActions');
56
var Route = require('../components/Route');
6-
var goBack = require('../helpers/goBack');
7-
var replaceWith = require('../helpers/replaceWith');
87
var Path = require('../helpers/Path');
98
var Redirect = require('../helpers/Redirect');
109
var Transition = require('../helpers/Transition');
@@ -37,9 +36,9 @@ function defaultAbortedTransitionHandler(transition) {
3736
var reason = transition.abortReason;
3837

3938
if (reason instanceof Redirect) {
40-
replaceWith(reason.to, reason.params, reason.query);
39+
LocationActions.replaceWith(reason.to, reason.params, reason.query);
4140
} else {
42-
goBack();
41+
LocationActions.goBack();
4342
}
4443
}
4544

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

61+
function maybeUpdateScroll(routes, rootRoute) {
62+
if (!routes.props.preserveScrollPosition && !rootRoute.props.preserveScrollPosition)
63+
LocationActions.updateScroll();
64+
}
65+
6266
/**
6367
* The <Routes> component configures the route hierarchy and renders the
6468
* route matching the current location when rendered into a document.
@@ -98,7 +102,6 @@ var Routes = React.createClass({
98102
};
99103
},
100104

101-
102105
getLocation: function () {
103106
var location = this.props.location;
104107

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

187190
if (rootMatch)
188-
maybeScrollWindow(routes, rootMatch.route);
191+
maybeUpdateScroll(routes, rootMatch.route);
189192
}
190193

191194
return transition;
@@ -443,11 +446,4 @@ function reversedArray(array) {
443446
return array.slice(0).reverse();
444447
}
445448

446-
function maybeScrollWindow(routes, rootRoute) {
447-
if (routes.props.preserveScrollPosition || rootRoute.props.preserveScrollPosition)
448-
return;
449-
450-
window.scrollTo(0, 0);
451-
}
452-
453449
module.exports = Routes;
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
var copyProperties = require('react/lib/copyProperties');
2+
var Dispatcher = require('react-dispatcher');
3+
4+
/**
5+
* Dispatches actions that modify the URL.
6+
*/
7+
var LocationDispatcher = copyProperties(new Dispatcher, {
8+
9+
handleViewAction: function (action) {
10+
this.dispatch({
11+
source: 'VIEW_ACTION',
12+
action: action
13+
});
14+
}
15+
16+
});
17+
18+
module.exports = LocationDispatcher;

modules/helpers/Transition.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
var mixInto = require('react/lib/mixInto');
2-
var transitionTo = require('./transitionTo');
2+
var transitionTo = require('../actions/LocationActions').transitionTo;
33
var Redirect = require('./Redirect');
44

55
/**

modules/helpers/goBack.js

-10
This file was deleted.

modules/helpers/replaceWith.js

-12
This file was deleted.

modules/helpers/transitionTo.js

-12
This file was deleted.

modules/stores/PathStore.js

+54-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
var warning = require('react/lib/warning');
22
var EventEmitter = require('events').EventEmitter;
3+
var LocationActions = require('../actions/LocationActions');
4+
var LocationDispatcher = require('../dispatchers/LocationDispatcher');
35
var supportsHistory = require('../helpers/supportsHistory');
46
var HistoryLocation = require('../locations/HistoryLocation');
57
var RefreshLocation = require('../locations/RefreshLocation');
@@ -11,6 +13,15 @@ function notifyChange() {
1113
_events.emit(CHANGE_EVENT);
1214
}
1315

16+
var _scrollPositions = {};
17+
18+
function recordScrollPosition(path) {
19+
_scrollPositions[path] = {
20+
x: window.scrollX,
21+
y: window.scrollY
22+
};
23+
}
24+
1425
var _location;
1526

1627
/**
@@ -58,27 +69,57 @@ var PathStore = {
5869
_location = null;
5970
},
6071

72+
/**
73+
* Returns the location object currently in use.
74+
*/
6175
getLocation: function () {
6276
return _location;
6377
},
6478

65-
push: function (path) {
66-
if (_location.getCurrentPath() !== path)
67-
_location.push(path);
68-
},
69-
70-
replace: function (path) {
71-
if (_location.getCurrentPath() !== path)
72-
_location.replace(path);
79+
/**
80+
* Returns the current URL path.
81+
*/
82+
getCurrentPath: function () {
83+
return _location.getCurrentPath();
7384
},
7485

75-
pop: function () {
76-
_location.pop();
86+
/**
87+
* Returns the last known scroll position for the given path.
88+
*/
89+
getScrollPosition: function (path) {
90+
return _scrollPositions[path] || { x: 0, y: 0 };
7791
},
7892

79-
getCurrentPath: function () {
80-
return _location.getCurrentPath();
81-
}
93+
dispatchToken: LocationDispatcher.register(function (payload) {
94+
var action = payload.action;
95+
var currentPath = _location.getCurrentPath();
96+
97+
switch (action.type) {
98+
case LocationActions.PUSH:
99+
if (currentPath !== action.path) {
100+
recordScrollPosition(currentPath);
101+
_location.push(action.path);
102+
}
103+
break;
104+
105+
case LocationActions.REPLACE:
106+
if (currentPath !== action.path) {
107+
recordScrollPosition(currentPath);
108+
_location.replace(action.path);
109+
}
110+
break;
111+
112+
case LocationActions.POP:
113+
recordScrollPosition(currentPath);
114+
_location.pop();
115+
break;
116+
117+
case LocationActions.UPDATE_SCROLL:
118+
var p = PathStore.getScrollPosition(currentPath);
119+
window.scrollTo(p.x, p.y);
120+
break;
121+
}
122+
})
82123

83124
};
84125

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"dependencies": {
5151
"es6-promise": "^1.0.0",
5252
"events": "^1.0.1",
53-
"qs": "^1.2.2"
53+
"qs": "^1.2.2",
54+
"react-dispatcher": "^0.2.1"
5455
},
5556
"keywords": [
5657
"react",

replaceWith.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
module.exports = require('./modules/helpers/replaceWith');
1+
module.exports = require('./modules/actions/LocationActions').replaceWith;

specs/PathStore.spec.js

+15-13
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,54 @@
11
require('./helper');
2-
var MemoryLocation = require('../modules/locations/MemoryLocation');
3-
var PathStore = require('../modules/stores/PathStore');
2+
var transitionTo = require('../modules/actions/LocationActions').transitionTo;
3+
var replaceWith = require('../modules/actions/LocationActions').replaceWith;
4+
var goBack = require('../modules/actions/LocationActions').goBack;
5+
var getCurrentPath = require('../modules/stores/PathStore').getCurrentPath;
46

57
describe('PathStore', function () {
68

79
beforeEach(function () {
8-
PathStore.push('/one');
10+
transitionTo('/one');
911
});
1012

1113
describe('when a new path is pushed to the URL', function () {
1214
beforeEach(function () {
13-
PathStore.push('/two');
15+
transitionTo('/two');
1416
});
1517

1618
it('has the correct path', function () {
17-
expect(PathStore.getCurrentPath()).toEqual('/two');
19+
expect(getCurrentPath()).toEqual('/two');
1820
});
1921
});
2022

2123
describe('when a new path is used to replace the URL', function () {
2224
beforeEach(function () {
23-
PathStore.push('/two');
24-
PathStore.replace('/three');
25+
transitionTo('/two');
26+
replaceWith('/three');
2527
});
2628

2729
it('has the correct path', function () {
28-
expect(PathStore.getCurrentPath()).toEqual('/three');
30+
expect(getCurrentPath()).toEqual('/three');
2931
});
3032

3133
describe('going back in history', function () {
3234
beforeEach(function () {
33-
PathStore.pop();
35+
goBack();
3436
});
3537

3638
it('has the path before the one that was replaced', function () {
37-
expect(PathStore.getCurrentPath()).toEqual('/one');
39+
expect(getCurrentPath()).toEqual('/one');
3840
});
3941
});
4042
});
4143

4244
describe('when going back in history', function () {
4345
beforeEach(function () {
44-
PathStore.push('/two');
45-
PathStore.pop();
46+
transitionTo('/two');
47+
goBack();
4648
});
4749

4850
it('has the correct path', function () {
49-
expect(PathStore.getCurrentPath()).toEqual('/one');
51+
expect(getCurrentPath()).toEqual('/one');
5052
});
5153
});
5254

specs/helper.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ beforeEach(function () {
1313
RouteStore.unregisterAllRoutes();
1414
});
1515

16+
var transitionTo = require('../modules/actions/LocationActions').transitionTo;
1617
var MemoryLocation = require('../modules/locations/MemoryLocation');
1718
var PathStore = require('../modules/stores/PathStore');
1819

1920
beforeEach(function () {
2021
PathStore.setup(MemoryLocation);
21-
PathStore.push('/');
22+
transitionTo('/');
2223
});
2324

2425
afterEach(function () {

transitionTo.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
module.exports = require('./modules/helpers/transitionTo');
1+
module.exports = require('./modules/actions/LocationActions').transitionTo;

0 commit comments

Comments
 (0)