Skip to content

Commit

Permalink
[changed] Remove preserveScrollPosition, add scrollStrategy
Browse files Browse the repository at this point in the history
This removes support for `preserveScrollPosition` due to its unforunate naming.
Instead, we introduce three scrolling strategies:

* `none`: Router doesn't scroll the window when routes change
* `scrollToTop` (default): Router always scrolls to top when routes change
* `imitateBrowser`: Router tries to act like browser acts with server-rendered pages: it scrolls to top when clicking on links, but tries to restore position when navigating back and forward

You can only specify these on <Routes />.
Per-route overrides are not supported, but you can supply a custom strategy object.

This also fixes #252.

Migration path:

The default changed from what corresponded to `imitateBrowser`, to `scrollToTop`.
If router's server-rendered scrolling imitation worked well for you, you must now specify it explicitly:

```
// before
<Routes>
<Routes preserveScrollPosition={false}>

// after
<Routes scrollStrategy='imitateBrowser'>
```

If you wish router to not try to manage scrolling, you must opt out:

```
// before
<Routes preserveScrollPosition={true}>

// after
<Routes scrollStrategy='none'>
```

Also, as a third option, you may now use the simple `scrollToTop` strategy.
  • Loading branch information
gaearon committed Sep 29, 2014
1 parent c96e34d commit 47f0599
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 136 deletions.
Binary file added modules/.DS_Store
Binary file not shown.
110 changes: 88 additions & 22 deletions modules/actions/LocationActions.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,96 @@
var supportsHistory = require('../utils/supportsHistory');
var HistoryLocation = require('../locations/HistoryLocation');
var RefreshLocation = require('../locations/RefreshLocation');
var LocationDispatcher = require('../dispatchers/LocationDispatcher');
var ActionTypes = require('../constants/ActionTypes');
var warning = require('react/lib/warning');
var isAbsoluteURL = require('../utils/isAbsoluteURL');
var makePath = require('../utils/makePath');

function loadURL(url) {
window.location = url;
}

var _location = null;
var _isDispatching = false;
var _previousPath = null;

function dispatchAction(actionType, operation) {
if (_isDispatching)
throw new Error('Cannot handle ' + actionType + ' in the middle of another action.');

_isDispatching = true;

var scrollPosition = {
x: window.scrollX,
y: window.scrollY
};

if (typeof operation === 'function')
operation(_location);

var path = _location.getCurrentPath();
LocationDispatcher.handleViewAction({
type: actionType,
path: path,
scrollPosition: scrollPosition
});

_isDispatching = false;
_previousPath = path;
}

function handleChange() {
var path = _location.getCurrentPath();

// Ignore changes inside or caused by dispatchAction
if (!_isDispatching && path !== _previousPath) {
dispatchAction(ActionTypes.POP);
}
}

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

PUSH: 'push',
REPLACE: 'replace',
POP: 'pop',
UPDATE_SCROLL: 'update-scroll',
getLocation: function () {
return _location;
},

setup: function (location) {
// When using HistoryLocation, automatically fallback
// to RefreshLocation in browsers that do not support
// the HTML5 history API.
if (location === HistoryLocation && !supportsHistory())
location = RefreshLocation;

if (_location !== null) {
warning(
_location === location,
'Cannot use location %s, already using %s', location, _location
);
return;
}

_location = location;

if (_location !== null) {
dispatchAction(ActionTypes.SETUP, function (location) {
if (typeof location.setup === 'function')
location.setup(handleChange);
});
}
},

teardown: function () {
if (_location !== null) {
if (typeof _location.teardown === 'function')
_location.teardown();

_location = null;
}
},

/**
* Transitions to the URL specified in the arguments by pushing
Expand All @@ -24,9 +100,9 @@ var LocationActions = {
if (isAbsoluteURL(to)) {
loadURL(to);
} else {
LocationDispatcher.handleViewAction({
type: LocationActions.PUSH,
path: makePath(to, params, query)
dispatchAction(ActionTypes.PUSH, function (location) {
var path = makePath(to, params, query);
location.push(path);
});
}
},
Expand All @@ -39,9 +115,9 @@ var LocationActions = {
if (isAbsoluteURL(to)) {
loadURL(to);
} else {
LocationDispatcher.handleViewAction({
type: LocationActions.REPLACE,
path: makePath(to, params, query)
dispatchAction(ActionTypes.REPLACE, function (location) {
var path = makePath(to, params, query);
location.replace(path);
});
}
},
Expand All @@ -50,18 +126,8 @@ var LocationActions = {
* 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
dispatchAction(ActionTypes.POP, function (location) {
location.pop();
});
}

Expand Down
7 changes: 5 additions & 2 deletions modules/components/Routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var Route = require('../components/Route');
var ActiveDelegate = require('../mixins/ActiveDelegate');
var PathListener = require('../mixins/PathListener');
var RouteStore = require('../stores/RouteStore');
var ScrollStore = require('../stores/ScrollStore');
var Path = require('../utils/Path');
var Promise = require('../utils/Promise');
var Redirect = require('../utils/Redirect');
Expand Down Expand Up @@ -51,9 +52,11 @@ function maybeUpdateScroll(routes) {
return;

var currentRoute = routes.getCurrentRoute();
var scrollPosition = ScrollStore.getScrollPosition();

if (!routes.props.preserveScrollPosition && currentRoute && !currentRoute.props.preserveScrollPosition)
LocationActions.updateScroll();
if (currentRoute && scrollPosition) {
window.scrollTo(scrollPosition.x, scrollPosition.y);
}
}

/**
Expand Down
10 changes: 10 additions & 0 deletions modules/constants/ActionTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
var keyMirror = require('react/lib/keyMirror');

var ActionTypes = keyMirror({
SETUP: null,
PUSH: null,
REPLACE: null,
POP: null
});

module.exports = ActionTypes;
2 changes: 0 additions & 2 deletions modules/locations/HistoryLocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ var HistoryLocation = {

push: function (path) {
window.history.pushState({ path: path }, '', path);
_onChange();
},

replace: function (path) {
window.history.replaceState({ path: path }, '', path);
_onChange();
},

pop: function () {
Expand Down
8 changes: 0 additions & 8 deletions modules/locations/MemoryLocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,19 @@ var warning = require('react/lib/warning');

var _lastPath = null;
var _currentPath = null;
var _onChange;

/**
* A Location that does not require a DOM.
*/
var MemoryLocation = {

setup: function (onChange) {
_onChange = onChange;
},

push: function (path) {
_lastPath = _currentPath;
_currentPath = path;
_onChange();
},

replace: function (path) {
_currentPath = path;
_onChange();
},

pop: function () {
Expand All @@ -32,7 +25,6 @@ var MemoryLocation = {

_currentPath = _lastPath;
_lastPath = null;
_onChange();
},

getCurrentPath: function () {
Expand Down
45 changes: 41 additions & 4 deletions modules/mixins/PathListener.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
var React = require('react');
var LocationActions = require('../actions/LocationActions');
var DefaultLocation = require('../locations/DefaultLocation');
var HashLocation = require('../locations/HashLocation');
var HistoryLocation = require('../locations/HistoryLocation');
var RefreshLocation = require('../locations/RefreshLocation');
var NoneStrategy = require('../strategies/NoneStrategy');
var ScrollToTopStrategy = require('../strategies/ScrollToTopStrategy');
var ImitateBrowserStrategy = require('../strategies/ImitateBrowserStrategy');
var PathStore = require('../stores/PathStore');
var ScrollStore = require('../stores/ScrollStore');

/**
* A hash of { name, location } pairs.
Expand All @@ -14,6 +18,15 @@ var NAMED_LOCATIONS = {
refresh: RefreshLocation
};

/**
* A hash of { name, scrollStrategy } pairs.
*/
var NAMED_SCROLL_STRATEGIES = {
none: NoneStrategy,
scrollToTop: ScrollToTopStrategy,
imitateBrowser: ImitateBrowserStrategy
};

/**
* A mixin for components that listen for changes to the current
* URL path.
Expand All @@ -26,12 +39,20 @@ var PathListener = {

if (typeof location === 'string' && !(location in NAMED_LOCATIONS))
return new Error('Unknown location "' + location + '", see ' + componentName);
}
},

scrollStrategy: function (props, propName, componentName) {
var scrollStrategy = props[propName];

if (typeof scrollStrategy === 'string' && !(scrollStrategy in NAMED_SCROLL_STRATEGIES))
return new Error('Unknown scrollStrategy "' + scrollStrategy + '", see ' + componentName);
},
},

getDefaultProps: function () {
return {
location: DefaultLocation
location: DefaultLocation,
scrollStrategy: ScrollToTopStrategy
};
},

Expand All @@ -48,8 +69,22 @@ var PathListener = {
return location;
},

/**
* Gets the scroll strategy object this component uses to
* restore scroll position when the path changes.
*/
getScrollStrategy: function () {
var scrollStrategy = this.props.scrollStrategy;

if (typeof scrollStrategy === 'string')
return NAMED_SCROLL_STRATEGIES[scrollStrategy];

return scrollStrategy;
},

componentWillMount: function () {
PathStore.setup(this.getLocation());
ScrollStore.setup(this.getScrollStrategy());
LocationActions.setup(this.getLocation());

if (this.updatePath)
this.updatePath(PathStore.getCurrentPath());
Expand All @@ -61,6 +96,8 @@ var PathListener = {

componentWillUnmount: function () {
PathStore.removeChangeListener(this.handlePathChange);
ScrollStore.teardown();
LocationActions.teardown();
},

handlePathChange: function () {
Expand Down
Loading

0 comments on commit 47f0599

Please sign in to comment.