Permalink
Browse files

feat($location): add $locatonChange[begin|completed] event

This allows location change cancelation
  • Loading branch information...
mhevery committed May 22, 2012
1 parent 8aa18f0 commit 92a2e1807657c69e1372106b0727675a30f4cbd7
Showing with 246 additions and 62 deletions.
  1. +6 −1 src/bootstrap/bootstrap-prettify.js
  2. +67 −43 src/ng/location.js
  3. +2 −3 src/ng/route.js
  4. +144 −13 test/ng/locationSpec.js
  5. +27 −2 test/ng/routeSpec.js
@@ -190,7 +190,12 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
$provide.value('$anchorScroll', angular.noop);
$provide.value('$browser', $browser);
$provide.provider('$location', function() {
this.$get = function() { return $location; };
this.$get = ['$rootScope', function($rootScope) {
docsRootScope.$on('$locationChangeSuccess', function(event, oldUrl, newUrl) {
$rootScope.$broadcast('$locationChangeSuccess', oldUrl, newUrl);
});
return $location;
}];
this.html5Mode = angular.noop;
});
$provide.decorator('$defer', ['$rootScope', '$delegate', function($rootScope, $delegate) {
View
@@ -408,7 +408,10 @@ function locationGetterSetter(property, preprocess) {
* @requires $rootElement
*
* @description
* The $location service parses the URL in the browser address bar (based on the {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar.
* The $location service parses the URL in the browser address bar (based on the
* {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL
* available to your application. Changes to the URL in the address bar are reflected into
* $location service and changes to $location are reflected into the browser address bar.
*
* **The $location service:**
*
@@ -421,7 +424,8 @@ function locationGetterSetter(property, preprocess) {
* - Clicks on a link.
* - Represents the URL object as a set of methods (protocol, host, port, path, search, hash).
*
* For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular Services: Using $location}
* For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular
* Services: Using $location}
*/
/**
@@ -470,65 +474,73 @@ function $LocationProvider(){
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
function( $rootScope, $browser, $sniffer, $rootElement) {
var currentUrl,
var $location,
basePath = $browser.baseHref() || '/',
pathPrefix = pathPrefixFromBase(basePath),
initUrl = $browser.url();
initUrl = $browser.url(),
absUrlPrefix;
if (html5Mode) {
if ($sniffer.history) {
currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix);
$location = new LocationUrl(
convertToHtml5Url(initUrl, basePath, hashPrefix),
pathPrefix);
} else {
currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix),
hashPrefix);
$location = new LocationHashbangUrl(
convertToHashbangUrl(initUrl, basePath, hashPrefix),
hashPrefix);
}
} else {
$location = new LocationHashbangUrl(initUrl, hashPrefix);
}
// link rewriting
var u = currentUrl,
absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix;
// link rewriting
absUrlPrefix = composeProtocolHostPort(
$location.protocol(), $location.host(), $location.port()) + pathPrefix;
$rootElement.bind('click', function(event) {
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
$rootElement.bind('click', function(event) {
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
if (event.ctrlKey || event.metaKey || event.which == 2) return;
if (event.ctrlKey || event.metaKey || event.which == 2) return;
var elm = jqLite(event.target);
var elm = jqLite(event.target);
// traverse the DOM up to find first A tag
while (elm.length && lowercase(elm[0].nodeName) !== 'a') {
elm = elm.parent();
}
// traverse the DOM up to find first A tag
while (elm.length && lowercase(elm[0].nodeName) !== 'a') {
elm = elm.parent();
}
var absHref = elm.prop('href');
var absHref = elm.prop('href');
if (!absHref ||
elm.attr('target') ||
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
return;
}
if (!absHref ||
elm.attr('target') ||
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
return;
}
// update location with href without the prefix
$location.url(absHref.substr(absUrlPrefix.length));
$rootScope.$apply();
event.preventDefault();
// hack to work around FF6 bug 684208 when scenario runner clicks on links
window.angular['ff-684208-preventDefault'] = true;
});
// update location with href without the prefix
currentUrl.url(absHref.substr(absUrlPrefix.length));
$rootScope.$apply();
event.preventDefault();
// hack to work around FF6 bug 684208 when scenario runner clicks on links
window.angular['ff-684208-preventDefault'] = true;
});
} else {
currentUrl = new LocationHashbangUrl(initUrl, hashPrefix);
}
// rewrite hashbang url <> html5 url
if (currentUrl.absUrl() != initUrl) {
$browser.url(currentUrl.absUrl(), true);
if ($location.absUrl() != initUrl) {
$browser.url($location.absUrl(), true);
}
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl) {
if (currentUrl.absUrl() != newUrl) {
if ($location.absUrl() != newUrl) {
$rootScope.$evalAsync(function() {
currentUrl.$$parse(newUrl);
var oldUrl = $location.absUrl();
$location.$$parse(newUrl);
afterLocationChange(oldUrl);
});
if (!$rootScope.$$phase) $rootScope.$digest();
}
@@ -537,17 +549,29 @@ function $LocationProvider(){
// update browser
var changeCounter = 0;
$rootScope.$watch(function $locationWatch() {
if ($browser.url() != currentUrl.absUrl()) {
var oldUrl = $browser.url();
if (!changeCounter || oldUrl != $location.absUrl()) {
changeCounter++;
$rootScope.$evalAsync(function() {
$browser.url(currentUrl.absUrl(), currentUrl.$$replace);
currentUrl.$$replace = false;
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
defaultPrevented) {
$location.$$parse(oldUrl);
} else {
$browser.url($location.absUrl(), $location.$$replace);
$location.$$replace = false;
afterLocationChange(oldUrl);
}
});
}
return changeCounter;
});
return currentUrl;
return $location;
function afterLocationChange(oldUrl) {
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
}
}];
}
View
@@ -286,7 +286,6 @@ function $RouteProvider(){
*/
var matcher = switchRouteMatcher,
dirty = 0,
forceReload = false,
$route = {
routes: routes,
@@ -304,12 +303,12 @@ function $RouteProvider(){
* creates new scope, reinstantiates the controller.
*/
reload: function() {
dirty++;
forceReload = true;
$rootScope.$evalAsync(updateRoute);
}
};
$rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute);
$rootScope.$on('$locationChangeSuccess', updateRoute);
return $route;
View
@@ -791,19 +791,6 @@ describe('$location', function() {
});
it('should not rewrite when history disabled', function() {
configureService('#new', false);
inject(
initBrowser(),
initLocation(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
}
);
});
it('should not rewrite full url links do different domain', function() {
configureService('http://www.dot.abc/a?b=c', true);
inject(
@@ -982,4 +969,148 @@ describe('$location', function() {
});
}
});
describe('location cancellation', function() {
it('should fire $before/afterLocationChange event', inject(function($location, $browser, $rootScope, $log) {
expect($browser.url()).toEqual('http://server/');
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
});
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
$log.info('after', newUrl, oldUrl, $browser.url());
});
expect($location.url()).toEqual('');
$location.url('/somePath');
expect($location.url()).toEqual('/somePath');
expect($browser.url()).toEqual('http://server/');
expect($log.info.logs).toEqual([]);
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/somePath', 'http://server/', 'http://server/#/somePath']);
expect($location.url()).toEqual('/somePath');
expect($browser.url()).toEqual('http://server/#/somePath');
}));
it('should allow $locationChangeStart event cancellation', inject(function($location, $browser, $rootScope, $log) {
expect($browser.url()).toEqual('http://server/');
expect($location.url()).toEqual('');
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
event.preventDefault();
});
$rootScope.$on('$locationChangeCompleted', function(event, newUrl, oldUrl) {
throw Error('location should have been canceled');
});
expect($location.url()).toEqual('');
$location.url('/somePath');
expect($location.url()).toEqual('/somePath');
expect($browser.url()).toEqual('http://server/');
expect($log.info.logs).toEqual([]);
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs[1]).toBeUndefined();
expect($location.url()).toEqual('');
expect($browser.url()).toEqual('http://server/');
}));
it ('should fire $locationChangeCompleted event when change from browser location bar',
inject(function($log, $location, $browser, $rootScope) {
$rootScope.$apply(); // clear initial $locationChangeStart
expect($browser.url()).toEqual('http://server/');
expect($location.url()).toEqual('');
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
throw Error('there is no before when user enters URL directly to browser');
});
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
$log.info('after', newUrl, oldUrl);
});
$browser.url('http://server/#/somePath');
$browser.poll();
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/somePath', 'http://server/']);
})
);
it('should listen on click events on href and prevent browser default in hasbang mode', function() {
module(function() {
return function($rootElement, $compile, $rootScope) {
$rootElement.html('<a href="http://server/#/somePath">link</a>');
$compile($rootElement)($rootScope);
jqLite(document.body).append($rootElement);
}
});
inject(function($location, $rootScope, $browser, $rootElement) {
var log = '',
link = $rootElement.find('a');
$rootScope.$on('$locationChangeStart', function(event) {
event.preventDefault();
log += '$locationChangeStart';
});
$rootScope.$on('$locationChangeCompleted', function() {
throw new Error('after cancellation in hashbang mode');
});
browserTrigger(link, 'click');
expect(log).toEqual('$locationChangeStart');
expect($browser.url()).toEqual('http://server/');
dealoc($rootElement);
});
});
it('should listen on click events on href and prevent browser default in html5 mode', function() {
module(function($locationProvider) {
$locationProvider.html5Mode(true);
return function($rootElement, $compile, $rootScope) {
$rootElement.html('<a href="http://server/somePath">link</a>');
$compile($rootElement)($rootScope);
jqLite(document.body).append($rootElement);
}
});
inject(function($location, $rootScope, $browser, $rootElement) {
var log = '',
link = $rootElement.find('a');
$rootScope.$on('$locationChangeStart', function(event) {
event.preventDefault();
log += '$locationChangeStart';
});
$rootScope.$on('$locationChangeCompleted', function() {
throw new Error('after cancalation in html5 mode');
});
browserTrigger(link, 'click');
expect(log).toEqual('$locationChangeStart');
expect($browser.url()).toEqual('http://server/');
dealoc($rootElement);
});
});
});
});
Oops, something went wrong.

0 comments on commit 92a2e18

Please sign in to comment.