Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

feat($location): add support for state and title in pushState #3325

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions src/ng/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function Browser(window, document, $log, $sniffer) {
//////////////////////////////////////////////////////////////

var lastBrowserUrl = location.href,
lastBrowserState = history.state,
baseElement = document.find('base'),
newLocation = null;

Expand All @@ -143,21 +144,30 @@ function Browser(window, document, $log, $sniffer) {
* {@link ng.$location $location service} to change url.
*
* @param {string} url New url (when used as setter)
* @param {boolean=} replace Should new url replace current history record ?
* @param {boolean=} replace Should new url replace current history record?
* @param {object=} state object to use with pushState/replaceState
* @param {string=} title to use with pushState/replaceState
*/
self.url = function(url, replace) {
self.url = function(url, replace, state, title) {
// Android Browser BFCache causes location, history reference to become stale.
if (location !== window.location) location = window.location;
if (history !== window.history) history = window.history;

// setter
if (url) {
if (lastBrowserUrl == url) return;
if (lastBrowserUrl == url &&
// if pushState supported, check if state changed
((lastBrowserState == null && state == null) || equals(lastBrowserState, state))) {
return;
}
lastBrowserUrl = url;
lastBrowserState = copy(state);
if ($sniffer.history) {
if (replace) history.replaceState(null, '', url);
title = title || rawDocument.title;
state = state || null;
if (replace) history.replaceState(state, title, url);
else {
history.pushState(null, '', url);
history.pushState(state, title, url);
// Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
baseElement.attr('href', baseElement.attr('href'));
}
Expand All @@ -170,13 +180,12 @@ function Browser(window, document, $log, $sniffer) {
}
}
return self;
// getter
} else {
// - newLocation is a workaround for an IE7-9 issue with location.replace and location.href
// methods not updating location.href synchronously.
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
return newLocation || location.href.replace(/%27/g,"'");
}
// getter
// - newLocation is a workaround for an IE7-9 issue with location.replace and location.href
// methods not updating location.href synchronously.
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
return newLocation || location.href.replace(/%27/g,"'");
};

var urlChangeListeners = [],
Expand All @@ -188,7 +197,7 @@ function Browser(window, document, $log, $sniffer) {

lastBrowserUrl = self.url();
forEach(urlChangeListeners, function(listener) {
listener(self.url());
listener(self.url(), history.state, rawDocument.title);
});
}

Expand Down
69 changes: 58 additions & 11 deletions src/ng/location.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,17 @@ LocationHashbangInHtml5Url.prototype =
$$html5: false,

/**
* Has any change been replacing ?
* Has any change been replacing?
* @private
*/
$$replace: false,

/**
* Current History API state.
* @private
*/
$$state: null,

/**
* @ngdoc method
* @name $location#absUrl
Expand Down Expand Up @@ -328,6 +334,43 @@ LocationHashbangInHtml5Url.prototype =
return this;
},

/**
* @ngdoc method
* @name $location#pushState
*
* @description
* Invokes history.pushState. Changes url, state and title.
*
* @param {object=} state object for pushState
* @param {string=} title for pushState
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
* @return {object} $location
*/
pushState: function(state, title, url, replace) {
this.$$state = copy(state);
this.$$title = title;
this.$$replace = replace;
this.url(url, replace);
return this;
},

/**
* @ngdoc method
* @name $location#replaceState.
*
* @description
* Invokes history.replaceState. Changes url, state and title.
*
* @param {object=} state object for replaceState
* @param {string=} title for replaceState
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
* @return {object} $location
*/
replaceState: function(state, title, url) {
this.pushState(state, title, url, true);
return this;
},

/**
* @ngdoc method
* @name $location#protocol
Expand Down Expand Up @@ -642,7 +685,7 @@ function $LocationProvider(){
}

// update $location when $browser url changes
$browser.onUrlChange(function(newUrl) {
$browser.onUrlChange(function(newUrl, state, title) {
if ($location.absUrl() != newUrl) {
$rootScope.$evalAsync(function() {
var oldUrl = $location.absUrl();
Expand All @@ -651,9 +694,9 @@ function $LocationProvider(){
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
oldUrl).defaultPrevented) {
$location.$$parse(oldUrl);
$browser.url(oldUrl);
$browser.url($location.absUrl(), false, state, title);
} else {
afterLocationChange(oldUrl);
afterLocationChange(oldUrl, state, title);
}
});
if (!$rootScope.$$phase) $rootScope.$digest();
Expand All @@ -663,30 +706,34 @@ function $LocationProvider(){
// update browser
var changeCounter = 0;
$rootScope.$watch(function $locationWatch() {
var oldUrl = $browser.url();
var currentReplace = $location.$$replace;
var oldUrl = $browser.url(),
currentReplace = $location.$$replace,
currentState = $location.$$state,
currentTitle = $location.$$title;

if (!changeCounter || oldUrl != $location.absUrl()) {
changeCounter++;
$rootScope.$evalAsync(function() {
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl, currentState, currentTitle).
defaultPrevented) {
$location.$$parse(oldUrl);
} else {
$browser.url($location.absUrl(), currentReplace);
afterLocationChange(oldUrl);
$browser.url($location.absUrl(), currentReplace, currentState, currentTitle);
afterLocationChange(oldUrl, currentState, currentTitle);
}
});
}
$location.$$replace = false;
delete $location.$$state;
delete $location.$$title;

return changeCounter;
});

return $location;

function afterLocationChange(oldUrl) {
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
function afterLocationChange(oldUrl, state, title) {
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, state, title);
}
}];
}
78 changes: 65 additions & 13 deletions test/ng/browserSpecs.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use strict';

var sniffer = {};

function MockWindow() {
var events = {};
var timeouts = this.timeouts = [];
var mockWindow = this;

this.setTimeout = function(fn) {
return timeouts.push(fn) - 1;
Expand Down Expand Up @@ -37,19 +40,32 @@ function MockWindow() {

this.location = {
href: 'http://server',
replace: noop
replace: function(url) {
this.href = url;
},
};

this.document = {
title: ''
};

this.history = {
replaceState: noop,
pushState: noop
state: null,
pushState: function(state, title, url) {
mockWindow.location.href = url;
mockWindow.history.state = copy(state);
mockWindow.document.title = title;
},
replaceState: function() {
this.pushState.apply(this, arguments);
}
};
}

function MockDocument() {
var self = this;

this[0] = window.document
this[0] = window.document;
this.basePath = '/';

this.find = function(name) {
Expand All @@ -71,7 +87,7 @@ function MockDocument() {

describe('browser', function() {

var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts, sniffer;
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts;

beforeEach(function() {
scripts = [];
Expand All @@ -80,9 +96,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)); },
Expand Down Expand Up @@ -470,6 +483,45 @@ describe('browser', function() {
});
});

describe('pushState & replaceState state & title handling', 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 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 not 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({prop: 'val1'});
});

it('should not 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: 'val3'});
});
});

describe('urlChange', function() {
var callback;

Expand All @@ -491,7 +543,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();
Expand All @@ -505,7 +557,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();
Expand All @@ -519,7 +571,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();
Expand All @@ -533,7 +585,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();

Expand All @@ -554,7 +606,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');
Expand Down
2 changes: 1 addition & 1 deletion test/ng/locationSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,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, undefined]);
expect($location.$$replace).toBe(false);
}));

Expand Down
2 changes: 1 addition & 1 deletion test/ngRoute/routeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,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, undefined]);
});
});
});
Expand Down