Permalink
Browse files

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

This allows location change cancelation
  • Loading branch information...
1 parent 8aa18f0 commit 92a2e1807657c69e1372106b0727675a30f4cbd7 @mhevery mhevery committed May 22, 2012
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;
@@ -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.