Skip to content

Commit cb9d4a9

Browse files
committed
feat($location): add [before|after]LocatonChange event
This allows location change cancalation
1 parent 54c283b commit cb9d4a9

File tree

5 files changed

+234
-61
lines changed

5 files changed

+234
-61
lines changed

src/bootstrap/bootstrap-prettify.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,12 @@ directive.ngEmbedApp = ['$templateCache', '$browser', '$rootScope', '$location',
190190
$provide.value('$anchorScroll', angular.noop);
191191
$provide.value('$browser', $browser);
192192
$provide.provider('$location', function() {
193-
this.$get = function() { return $location; };
193+
this.$get = ['$rootScope', function($rootScope) {
194+
docsRootScope.$on('$afterLocationChange', function(event, oldUrl, newUrl) {
195+
$rootScope.$broadcast('$afterLocationChange', oldUrl, newUrl);
196+
});
197+
return $location;
198+
}];
194199
this.html5Mode = angular.noop;
195200
});
196201
$provide.decorator('$defer', ['$rootScope', '$delegate', function($rootScope, $delegate) {

src/ng/location.js

+56-42
Original file line numberDiff line numberDiff line change
@@ -470,84 +470,98 @@ function $LocationProvider(){
470470

471471
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
472472
function( $rootScope, $browser, $sniffer, $rootElement) {
473-
var currentUrl,
473+
var $location,
474474
basePath = $browser.baseHref() || '/',
475475
pathPrefix = pathPrefixFromBase(basePath),
476-
initUrl = $browser.url();
476+
initUrl = $browser.url(),
477+
absUrlPrefix;
477478

478479
if (html5Mode) {
479480
if ($sniffer.history) {
480-
currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix);
481+
$location = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix);
481482
} else {
482-
currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix),
483-
hashPrefix);
483+
$location = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix), hashPrefix);
484484
}
485+
} else {
486+
$location = new LocationHashbangUrl(initUrl, hashPrefix);
487+
}
485488

486-
// link rewriting
487-
var u = currentUrl,
488-
absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix;
489+
// link rewriting
490+
absUrlPrefix = composeProtocolHostPort($location.protocol(), $location.host(), $location.port()) + pathPrefix;
489491

490-
$rootElement.bind('click', function(event) {
491-
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
492-
// currently we open nice url link and redirect then
492+
$rootElement.bind('click', function(event) {
493+
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
494+
// currently we open nice url link and redirect then
493495

494-
if (event.ctrlKey || event.metaKey || event.which == 2) return;
496+
if (event.ctrlKey || event.metaKey || event.which == 2) return;
495497

496-
var elm = jqLite(event.target);
498+
var elm = jqLite(event.target);
497499

498-
// traverse the DOM up to find first A tag
499-
while (elm.length && lowercase(elm[0].nodeName) !== 'a') {
500-
elm = elm.parent();
501-
}
500+
// traverse the DOM up to find first A tag
501+
while (elm.length && lowercase(elm[0].nodeName) !== 'a') {
502+
elm = elm.parent();
503+
}
502504

503-
var absHref = elm.prop('href');
505+
var absHref = elm.prop('href');
504506

505-
if (!absHref ||
506-
elm.attr('target') ||
507-
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
508-
return;
509-
}
507+
if (!absHref ||
508+
elm.attr('target') ||
509+
absHref.indexOf(absUrlPrefix) !== 0) { // link to different domain or base path
510+
return;
511+
}
512+
513+
// update location with href without the prefix
514+
$location.url(absHref.substr(absUrlPrefix.length));
515+
$rootScope.$apply();
516+
event.preventDefault();
517+
// hack to work around FF6 bug 684208 when scenario runner clicks on links
518+
window.angular['ff-684208-preventDefault'] = true;
519+
});
510520

511-
// update location with href without the prefix
512-
currentUrl.url(absHref.substr(absUrlPrefix.length));
513-
$rootScope.$apply();
514-
event.preventDefault();
515-
// hack to work around FF6 bug 684208 when scenario runner clicks on links
516-
window.angular['ff-684208-preventDefault'] = true;
517-
});
518-
} else {
519-
currentUrl = new LocationHashbangUrl(initUrl, hashPrefix);
520-
}
521521

522522
// rewrite hashbang url <> html5 url
523-
if (currentUrl.absUrl() != initUrl) {
524-
$browser.url(currentUrl.absUrl(), true);
523+
if ($location.absUrl() != initUrl) {
524+
$browser.url($location.absUrl(), true);
525525
}
526526

527527
// update $location when $browser url changes
528528
$browser.onUrlChange(function(newUrl) {
529-
if (currentUrl.absUrl() != newUrl) {
529+
if ($location.absUrl() != newUrl) {
530530
$rootScope.$evalAsync(function() {
531-
currentUrl.$$parse(newUrl);
531+
var oldUrl = $location.absUrl();
532+
533+
$location.$$parse(newUrl);
534+
afterLocationChange(oldUrl);
532535
});
533536
if (!$rootScope.$$phase) $rootScope.$digest();
534537
}
535538
});
536539

537540
// update browser
538541
var changeCounter = 0;
539-
$rootScope.$watch(function $locationWatch() {
540-
if ($browser.url() != currentUrl.absUrl()) {
542+
$rootScope.$watch(function() {
543+
var oldUrl = $browser.url();
544+
545+
if (!changeCounter || oldUrl != $location.absUrl()) {
541546
changeCounter++;
542547
$rootScope.$evalAsync(function() {
543-
$browser.url(currentUrl.absUrl(), currentUrl.$$replace);
544-
currentUrl.$$replace = false;
548+
if ($rootScope.$broadcast('$beforeLocationChange', oldUrl, $location.absUrl()).defaultPrevented) {
549+
$location.$$parse(oldUrl);
550+
} else {
551+
$browser.url($location.absUrl(), $location.$$replace);
552+
$location.$$replace = false;
553+
afterLocationChange(oldUrl);
554+
}
545555
});
546556
}
547557

548558
return changeCounter;
549559
});
550560

551-
return currentUrl;
561+
return $location;
562+
563+
function afterLocationChange(oldUrl) {
564+
$rootScope.$broadcast('$afterLocationChange', oldUrl, $location.absUrl());
565+
}
552566
}];
553567
}

src/ng/route.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,6 @@ function $RouteProvider(){
287287
*/
288288

289289
var matcher = switchRouteMatcher,
290-
dirty = 0,
291290
forceReload = false,
292291
$route = {
293292
routes: routes,
@@ -305,12 +304,12 @@ function $RouteProvider(){
305304
* creates new scope, reinstantiates the controller.
306305
*/
307306
reload: function() {
308-
dirty++;
309307
forceReload = true;
308+
$rootScope.$evalAsync(updateRoute);
310309
}
311310
};
312311

313-
$rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute);
312+
$rootScope.$on('$afterLocationChange', updateRoute);
314313

315314
return $route;
316315

test/ng/locationSpec.js

+143-13
Original file line numberDiff line numberDiff line change
@@ -791,19 +791,6 @@ describe('$location', function() {
791791
});
792792

793793

794-
it('should not rewrite when history disabled', function() {
795-
configureService('#new', false);
796-
inject(
797-
initBrowser(),
798-
initLocation(),
799-
function($browser) {
800-
browserTrigger(link, 'click');
801-
expectNoRewrite($browser);
802-
}
803-
);
804-
});
805-
806-
807794
it('should not rewrite full url links do different domain', function() {
808795
configureService('http://www.dot.abc/a?b=c', true);
809796
inject(
@@ -982,4 +969,147 @@ describe('$location', function() {
982969
});
983970
}
984971
});
972+
973+
974+
describe('location cancellation', function() {
975+
it('should fire $before/afterLocationChange event', inject(function($location, $browser, $rootScope, $log) {
976+
expect($browser.url()).toEqual('http://server/');
977+
978+
$rootScope.$on('$beforeLocationChange', function(event, newUrl, oldUrl) {
979+
$log.info('before', newUrl, oldUrl, $browser.url());
980+
});
981+
$rootScope.$on('$afterLocationChange', function(event, newUrl, oldUrl) {
982+
$log.info('after', newUrl, oldUrl, $browser.url());
983+
});
984+
985+
expect($location.url()).toEqual('');
986+
$location.url('/somePath');
987+
expect($location.url()).toEqual('/somePath');
988+
expect($browser.url()).toEqual('http://server/');
989+
expect($log.info.logs).toEqual([]);
990+
991+
$rootScope.$apply();
992+
993+
expect($log.info.logs.shift()).
994+
toEqual(['before', 'http://server/', 'http://server/#/somePath', 'http://server/']);
995+
expect($log.info.logs.shift()).
996+
toEqual(['after', 'http://server/', 'http://server/#/somePath', 'http://server/#/somePath']);
997+
expect($location.url()).toEqual('/somePath');
998+
expect($browser.url()).toEqual('http://server/#/somePath');
999+
}));
1000+
1001+
1002+
it('should allow $beforeLocationChange event cancellation', inject(function($location, $browser, $rootScope, $log) {
1003+
expect($browser.url()).toEqual('http://server/');
1004+
expect($location.url()).toEqual('');
1005+
1006+
$rootScope.$on('$beforeLocationChange', function(event, newUrl, oldUrl) {
1007+
$log.info('before', newUrl, oldUrl, $browser.url());
1008+
event.preventDefault();
1009+
});
1010+
$rootScope.$on('$afterLocationChange', function(event, newUrl, oldUrl) {
1011+
throw Error('location should have been canceled');
1012+
});
1013+
1014+
expect($location.url()).toEqual('');
1015+
$location.url('/somePath');
1016+
expect($location.url()).toEqual('/somePath');
1017+
expect($browser.url()).toEqual('http://server/');
1018+
expect($log.info.logs).toEqual([]);
1019+
1020+
$rootScope.$apply();
1021+
1022+
expect($log.info.logs.shift()).
1023+
toEqual(['before', 'http://server/', 'http://server/#/somePath', 'http://server/']);
1024+
expect($log.info.logs[1]).toBeUndefined();
1025+
expect($location.url()).toEqual('');
1026+
expect($browser.url()).toEqual('http://server/');
1027+
}));
1028+
1029+
it ('should fire $afterLocationChange event when change from browser location bar',
1030+
inject(function($log, $location, $browser, $rootScope) {
1031+
$rootScope.$apply(); // clear initial $beforeLocationChange
1032+
1033+
expect($browser.url()).toEqual('http://server/');
1034+
expect($location.url()).toEqual('');
1035+
1036+
$rootScope.$on('$beforeLocationChange', function(event, newUrl, oldUrl) {
1037+
throw Error('there is no before when user enters URL directly to browser');
1038+
});
1039+
$rootScope.$on('$afterLocationChange', function(event, newUrl, oldUrl) {
1040+
$log.info('after', newUrl, oldUrl);
1041+
});
1042+
1043+
1044+
$browser.url('http://server/#/somePath');
1045+
$browser.poll();
1046+
1047+
expect($log.info.logs.shift()).toEqual(['after', 'http://server/', 'http://server/#/somePath']);
1048+
})
1049+
);
1050+
1051+
1052+
it('should listen on click events on href and prevent browser default in hasbang mode', function() {
1053+
module(function() {
1054+
return function($rootElement, $compile, $rootScope) {
1055+
$rootElement.html('<a href="http://server/#/somePath">link</a>');
1056+
$compile($rootElement)($rootScope);
1057+
jqLite(document.body).append($rootElement);
1058+
}
1059+
});
1060+
1061+
inject(function($location, $rootScope, $browser, $rootElement) {
1062+
var log = '',
1063+
link = $rootElement.find('a');
1064+
1065+
1066+
$rootScope.$on('$beforeLocationChange', function(event) {
1067+
event.preventDefault();
1068+
log += '$beforeLocationChange';
1069+
});
1070+
$rootScope.$on('$afterLocationChange', function() {
1071+
throw new Error('after cancellation in hashbang mode');
1072+
});
1073+
1074+
browserTrigger(link, 'click');
1075+
1076+
expect(log).toEqual('$beforeLocationChange');
1077+
expect($browser.url()).toEqual('http://server/');
1078+
1079+
dealoc($rootElement);
1080+
});
1081+
});
1082+
1083+
1084+
it('should listen on click events on href and prevent browser default in html5 mode', function() {
1085+
module(function($locationProvider) {
1086+
$locationProvider.html5Mode(true);
1087+
return function($rootElement, $compile, $rootScope) {
1088+
$rootElement.html('<a href="http://server/somePath">link</a>');
1089+
$compile($rootElement)($rootScope);
1090+
jqLite(document.body).append($rootElement);
1091+
}
1092+
});
1093+
1094+
inject(function($location, $rootScope, $browser, $rootElement) {
1095+
var log = '',
1096+
link = $rootElement.find('a');
1097+
1098+
$rootScope.$on('$beforeLocationChange', function(event) {
1099+
event.preventDefault();
1100+
log += '$beforeLocationChange';
1101+
});
1102+
$rootScope.$on('$afterLocationChange', function() {
1103+
throw new Error('after cancalation in html5 mode');
1104+
});
1105+
1106+
browserTrigger(link, 'click');
1107+
1108+
expect(log).toEqual('$beforeLocationChange');
1109+
expect($browser.url()).toEqual('http://server/');
1110+
1111+
dealoc($rootElement);
1112+
});
1113+
});
1114+
});
9851115
});

test/ng/routeSpec.js

+27-2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ describe('$route', function() {
6060
});
6161

6262

63+
it('should not change route when location is canceled', function() {
64+
module(function($routeProvider) {
65+
$routeProvider.when('/somePath', {template: 'some path'});
66+
});
67+
inject(function($route, $location, $rootScope, $log) {
68+
$rootScope.$on('$beforeLocationChange', function(event) {
69+
$log.info('$beforeLocationChange');
70+
event.preventDefault();
71+
});
72+
73+
$rootScope.$on('$beforeRouteChange', function(event) {
74+
throw new Error('Should not get here');
75+
});
76+
77+
$location.path('/somePath');
78+
$rootScope.$digest();
79+
80+
expect($log.info.logs.shift()).toEqual(['$beforeLocationChange']);
81+
});
82+
});
83+
84+
6385
it('should match a route that contains special chars in the path', function() {
6486
module(function($routeProvider) {
6587
$routeProvider.when('/$test.23/foo(bar)/:baz', {templateUrl: 'test.html'});
@@ -540,8 +562,11 @@ describe('$route', function() {
540562
});
541563
inject(function($route, $location, $rootScope) {
542564
var replace;
543-
$rootScope.$watch(function() {
544-
if (isUndefined(replace)) replace = $location.$$replace;
565+
566+
$rootScope.$on('$beforeLocationChange', function(oldUrl, newUrl) {
567+
if (newUrl == 'http://server/#/foo/id3/eId') {
568+
replace = $location.$$replace;
569+
}
545570
});
546571

547572
$location.path('/foo/id3/eId');

0 commit comments

Comments
 (0)