Skip to content

Commit 0e7a182

Browse files
seanadkinsonmjackson
authored andcommittedAug 13, 2014
[added] pluggable history implementations
closes #166
1 parent f51a7c6 commit 0e7a182

11 files changed

+318
-140
lines changed
 

‎Location.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('./modules/helpers/Location');

‎index.js

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ exports.Link = require('./Link');
44
exports.Redirect = require('./Redirect');
55
exports.Route = require('./Route');
66
exports.Routes = require('./Routes');
7+
exports.Location = require('./Location');
78
exports.goBack = require('./goBack');
89
exports.replaceWith = require('./replaceWith');
910
exports.transitionTo = require('./transitionTo');

‎modules/components/Routes.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var mergeProperties = require('../helpers/mergeProperties');
55
var goBack = require('../helpers/goBack');
66
var replaceWith = require('../helpers/replaceWith');
77
var transitionTo = require('../helpers/transitionTo');
8+
var Location = require('../helpers/Location');
89
var Route = require('../components/Route');
910
var Path = require('../helpers/Path');
1011
var ActiveStore = require('../stores/ActiveStore');
@@ -53,8 +54,15 @@ var Routes = React.createClass({
5354
},
5455

5556
propTypes: {
56-
location: React.PropTypes.oneOf([ 'hash', 'history' ]).isRequired,
57-
preserveScrollPosition: React.PropTypes.bool
57+
preserveScrollPosition: React.PropTypes.bool,
58+
location: function(props, propName, componentName) {
59+
var location = props[propName];
60+
if (!Location[location]) {
61+
return new Error('No matching location: "' + location +
62+
'". Must be one of: ' + Object.keys(Location) +
63+
'. See: ' + componentName);
64+
}
65+
}
5866
},
5967

6068
getDefaultProps: function () {

‎modules/helpers/DisabledLocation.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
var getWindowPath = require('./getWindowPath');
2+
3+
/**
4+
* Location handler that doesn't actually do any location handling. Instead, requests
5+
* are sent to the server as normal.
6+
*/
7+
var DisabledLocation = {
8+
9+
type: 'disabled',
10+
11+
init: function() { },
12+
13+
destroy: function() { },
14+
15+
getCurrentPath: function() {
16+
return getWindowPath();
17+
},
18+
19+
push: function(path) {
20+
window.location = path;
21+
},
22+
23+
replace: function(path) {
24+
window.location.replace(path);
25+
},
26+
27+
back: function() {
28+
window.history.back();
29+
}
30+
31+
};
32+
33+
module.exports = DisabledLocation;
34+

‎modules/helpers/HashLocation.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
var getWindowPath = require('./getWindowPath');
2+
3+
function getWindowChangeEvent() {
4+
return window.addEventListener ? 'hashchange' : 'onhashchange';
5+
}
6+
7+
/**
8+
* Location handler which uses the `window.location.hash` to push and replace URLs
9+
*/
10+
var HashLocation = {
11+
12+
type: 'hash',
13+
onChange: null,
14+
15+
init: function(onChange) {
16+
var changeEvent = getWindowChangeEvent();
17+
18+
if (window.location.hash === '') {
19+
this.replace('/');
20+
}
21+
22+
if (window.addEventListener) {
23+
window.addEventListener(changeEvent, onChange, false);
24+
} else {
25+
window.attachEvent(changeEvent, onChange);
26+
}
27+
28+
this.onChange = onChange;
29+
onChange();
30+
},
31+
32+
destroy: function() {
33+
var changeEvent = getWindowChangeEvent();
34+
if (window.removeEventListener) {
35+
window.removeEventListener(changeEvent, this.onChange, false);
36+
} else {
37+
window.detachEvent(changeEvent, this.onChange);
38+
}
39+
},
40+
41+
getCurrentPath: function() {
42+
return window.location.hash.substr(1);
43+
},
44+
45+
push: function(path) {
46+
window.location.hash = path;
47+
},
48+
49+
replace: function(path) {
50+
window.location.replace(getWindowPath() + '#' + path);
51+
},
52+
53+
back: function() {
54+
window.history.back();
55+
}
56+
57+
};
58+
59+
module.exports = HashLocation;
60+

‎modules/helpers/HistoryLocation.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
var getWindowPath = require('./getWindowPath');
2+
3+
/**
4+
* Location handler which uses the HTML5 History API to push and replace URLs
5+
*/
6+
var HistoryLocation = {
7+
8+
type: 'history',
9+
onChange: null,
10+
11+
init: function(onChange) {
12+
if (window.addEventListener) {
13+
window.addEventListener('popstate', onChange, false);
14+
} else {
15+
window.attachEvent('popstate', onChange);
16+
}
17+
this.onChange = onChange;
18+
onChange();
19+
},
20+
21+
destroy: function() {
22+
if (window.removeEventListener) {
23+
window.removeEventListener('popstate', this.onChange, false);
24+
} else {
25+
window.detachEvent('popstate', this.onChange);
26+
}
27+
},
28+
29+
getCurrentPath: function() {
30+
return getWindowPath();
31+
},
32+
33+
push: function(path) {
34+
window.history.pushState({ path: path }, '', path);
35+
this.onChange();
36+
},
37+
38+
replace: function(path) {
39+
window.history.replaceState({ path: path }, '', path);
40+
this.onChange();
41+
},
42+
43+
back: function() {
44+
window.history.back();
45+
}
46+
47+
};
48+
49+
module.exports = HistoryLocation;
50+

‎modules/helpers/Location.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Map of location type to handler.
3+
* @see Routes#location
4+
*/
5+
module.exports = {
6+
hash: require('./HashLocation'),
7+
history: require('./HistoryLocation'),
8+
disabled: require('./DisabledLocation'),
9+
memory: require('./MemoryLocation')
10+
};

‎modules/helpers/MemoryLocation.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
var invariant = require('react/lib/invariant');
2+
3+
var _lastPath;
4+
var _currentPath = '/';
5+
6+
/**
7+
* Fake location handler that can be used outside the scope of the browser. It
8+
* tracks the current and previous path, as given to #push() and #replace().
9+
*/
10+
var MemoryLocation = {
11+
12+
type: 'memory',
13+
onChange: null,
14+
15+
init: function(onChange) {
16+
this.onChange = onChange;
17+
},
18+
19+
destroy: function() {
20+
this.onChange = null;
21+
_lastPath = null;
22+
_currentPath = '/';
23+
},
24+
25+
getCurrentPath: function() {
26+
return _currentPath;
27+
},
28+
29+
push: function(path) {
30+
_lastPath = _currentPath;
31+
_currentPath = path;
32+
this.onChange();
33+
},
34+
35+
replace: function(path) {
36+
_currentPath = path;
37+
this.onChange();
38+
},
39+
40+
back: function() {
41+
invariant(
42+
_lastPath,
43+
'You cannot make the URL store go back more than once when it does not use the DOM'
44+
);
45+
46+
_currentPath = _lastPath;
47+
_lastPath = null;
48+
this.onChange();
49+
}
50+
};
51+
52+
module.exports = MemoryLocation;
53+

‎modules/helpers/getWindowPath.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Returns the current URL path from `window.location`, including query string
3+
*/
4+
function getWindowPath() {
5+
return window.location.pathname + window.location.search;
6+
}
7+
8+
module.exports = getWindowPath;
9+

‎modules/stores/URLStore.js

+16-84
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,14 @@
11
var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
22
var invariant = require('react/lib/invariant');
33
var warning = require('react/lib/warning');
4-
5-
var _location;
6-
var _currentPath = '/';
7-
var _lastPath = null;
8-
9-
function getWindowChangeEvent(location) {
10-
if (location === 'history')
11-
return 'popstate';
12-
13-
return window.addEventListener ? 'hashchange' : 'onhashchange';
14-
}
15-
16-
function getWindowPath() {
17-
return window.location.pathname + window.location.search;
18-
}
4+
var Location = require('../helpers/Location');
195

206
var EventEmitter = require('event-emitter');
217
var _events = EventEmitter();
228

9+
var _location;
10+
var _locationHandler;
11+
2312
function notifyChange() {
2413
_events.emit('change');
2514
}
@@ -56,13 +45,7 @@ var URLStore = {
5645
* Returns the value of the current URL path.
5746
*/
5847
getCurrentPath: function () {
59-
if (_location === 'history' || _location === 'disabledHistory')
60-
return getWindowPath();
61-
62-
if (_location === 'hash')
63-
return window.location.hash.substr(1);
64-
65-
return _currentPath;
48+
return _locationHandler.getCurrentPath();
6649
},
6750

6851
/**
@@ -72,55 +55,22 @@ var URLStore = {
7255
if (path === this.getCurrentPath())
7356
return;
7457

75-
if (_location === 'disabledHistory')
76-
return window.location = path;
77-
78-
if (_location === 'history') {
79-
window.history.pushState({ path: path }, '', path);
80-
notifyChange();
81-
} else if (_location === 'hash') {
82-
window.location.hash = path;
83-
} else {
84-
_lastPath = _currentPath;
85-
_currentPath = path;
86-
notifyChange();
87-
}
58+
_locationHandler.push(path);
8859
},
8960

9061
/**
9162
* Replaces the current URL path with the given path without adding an entry
9263
* to the browser's history.
9364
*/
9465
replace: function (path) {
95-
if (_location === 'disabledHistory') {
96-
window.location.replace(path);
97-
} else if (_location === 'history') {
98-
window.history.replaceState({ path: path }, '', path);
99-
notifyChange();
100-
} else if (_location === 'hash') {
101-
window.location.replace(getWindowPath() + '#' + path);
102-
} else {
103-
_currentPath = path;
104-
notifyChange();
105-
}
66+
_locationHandler.replace(path);
10667
},
10768

10869
/**
10970
* Reverts the URL to whatever it was before the last update.
11071
*/
11172
back: function () {
112-
if (_location != null) {
113-
window.history.back();
114-
} else {
115-
invariant(
116-
_lastPath,
117-
'You cannot make the URL store go back more than once when it does not use the DOM'
118-
);
119-
120-
_currentPath = _lastPath;
121-
_lastPath = null;
122-
notifyChange();
123-
}
73+
_locationHandler.back();
12474
},
12575

12676
/**
@@ -151,30 +101,19 @@ var URLStore = {
151101
}
152102

153103
if (location === 'history' && !supportsHistory()) {
154-
_location = 'disabledHistory';
155-
return;
104+
location = 'disabled';
156105
}
157106

158-
var changeEvent = getWindowChangeEvent(location);
107+
_location = location;
108+
_locationHandler = Location[location];
159109

160110
invariant(
161-
changeEvent || location === 'disabledHistory',
111+
_locationHandler,
162112
'The URL store location "' + location + '" is not valid. ' +
163-
'It must be either "hash" or "history"'
113+
'It must be any of: ' + Object.keys(Location)
164114
);
165115

166-
_location = location;
167-
168-
if (location === 'hash' && window.location.hash === '')
169-
URLStore.replace('/');
170-
171-
if (window.addEventListener) {
172-
window.addEventListener(changeEvent, notifyChange, false);
173-
} else {
174-
window.attachEvent(changeEvent, notifyChange);
175-
}
176-
177-
notifyChange();
116+
_locationHandler.init(notifyChange);
178117
},
179118

180119
/**
@@ -184,16 +123,9 @@ var URLStore = {
184123
if (_location == null)
185124
return;
186125

187-
var changeEvent = getWindowChangeEvent(_location);
188-
189-
if (window.removeEventListener) {
190-
window.removeEventListener(changeEvent, notifyChange, false);
191-
} else {
192-
window.detachEvent(changeEvent, notifyChange);
193-
}
194-
126+
_locationHandler.destroy();
195127
_location = null;
196-
_currentPath = '/';
128+
_locationHandler = null;
197129
}
198130

199131
};

‎specs/URLStore.spec.js

+74-54
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,101 @@
11
require('./helper');
22
var URLStore = require('../modules/stores/URLStore');
33

4-
describe('when a new path is pushed to the URL', function () {
5-
beforeEach(function () {
6-
URLStore.push('/a/b/c');
4+
describe('URLStore', function() {
5+
6+
beforeEach(function() {
7+
URLStore.setup("hash");
78
});
89

9-
afterEach(function () {
10+
afterEach(function() {
1011
URLStore.teardown();
1112
});
1213

13-
it('has the correct path', function () {
14-
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
15-
});
16-
});
14+
describe('when a new path is pushed to the URL', function() {
15+
beforeEach(function() {
16+
URLStore.push('/a/b/c');
17+
});
1718

18-
describe('when a new path is used to replace the URL', function () {
19-
beforeEach(function () {
20-
URLStore.replace('/a/b/c');
19+
it('has the correct path', function() {
20+
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
21+
});
2122
});
2223

23-
afterEach(function () {
24-
URLStore.teardown();
25-
});
24+
describe('when a new path is used to replace the URL', function() {
25+
beforeEach(function() {
26+
URLStore.replace('/a/b/c');
27+
});
2628

27-
it('has the correct path', function () {
28-
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
29+
it('has the correct path', function() {
30+
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
31+
});
2932
});
30-
});
3133

32-
describe('when going back in history', function () {
33-
afterEach(function () {
34-
URLStore.teardown();
34+
describe('when going back in history', function() {
35+
it('has the correct path', function() {
36+
URLStore.push('/a/b/c');
37+
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
38+
39+
URLStore.push('/d/e/f');
40+
expect(URLStore.getCurrentPath()).toEqual('/d/e/f');
41+
42+
URLStore.back();
43+
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
44+
});
3545
});
3646

37-
it('has the correct path', function () {
38-
URLStore.push('/a/b/c');
39-
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
47+
describe('when navigating back to the root', function() {
48+
beforeEach(function() {
49+
URLStore.teardown();
4050

41-
URLStore.push('/d/e/f');
42-
expect(URLStore.getCurrentPath()).toEqual('/d/e/f');
51+
// simulating that the browser opens a page with #/dashboard
52+
window.location.hash = '/dashboard';
53+
URLStore.setup('hash');
54+
});
4355

44-
URLStore.back();
45-
expect(URLStore.getCurrentPath()).toEqual('/a/b/c');
56+
it('should have the correct path', function() {
57+
URLStore.push('/');
58+
expect(window.location.hash).toEqual('#/');
59+
});
4660
});
4761

48-
it('should not go back before recorded history', function () {
49-
var error = false;
50-
try {
51-
URLStore.back();
52-
} catch (e) {
53-
error = true;
54-
}
62+
describe('when using history location handler', function() {
63+
itShouldManagePathsForLocation('history');
64+
});
5565

56-
expect(error).toEqual(true);
66+
describe('when using memory location handler', function() {
67+
itShouldManagePathsForLocation('memory');
5768
});
58-
});
5969

60-
describe('when navigating back to the root', function() {
61-
beforeEach(function () {
62-
// not all tests are constructing and tearing down the URLStore.
63-
// Let's set it up correctly once and then tear it down to ensure that all
64-
// variables in the URLStore module are reset.
65-
URLStore.setup('hash');
66-
URLStore.teardown();
70+
function itShouldManagePathsForLocation(location) {
71+
var origPath;
6772

68-
// simulating that the browser opens a page with #/dashboard
69-
window.location.hash = '/dashboard';
70-
URLStore.setup('hash');
71-
});
73+
beforeEach(function() {
74+
URLStore.teardown();
75+
URLStore.setup(location);
76+
origPath = URLStore.getCurrentPath();
77+
});
7278

73-
afterEach(function () {
74-
URLStore.teardown();
75-
});
79+
afterEach(function() {
80+
URLStore.push(origPath);
81+
expect(URLStore.getCurrentPath()).toEqual(origPath);
82+
});
83+
84+
it('should manage the path correctly', function() {
85+
URLStore.push('/test');
86+
expect(URLStore.getCurrentPath()).toEqual('/test');
87+
88+
URLStore.push('/test/123');
89+
expect(URLStore.getCurrentPath()).toEqual('/test/123');
90+
91+
URLStore.replace('/test/replaced');
92+
expect(URLStore.getCurrentPath()).toEqual('/test/replaced');
93+
94+
URLStore.back();
95+
expect(URLStore.getCurrentPath()).toEqual('/test');
96+
97+
});
98+
}
7699

77-
it('should have the correct path', function () {
78-
URLStore.push('/');
79-
expect(window.location.hash).toEqual('#/');
80-
});
81100
});
101+

0 commit comments

Comments
 (0)
Please sign in to comment.