Skip to content

Commit

Permalink
Add location.state
Browse files Browse the repository at this point in the history
  • Loading branch information
mjackson committed Jun 8, 2015
1 parent ddd9af8 commit 11cdab3
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 142 deletions.
60 changes: 32 additions & 28 deletions modules/BrowserHistory.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import DOMHistory from './DOMHistory';
import { getWindowPath, supportsHistory } from './DOMUtils';
import { getWindowPath, getWindowScrollPosition, supportsHistory } from './DOMUtils';
import NavigationTypes from './NavigationTypes';

function createRandomKey() {
return Math.random().toString(36).substr(2);
}
import Location from './Location';

/**
* A history implementation for DOM environments that support the
Expand All @@ -18,26 +15,31 @@ function createRandomKey() {
*/
export class BrowserHistory extends DOMHistory {

constructor(getScrollPosition) {
super(getScrollPosition);
constructor(getScrollPosition=getWindowScrollPosition) {
super();
this.getScrollPosition = getScrollPosition;
this.handlePopState = this.handlePopState.bind(this);
this.isSupported = supportsHistory();
}

_updateLocation(navigationType) {
var key = null;
var state = null;

if (this.isSupported) {
var state = window.history.state;
key = state && state.key;
state = window.history.state || {};

if (!key) {
key = createRandomKey();
window.history.replaceState({ key }, '');
if (!state.key) {
state.key = createRandomKey();
window.history.replaceState(state, '');
}
}

this.location = this._createLocation(getWindowPath(), key, navigationType);
this.location = new Location(state, getWindowPath(), navigationType);
}

setup() {
if (this.location == null)
this._updateLocation();
}

handlePopState(event) {
Expand Down Expand Up @@ -72,31 +74,33 @@ export class BrowserHistory extends DOMHistory {
}
}

setup() {
if (this.location == null)
this._updateLocation();
}

// http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate
push(path) {
pushState(state, path) {
if (this.isSupported) {
this._recordScrollPosition();
var currentState = window.history.state;

var key = createRandomKey();
window.history.pushState({ key }, '', path);
this.location = this._createLocation(path, key, NavigationTypes.PUSH);
if (currentState) {
Object.assign(currentState, this.getScrollPosition());
window.history.replaceState(currentState, '');
}

state = this._createState(state);

window.history.pushState(state, '', path);
this.location = new Location(state, path, NavigationTypes.PUSH);
this._notifyChange();
} else {
window.location = path;
}
}

// http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-replacestate
replace(path) {
replaceState(state, path) {
if (this.isSupported) {
var key = createRandomKey();
window.history.replaceState({ key }, '', path);
this.location = this._createLocation(path, key, NavigationTypes.REPLACE);
state = this._createState(state);

window.history.replaceState(state, '', path);
this.location = new Location(state, path, NavigationTypes.REPLACE);
this._notifyChange();
} else {
window.location.replace(path);
Expand Down
24 changes: 24 additions & 0 deletions modules/ChangeEmitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class ChangeEmitter {

constructor() {
this.changeListeners = [];
}

_notifyChange() {
for (var i = 0, len = this.changeListeners.length; i < len; ++i)
this.changeListeners[i].call(this);
}

addChangeListener(listener) {
this.changeListeners.push(listener);
}

removeChangeListener(listener) {
this.changeListeners = this.changeListeners.filter(function (li) {
return li !== listener;
});
}

}

export default ChangeEmitter;
24 changes: 1 addition & 23 deletions modules/DOMHistory.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import History from './History';
import { getWindowScrollPosition } from './DOMUtils';
import Location from './Location';

/**
* A history interface that assumes a DOM environment.
*/
export class DOMHistory extends History {

constructor(getScrollPosition=getWindowScrollPosition) {
super();
this.getScrollPosition = getScrollPosition;
this.scrollHistory = {};
}
class DOMHistory extends History {

go(n) {
if (n === 0)
Expand All @@ -20,20 +12,6 @@ export class DOMHistory extends History {
window.history.go(n);
}

_createLocation(path, key, navigationType) {
var scrollKey = key || path;
var scrollPosition = this.scrollHistory[scrollKey];

return new Location(path, key, navigationType, scrollPosition);
}

_recordScrollPosition() {
var location = this.location;
var scrollKey = location.key || location.path;

this.scrollHistory[scrollKey] = this.getScrollPosition();
}

}

export default DOMHistory;
27 changes: 12 additions & 15 deletions modules/DOMUtils.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
function getHashPath() {
export var canUseDOM = !!(
(typeof window !== 'undefined' &&
window.document && window.document.createElement)
);

export function getHashPath() {
return decodeURI(
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
window.location.href.split('#')[1] || ''
);
}

function replaceHashPath(path) {
export function replaceHashPath(path) {
window.location.replace(
window.location.pathname + window.location.search + '#' + path
);
}

function getWindowPath() {
export function getWindowPath() {
return decodeURI(
window.location.pathname + window.location.search
);
}

function getWindowScrollPosition() {
export function getWindowScrollPosition() {
return {
x: window.pageXOffset || document.documentElement.scrollLeft,
y: window.pageYOffset || document.documentElement.scrollTop
scrollX: window.pageXOffset || document.documentElement.scrollLeft,
scrollY: window.pageYOffset || document.documentElement.scrollTop
};
}

Expand All @@ -31,18 +36,10 @@ function getWindowScrollPosition() {
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
* changed to avoid false negatives for Windows Phones: https://github.com/rackt/react-router/issues/586
*/
function supportsHistory() {
export function supportsHistory() {
var ua = navigator.userAgent;
if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1) {
return false;
}
return window.history && 'pushState' in window.history;
}

module.exports = {
getHashPath,
replaceHashPath,
getWindowPath,
getWindowScrollPosition,
supportsHistory
};
56 changes: 42 additions & 14 deletions modules/HashHistory.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import DOMHistory from './DOMHistory';
import NavigationTypes from './NavigationTypes';
import { getHashPath, replaceHashPath } from './DOMUtils';
import { getHashPath, getWindowScrollPosition, replaceHashPath } from './DOMUtils';
import { isAbsolutePath } from './URLUtils';
import Location from './Location';

function ensureSlash() {
var path = getHashPath();
Expand All @@ -21,13 +22,29 @@ function ensureSlash() {
*/
export class HashHistory extends DOMHistory {

constructor(getScrollPosition) {
super(getScrollPosition);
constructor(getScrollPosition=getWindowScrollPosition) {
super();
this.getScrollPosition = getScrollPosition;
this.handleHashChange = this.handleHashChange.bind(this);

// We keep known states in memory so we don't have to keep a key
// in the URL. We could have persistence if we did, but we can always
// add that later if people need it. I suspect that at this point
// most users who need state are already using BrowserHistory.
this.states = {};
}

_updateLocation(navigationType) {
this.location = this._createLocation(getHashPath(), null, navigationType);
var path = getHashPath();
var state = this.states[path] || null;
this.location = new Location(state, path, navigationType);
}

setup() {
if (this.location == null) {
ensureSlash();
this._updateLocation();
}
}

handleHashChange() {
Expand Down Expand Up @@ -61,30 +78,41 @@ export class HashHistory extends DOMHistory {
}
}

setup() {
if (this.location == null) {
ensureSlash();
this._updateLocation();
pushState(state, path) {
var location = this.location;
var currentState = location && this.states[location.path];

if (currentState) {
Object.assign(currentState, this.getScrollPosition());
this.states[location.path] = currentState;
}
}

push(path) {
this._recordScrollPosition();
state = this._createState(state);
this.states[path] = state;

this._ignoreHashChange = true;
window.location.hash = path;
this._ignoreHashChange = false;

this.location = this._createLocation(path, null, NavigationTypes.PUSH);
this.location = new Location(state, path, NavigationTypes.PUSH);

this._notifyChange();
}

replace(path) {
replaceState(state, path) {
state = this._createState(state);

var location = this.location;

if (location && this.states[location.path])
this.states[location.path] = state;

this._ignoreHashChange = true;
replaceHashPath(path);
this._ignoreHashChange = false;

this.location = this._createLocation(path, null, NavigationTypes.REPLACE);
this.location = new Location(state, path, NavigationTypes.REPLACE);

this._notifyChange();
}

Expand Down
40 changes: 20 additions & 20 deletions modules/History.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import invariant from 'invariant';
import ChangeEmitter from './ChangeEmitter';

var RequiredSubclassMethods = [ 'push', 'replace', 'go' ];
var RequiredSubclassMethods = [ 'pushState', 'replaceState', 'go' ];

function createRandomKey() {
return Math.random().toString(36).substr(2);
}

/**
* A history interface that normalizes the differences across
* various environments and implementations. Requires concrete
* subclasses to implement the following methods:
*
* - push(path)
* - replace(path)
* - pushState(state, path)
* - replaceState(state, path)
* - go(n)
*/
export class History {
class History extends ChangeEmitter {

constructor() {
super();

RequiredSubclassMethods.forEach(function (method) {
invariant(
typeof this[method] === 'function',
Expand All @@ -22,25 +29,9 @@ export class History {
);
}, this);

this.changeListeners = [];
this.location = null;
}

_notifyChange() {
for (var i = 0, len = this.changeListeners.length; i < len; ++i)
this.changeListeners[i].call(this);
}

addChangeListener(listener) {
this.changeListeners.push(listener);
}

removeChangeListener(listener) {
this.changeListeners = this.changeListeners.filter(function (li) {
return li !== listener;
});
}

back() {
this.go(-1);
}
Expand All @@ -49,6 +40,15 @@ export class History {
this.go(1);
}

_createState(state) {
state = state || {};

if (!state.key)
state.key = createRandomKey();

return state;
}

}

export default History;
Loading

5 comments on commit 11cdab3

@agundermann
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjackson Is this state meant for data coming from the apps or for internal things like scroll position? I'm asking because one of the ideas of state.key was so that we don't have to store data in history.state and use sessionStorage.set(location.key, state) instead, which makes it possible to store state after pop transitions.

@mjackson
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@taurose it's for both.

The idea here is that you can give us some state and we'll store it and give it back to you on location.state when you need it. We reserve a few properties for the router, including key, scrollX, and scrollY. If you create an instance of HashHistory with a queryKey, we use a query string parameter + sessionStorage for persistence.

Does that work?

@mjackson
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also planning on putting a state prop on <Link> so that links can pass stuff into this state.

@agundermann
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would, but why not use sessionStorage for BrowserHistory as well? As I said, the drawback I see in using the built in history state is that it can't be changed after a transition, i.e. you couldn't provide an API for setting state in componentWillUnmount or transitionHook for pop transitions because replaceState would override the state of the next page. I guess that means for use cases like #1188 we'd have to call replaceState on componentDidUpdate, or use location.key and manage it ourselves.

Setting state with <Link> seems like a good idea. How is this related to #1273 ? I think we should differentiate between persistent state and state that's only used once.

@mjackson
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use sessionStorage for BrowserHistory as well?

Sure, we could totally do that. I actually had that same thought. In fact, @ryanflorence just suggested that we might be able to use componentWillUnmount to save the scroll state instead of hard-coding it into the DOM history implementations. That would definitely be an interesting exercise. You up for making a PR @taurose? :)

I think we should differentiate between persistent state and state that's only used once.

I think the only state we need to worry about is the persistent kind. If you have state you only use once, you need to figure out some other way to pass that around.

Please sign in to comment.