mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-23 19:40:56 +08:00
feat($location): add support for History API state handling
Adds $location state method allowing to get/set a History API state via pushState & replaceState methods. Note that: - Angular treats states undefined and null as the same; trying to change one to the other without touching the URL won't do anything. This is necessary to prevent infinite digest loops when setting the URL to itself in IE<10 in the HTML5 hash fallback mode. - The state() method is not compatible with browsers not supporting the HTML5 History API, e.g. IE 9 or Android < 4.0. Closes #9027
This commit is contained in:
committed by
Igor Minar
parent
8ee1ba4b94
commit
6fd36deed9
@@ -1,8 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
var historyEntriesLength;
|
||||
var sniffer = {};
|
||||
|
||||
function MockWindow() {
|
||||
var events = {};
|
||||
var timeouts = this.timeouts = [];
|
||||
var locationHref = 'http://server/';
|
||||
var mockWindow = this;
|
||||
|
||||
historyEntriesLength = 1;
|
||||
|
||||
this.setTimeout = function(fn) {
|
||||
return timeouts.push(fn) - 1;
|
||||
@@ -36,13 +43,30 @@ function MockWindow() {
|
||||
};
|
||||
|
||||
this.location = {
|
||||
href: 'http://server/',
|
||||
replace: noop
|
||||
get href() {
|
||||
return locationHref;
|
||||
},
|
||||
set href(value) {
|
||||
locationHref = value;
|
||||
mockWindow.history.state = null;
|
||||
historyEntriesLength++;
|
||||
},
|
||||
replace: function(url) {
|
||||
locationHref = url;
|
||||
mockWindow.history.state = null;
|
||||
},
|
||||
};
|
||||
|
||||
this.history = {
|
||||
replaceState: noop,
|
||||
pushState: noop
|
||||
state: null,
|
||||
pushState: function() {
|
||||
this.replaceState.apply(this, arguments);
|
||||
historyEntriesLength++;
|
||||
},
|
||||
replaceState: function(state, title, url) {
|
||||
locationHref = url;
|
||||
mockWindow.history.state = copy(state);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,7 +95,7 @@ function MockDocument() {
|
||||
|
||||
describe('browser', function() {
|
||||
/* global Browser: false */
|
||||
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts, sniffer;
|
||||
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts;
|
||||
|
||||
beforeEach(function() {
|
||||
scripts = [];
|
||||
@@ -80,9 +104,6 @@ describe('browser', function() {
|
||||
fakeWindow = new MockWindow();
|
||||
fakeDocument = new MockDocument();
|
||||
|
||||
var fakeBody = [{appendChild: function(node){scripts.push(node);},
|
||||
removeChild: function(node){removedScripts.push(node);}}];
|
||||
|
||||
logs = {log:[], warn:[], info:[], error:[]};
|
||||
|
||||
var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
|
||||
@@ -93,6 +114,32 @@ describe('browser', function() {
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
});
|
||||
|
||||
describe('MockBrowser historyEntriesLength', function() {
|
||||
it('should increment historyEntriesLength when setting location.href', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.location.href = '/foo';
|
||||
expect(historyEntriesLength).toBe(2);
|
||||
});
|
||||
|
||||
it('should not increment historyEntriesLength when using location.replace', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.location.replace('/foo');
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment historyEntriesLength when using history.pushState', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.history.pushState({a: 2}, 'foo', '/bar');
|
||||
expect(historyEntriesLength).toBe(2);
|
||||
});
|
||||
|
||||
it('should not increment historyEntriesLength when using history.replaceState', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.history.replaceState({a: 2}, 'foo', '/bar');
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain cookie cruncher', function() {
|
||||
expect(browser.cookies).toBeDefined();
|
||||
});
|
||||
@@ -546,6 +593,68 @@ describe('browser', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('url (when state passed)', function() {
|
||||
var currentHref;
|
||||
|
||||
beforeEach(function() {
|
||||
sniffer = {history: true, hashchange: true};
|
||||
currentHref = fakeWindow.location.href;
|
||||
});
|
||||
|
||||
it('should change state', function() {
|
||||
browser.url(currentHref + '/something', false, {prop: 'val1'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
|
||||
});
|
||||
|
||||
it('should allow to set falsy states (except `undefined`)', function() {
|
||||
fakeWindow.history.state = {prop: 'val1'};
|
||||
|
||||
browser.url(currentHref, false, null);
|
||||
expect(fakeWindow.history.state).toBe(null);
|
||||
|
||||
browser.url(currentHref, false, false);
|
||||
expect(fakeWindow.history.state).toBe(false);
|
||||
|
||||
browser.url(currentHref, false, '');
|
||||
expect(fakeWindow.history.state).toBe('');
|
||||
|
||||
browser.url(currentHref, false, 0);
|
||||
expect(fakeWindow.history.state).toBe(0);
|
||||
});
|
||||
|
||||
it('should treat `undefined` state as `null`', function() {
|
||||
fakeWindow.history.state = {prop: 'val1'};
|
||||
|
||||
browser.url(currentHref, false, undefined);
|
||||
expect(fakeWindow.history.state).toBe(null);
|
||||
});
|
||||
|
||||
it('should do pushState with the same URL and a different state', function() {
|
||||
browser.url(currentHref, false, {prop: 'val1'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
|
||||
|
||||
browser.url(currentHref, false, null);
|
||||
expect(fakeWindow.history.state).toBe(null);
|
||||
|
||||
browser.url(currentHref, false, {prop: 'val2'});
|
||||
browser.url(currentHref, false, {prop: 'val3'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val3'});
|
||||
});
|
||||
|
||||
it('should do pushState with the same URL and null state', function() {
|
||||
fakeWindow.history.state = {prop: 'val1'};
|
||||
browser.url(currentHref, false, null);
|
||||
expect(fakeWindow.history.state).toEqual(null);
|
||||
});
|
||||
|
||||
it('should do pushState with the same URL and the same non-null state', function() {
|
||||
browser.url(currentHref, false, {prop: 'val2'});
|
||||
fakeWindow.history.state = {prop: 'val3'};
|
||||
browser.url(currentHref, false, {prop: 'val2'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val2'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlChange', function() {
|
||||
var callback;
|
||||
|
||||
@@ -567,7 +676,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/new';
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new', null);
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
fakeWindow.setTimeout.flush();
|
||||
@@ -581,7 +690,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/new';
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new', null);
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
fakeWindow.setTimeout.flush();
|
||||
@@ -595,7 +704,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/new';
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new', null);
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
fakeWindow.setTimeout.flush();
|
||||
@@ -609,7 +718,7 @@ describe('browser', function() {
|
||||
|
||||
fakeWindow.location.href = 'http://server.new';
|
||||
fakeWindow.setTimeout.flush();
|
||||
expect(callback).toHaveBeenCalledWith('http://server.new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server.new', null);
|
||||
|
||||
callback.reset();
|
||||
|
||||
@@ -630,7 +739,7 @@ describe('browser', function() {
|
||||
|
||||
fakeWindow.location.href = 'http://server/#new';
|
||||
fakeWindow.setTimeout.flush();
|
||||
expect(callback).toHaveBeenCalledWith('http://server/#new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/#new', null);
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
fakeWindow.fire('hashchange');
|
||||
@@ -801,7 +910,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/some/deep/path';
|
||||
var changeUrlCount = 0;
|
||||
var _url = browser.url;
|
||||
browser.url = function(newUrl, replace) {
|
||||
browser.url = function(newUrl, replace, state) {
|
||||
if (newUrl) {
|
||||
changeUrlCount++;
|
||||
}
|
||||
@@ -817,7 +926,7 @@ describe('browser', function() {
|
||||
// from $location for rewriting the initial url into a hash url
|
||||
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', true);
|
||||
// from the initial call to the watch in $location for watching $location
|
||||
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', false);
|
||||
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', false, null);
|
||||
expect(changeUrlCount).toBe(2);
|
||||
});
|
||||
|
||||
@@ -836,11 +945,13 @@ describe('browser', function() {
|
||||
var current = fakeWindow.location.href;
|
||||
var newUrl = 'notyet';
|
||||
sniffer.history = false;
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
browser.url(newUrl, true);
|
||||
expect(browser.url()).toBe(newUrl);
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
$rootScope.$digest();
|
||||
expect(browser.url()).toBe(newUrl);
|
||||
expect(fakeWindow.location.href).toBe(current);
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -388,6 +388,36 @@ describe('$location', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('state', function () {
|
||||
it('should set $$state and return itself', function() {
|
||||
expect(url.$$state).toEqual(null);
|
||||
|
||||
var returned = url.state({a: 2});
|
||||
expect(url.$$state).toEqual({a: 2});
|
||||
expect(returned).toBe(url);
|
||||
});
|
||||
|
||||
it('should set state', function () {
|
||||
url.state({a: 2});
|
||||
expect(url.state()).toEqual({a: 2});
|
||||
});
|
||||
|
||||
it('should allow to set both URL and state', function() {
|
||||
url.url('/foo').state({a: 2});
|
||||
expect(url.url()).toEqual('/foo');
|
||||
expect(url.state()).toEqual({a: 2});
|
||||
});
|
||||
|
||||
it('should allow to mix state and various URL functions', function() {
|
||||
url.path('/foo').hash('abcd').state({a: 2}).search('bar', 'baz');
|
||||
expect(url.path()).toEqual('/foo');
|
||||
expect(url.state()).toEqual({a: 2});
|
||||
expect(url.search() && url.search().bar).toBe('baz');
|
||||
expect(url.hash()).toEqual('abcd');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('encoding', function() {
|
||||
|
||||
it('should encode special characters', function() {
|
||||
@@ -684,7 +714,7 @@ describe('$location', function() {
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true, null]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
}));
|
||||
|
||||
@@ -721,6 +751,122 @@ describe('$location', function() {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('wiring in html5 mode', function() {
|
||||
|
||||
beforeEach(initService({html5Mode: true, supportHistory: true}));
|
||||
beforeEach(inject(initBrowser({url:'http://new.com/a/b/', basePath: '/a/b/'})));
|
||||
|
||||
it('should initialize state to $browser.state()', inject(function($browser) {
|
||||
$browser.$$state = {a: 2};
|
||||
inject(function($location) {
|
||||
expect($location.state()).toEqual({a: 2});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should update $location when browser state changes', inject(function($browser, $location) {
|
||||
$browser.url('http://new.com/a/b/', false, {b: 3});
|
||||
$browser.poll();
|
||||
expect($location.state()).toEqual({b: 3});
|
||||
}));
|
||||
|
||||
it('should replace browser url & state when replace() was called at least once',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/n/url').state({a: 2}).replace();
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/n/url', true, {a: 2}]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
expect($location.$$state).toEqual({a: 2});
|
||||
}));
|
||||
|
||||
it('should use only the most recent url & state definition',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/n/url').state({a: 2}).replace().state({b: 3}).path('/o/url');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/o/url', true, {b: 3}]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
expect($location.$$state).toEqual({b: 3});
|
||||
}));
|
||||
|
||||
it('should allow to set state without touching the URL',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.state({a: 2}).replace().state({b: 3});
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/', true, {b: 3}]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
expect($location.$$state).toEqual({b: 3});
|
||||
}));
|
||||
|
||||
it('should always reset replace flag after running watch', inject(function($rootScope, $location) {
|
||||
// init watches
|
||||
$location.url('/initUrl').state({a: 2});
|
||||
$rootScope.$apply();
|
||||
|
||||
// changes url & state but resets them before digest
|
||||
$location.url('/newUrl').state({a: 2}).replace().state({b: 3}).url('/initUrl');
|
||||
$rootScope.$apply();
|
||||
expect($location.$$replace).toBe(false);
|
||||
|
||||
// set the url to the old value
|
||||
$location.url('/newUrl').state({a: 2}).replace();
|
||||
$rootScope.$apply();
|
||||
expect($location.$$replace).toBe(false);
|
||||
|
||||
// doesn't even change url only calls replace()
|
||||
$location.replace();
|
||||
$rootScope.$apply();
|
||||
expect($location.$$replace).toBe(false);
|
||||
}));
|
||||
|
||||
it('should allow to modify state only before digest',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var o = {a: 2};
|
||||
$location.state(o);
|
||||
o.a = 3;
|
||||
$rootScope.$apply();
|
||||
expect($browser.state()).toEqual({a: 3});
|
||||
|
||||
o.a = 4;
|
||||
$rootScope.$apply();
|
||||
expect($browser.state()).toEqual({a: 3});
|
||||
}));
|
||||
|
||||
it('should make $location.state() referencially identical with $browser.state() after digest',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
$location.state({a: 2});
|
||||
$rootScope.$apply();
|
||||
expect($location.state()).toBe($browser.state());
|
||||
}));
|
||||
|
||||
it('should allow to query the state after digest',
|
||||
inject(function($rootScope, $location) {
|
||||
$location.url('/foo').state({a: 2});
|
||||
$rootScope.$apply();
|
||||
expect($location.state()).toEqual({a: 2});
|
||||
}));
|
||||
|
||||
it('should reset the state on .url() after digest',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
$location.url('/foo').state({a: 2});
|
||||
$rootScope.$apply();
|
||||
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.url('/bar');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/bar', false, null]);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
// html5 history is disabled
|
||||
describe('disabled history', function() {
|
||||
@@ -1771,9 +1917,21 @@ describe('$location', function() {
|
||||
"$location in HTML5 mode requires a <base> tag to be present!");
|
||||
});
|
||||
});
|
||||
|
||||
it('should support state', function() {
|
||||
expect(location.state({a: 2}).state()).toEqual({a: 2});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function throwOnState(location) {
|
||||
expect(function () {
|
||||
location.state({a: 2});
|
||||
}).toThrowMinErr('$location', 'nostate', 'History API state support is available only ' +
|
||||
'in HTML5 mode and only in browsers supporting HTML5 History API'
|
||||
);
|
||||
}
|
||||
|
||||
describe('LocationHashbangUrl', function() {
|
||||
var location;
|
||||
|
||||
@@ -1828,6 +1986,10 @@ describe('$location', function() {
|
||||
expect(location.url()).toBe('/http://example.com/');
|
||||
expect(location.absUrl()).toBe('http://server/pre/index.html#/http://example.com/');
|
||||
});
|
||||
|
||||
it('should throw on url(urlString, stateObject)', function () {
|
||||
throwOnState(location);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1854,5 +2016,9 @@ describe('$location', function() {
|
||||
// Note: relies on the previous state!
|
||||
expect(parseLinkAndReturn(locationIndex, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/index.html#!/otherPath#test');
|
||||
});
|
||||
|
||||
it('should throw on url(urlString, stateObject)', function () {
|
||||
throwOnState(location);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +79,8 @@ describe('$$rAF', function() {
|
||||
//we need to create our own injector to work around the ngMock overrides
|
||||
var injector = createInjector(['ng', function($provide) {
|
||||
$provide.value('$window', {
|
||||
location : window.location,
|
||||
location: window.location,
|
||||
history: window.history,
|
||||
webkitRequestAnimationFrame: jasmine.createSpy('$window.webkitRequestAnimationFrame'),
|
||||
webkitCancelRequestAnimationFrame: jasmine.createSpy('$window.webkitCancelRequestAnimationFrame')
|
||||
});
|
||||
|
||||
@@ -883,7 +883,7 @@ describe('$route', function() {
|
||||
|
||||
expect($location.path()).toEqual('/bar/id3');
|
||||
expect($browserUrl.mostRecentCall.args)
|
||||
.toEqual(['http://server/#/bar/id3?extra=eId', true]);
|
||||
.toEqual(['http://server/#/bar/id3?extra=eId', true, null]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user