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

This allows location change cancelation
This commit is contained in:
Misko Hevery 2012-05-22 16:45:56 -07:00
parent 8aa18f0ad0
commit 92a2e18076
5 changed files with 252 additions and 68 deletions

View file

@ -190,7 +190,12 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
$provide.value('$anchorScroll', angular.noop); $provide.value('$anchorScroll', angular.noop);
$provide.value('$browser', $browser); $provide.value('$browser', $browser);
$provide.provider('$location', function() { $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; this.html5Mode = angular.noop;
}); });
$provide.decorator('$defer', ['$rootScope', '$delegate', function($rootScope, $delegate) { $provide.decorator('$defer', ['$rootScope', '$delegate', function($rootScope, $delegate) {

View file

@ -408,7 +408,10 @@ function locationGetterSetter(property, preprocess) {
* @requires $rootElement * @requires $rootElement
* *
* @description * @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:** * **The $location service:**
* *
@ -421,7 +424,8 @@ function locationGetterSetter(property, preprocess) {
* - Clicks on a link. * - Clicks on a link.
* - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). * - 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', this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
function( $rootScope, $browser, $sniffer, $rootElement) { function( $rootScope, $browser, $sniffer, $rootElement) {
var currentUrl, var $location,
basePath = $browser.baseHref() || '/', basePath = $browser.baseHref() || '/',
pathPrefix = pathPrefixFromBase(basePath), pathPrefix = pathPrefixFromBase(basePath),
initUrl = $browser.url(); initUrl = $browser.url(),
absUrlPrefix;
if (html5Mode) { if (html5Mode) {
if ($sniffer.history) { if ($sniffer.history) {
currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix); $location = new LocationUrl(
convertToHtml5Url(initUrl, basePath, hashPrefix),
pathPrefix);
} else { } else {
currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix), $location = new LocationHashbangUrl(
hashPrefix); convertToHashbangUrl(initUrl, basePath, hashPrefix),
hashPrefix);
} }
// link rewriting
var u = currentUrl,
absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.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
if (event.ctrlKey || event.metaKey || event.which == 2) return;
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();
}
var absHref = elm.prop('href');
if (!absHref ||
elm.attr('target') ||
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
return;
}
// 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 { } else {
currentUrl = new LocationHashbangUrl(initUrl, hashPrefix); $location = new LocationHashbangUrl(initUrl, hashPrefix);
} }
// 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
if (event.ctrlKey || event.metaKey || event.which == 2) return;
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();
}
var absHref = elm.prop('href');
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;
});
// rewrite hashbang url <> html5 url // rewrite hashbang url <> html5 url
if (currentUrl.absUrl() != initUrl) { if ($location.absUrl() != initUrl) {
$browser.url(currentUrl.absUrl(), true); $browser.url($location.absUrl(), true);
} }
// update $location when $browser url changes // update $location when $browser url changes
$browser.onUrlChange(function(newUrl) { $browser.onUrlChange(function(newUrl) {
if (currentUrl.absUrl() != newUrl) { if ($location.absUrl() != newUrl) {
$rootScope.$evalAsync(function() { $rootScope.$evalAsync(function() {
currentUrl.$$parse(newUrl); var oldUrl = $location.absUrl();
$location.$$parse(newUrl);
afterLocationChange(oldUrl);
}); });
if (!$rootScope.$$phase) $rootScope.$digest(); if (!$rootScope.$$phase) $rootScope.$digest();
} }
@ -537,17 +549,29 @@ function $LocationProvider(){
// update browser // update browser
var changeCounter = 0; var changeCounter = 0;
$rootScope.$watch(function $locationWatch() { $rootScope.$watch(function $locationWatch() {
if ($browser.url() != currentUrl.absUrl()) { var oldUrl = $browser.url();
if (!changeCounter || oldUrl != $location.absUrl()) {
changeCounter++; changeCounter++;
$rootScope.$evalAsync(function() { $rootScope.$evalAsync(function() {
$browser.url(currentUrl.absUrl(), currentUrl.$$replace); if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
currentUrl.$$replace = false; defaultPrevented) {
$location.$$parse(oldUrl);
} else {
$browser.url($location.absUrl(), $location.$$replace);
$location.$$replace = false;
afterLocationChange(oldUrl);
}
}); });
} }
return changeCounter; return changeCounter;
}); });
return currentUrl; return $location;
function afterLocationChange(oldUrl) {
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
}
}]; }];
} }

View file

@ -286,7 +286,6 @@ function $RouteProvider(){
*/ */
var matcher = switchRouteMatcher, var matcher = switchRouteMatcher,
dirty = 0,
forceReload = false, forceReload = false,
$route = { $route = {
routes: routes, routes: routes,
@ -304,12 +303,12 @@ function $RouteProvider(){
* creates new scope, reinstantiates the controller. * creates new scope, reinstantiates the controller.
*/ */
reload: function() { reload: function() {
dirty++;
forceReload = true; forceReload = true;
$rootScope.$evalAsync(updateRoute);
} }
}; };
$rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute); $rootScope.$on('$locationChangeSuccess', updateRoute);
return $route; return $route;

View file

@ -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() { it('should not rewrite full url links do different domain', function() {
configureService('http://www.dot.abc/a?b=c', true); configureService('http://www.dot.abc/a?b=c', true);
inject( 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);
});
});
});
}); });

View file

@ -60,6 +60,28 @@ describe('$route', function() {
}); });
it('should not change route when location is canceled', function() {
module(function($routeProvider) {
$routeProvider.when('/somePath', {template: 'some path'});
});
inject(function($route, $location, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event) {
$log.info('$locationChangeStart');
event.preventDefault();
});
$rootScope.$on('$beforeRouteChange', function(event) {
throw new Error('Should not get here');
});
$location.path('/somePath');
$rootScope.$digest();
expect($log.info.logs.shift()).toEqual(['$locationChangeStart']);
});
});
it('should match a route that contains special chars in the path', function() { it('should match a route that contains special chars in the path', function() {
module(function($routeProvider) { module(function($routeProvider) {
$routeProvider.when('/$test.23/foo(bar)/:baz', {templateUrl: 'test.html'}); $routeProvider.when('/$test.23/foo(bar)/:baz', {templateUrl: 'test.html'});
@ -540,8 +562,11 @@ describe('$route', function() {
}); });
inject(function($route, $location, $rootScope) { inject(function($route, $location, $rootScope) {
var replace; var replace;
$rootScope.$watch(function() {
if (isUndefined(replace)) replace = $location.$$replace; $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
if (oldUrl == 'http://server/#/foo/id3/eId') {
replace = $location.$$replace;
}
}); });
$location.path('/foo/id3/eId'); $location.path('/foo/id3/eId');