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

Commit f4ff11b

Browse files
committed
feat($route): ability to cancel $routeChangeStart event
Calling `preventDefault()` on a `$routeChangeStart` event will prevent the route change and also call `preventDefault` on the `$locationChangeStart` event, which prevents the location change as well. BREAKING CHANGE: Order of events has changed. Previously: `$locationChangeStart` -> `$locationChangeSuccess` -> `$routeChangeStart` -> `$routeChangeSuccess` Now: `$locationChangeStart` -> `$routeChangeStart` -> `$locationChangeSuccess` -> -> `$routeChangeSuccess` Fixes #5581 Closes #5714 Closes #9502
1 parent 0d3b69a commit f4ff11b

File tree

3 files changed

+121
-38
lines changed

3 files changed

+121
-38
lines changed

src/ng/location.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,9 @@ function $LocationProvider(){
692692
* @name $location#$locationChangeStart
693693
* @eventType broadcast on root scope
694694
* @description
695-
* Broadcasted before a URL will change. This change can be prevented by calling
695+
* Broadcasted before a URL will change.
696+
*
697+
* This change can be prevented by calling
696698
* `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more
697699
* details about event object. Upon successful change
698700
* {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.

src/ngRoute/route.js

+60-35
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,10 @@ function $RouteProvider(){
375375
* defined in `resolve` route property. Once all of the dependencies are resolved
376376
* `$routeChangeSuccess` is fired.
377377
*
378+
* The route change (and the `$location` change that triggered it) can be prevented
379+
* by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on}
380+
* for more details about event object.
381+
*
378382
* @param {Object} angularEvent Synthetic event object.
379383
* @param {Route} next Future route information.
380384
* @param {Route} current Current route information.
@@ -419,6 +423,8 @@ function $RouteProvider(){
419423
*/
420424

421425
var forceReload = false,
426+
preparedRoute,
427+
preparedRouteIsUpdateOnly,
422428
$route = {
423429
routes: routes,
424430

@@ -435,7 +441,11 @@ function $RouteProvider(){
435441
*/
436442
reload: function() {
437443
forceReload = true;
438-
$rootScope.$evalAsync(updateRoute);
444+
$rootScope.$evalAsync(function() {
445+
// Don't support cancellation of a reload for now...
446+
prepareRoute();
447+
commitRoute();
448+
});
439449
},
440450

441451
/**
@@ -469,7 +479,8 @@ function $RouteProvider(){
469479
}
470480
};
471481

472-
$rootScope.$on('$locationChangeSuccess', updateRoute);
482+
$rootScope.$on('$locationChangeStart', prepareRoute);
483+
$rootScope.$on('$locationChangeSuccess', commitRoute);
473484

474485
return $route;
475486

@@ -507,54 +518,68 @@ function $RouteProvider(){
507518
return params;
508519
}
509520

510-
function updateRoute() {
511-
var next = parseRoute(),
512-
last = $route.current;
513-
514-
if (next && last && next.$$route === last.$$route
515-
&& angular.equals(next.pathParams, last.pathParams)
516-
&& !next.reloadOnSearch && !forceReload) {
517-
last.params = next.params;
518-
angular.copy(last.params, $routeParams);
519-
$rootScope.$broadcast('$routeUpdate', last);
520-
} else if (next || last) {
521+
function prepareRoute($locationEvent) {
522+
var lastRoute = $route.current;
523+
524+
preparedRoute = parseRoute();
525+
preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
526+
&& angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
527+
&& !preparedRoute.reloadOnSearch && !forceReload;
528+
529+
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
530+
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
531+
if ($locationEvent) {
532+
$locationEvent.preventDefault();
533+
}
534+
}
535+
}
536+
}
537+
538+
function commitRoute() {
539+
var lastRoute = $route.current;
540+
var nextRoute = preparedRoute;
541+
542+
if (preparedRouteIsUpdateOnly) {
543+
lastRoute.params = nextRoute.params;
544+
angular.copy(lastRoute.params, $routeParams);
545+
$rootScope.$broadcast('$routeUpdate', lastRoute);
546+
} else if (nextRoute || lastRoute) {
521547
forceReload = false;
522-
$rootScope.$broadcast('$routeChangeStart', next, last);
523-
$route.current = next;
524-
if (next) {
525-
if (next.redirectTo) {
526-
if (angular.isString(next.redirectTo)) {
527-
$location.path(interpolate(next.redirectTo, next.params)).search(next.params)
548+
$route.current = nextRoute;
549+
if (nextRoute) {
550+
if (nextRoute.redirectTo) {
551+
if (angular.isString(nextRoute.redirectTo)) {
552+
$location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params)
528553
.replace();
529554
} else {
530-
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
555+
$location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search()))
531556
.replace();
532557
}
533558
}
534559
}
535560

536-
$q.when(next).
561+
$q.when(nextRoute).
537562
then(function() {
538-
if (next) {
539-
var locals = angular.extend({}, next.resolve),
563+
if (nextRoute) {
564+
var locals = angular.extend({}, nextRoute.resolve),
540565
template, templateUrl;
541566

542567
angular.forEach(locals, function(value, key) {
543568
locals[key] = angular.isString(value) ?
544569
$injector.get(value) : $injector.invoke(value, null, null, key);
545570
});
546571

547-
if (angular.isDefined(template = next.template)) {
572+
if (angular.isDefined(template = nextRoute.template)) {
548573
if (angular.isFunction(template)) {
549-
template = template(next.params);
574+
template = template(nextRoute.params);
550575
}
551-
} else if (angular.isDefined(templateUrl = next.templateUrl)) {
576+
} else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) {
552577
if (angular.isFunction(templateUrl)) {
553-
templateUrl = templateUrl(next.params);
578+
templateUrl = templateUrl(nextRoute.params);
554579
}
555580
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
556581
if (angular.isDefined(templateUrl)) {
557-
next.loadedTemplateUrl = templateUrl;
582+
nextRoute.loadedTemplateUrl = templateUrl;
558583
template = $templateRequest(templateUrl);
559584
}
560585
}
@@ -566,16 +591,16 @@ function $RouteProvider(){
566591
}).
567592
// after route change
568593
then(function(locals) {
569-
if (next == $route.current) {
570-
if (next) {
571-
next.locals = locals;
572-
angular.copy(next.params, $routeParams);
594+
if (nextRoute == $route.current) {
595+
if (nextRoute) {
596+
nextRoute.locals = locals;
597+
angular.copy(nextRoute.params, $routeParams);
573598
}
574-
$rootScope.$broadcast('$routeChangeSuccess', next, last);
599+
$rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
575600
}
576601
}, function(error) {
577-
if (next == $route.current) {
578-
$rootScope.$broadcast('$routeChangeError', next, last, error);
602+
if (nextRoute == $route.current) {
603+
$rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error);
579604
}
580605
});
581606
}

test/ngRoute/routeSpec.js

+58-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use strict';
22

33
describe('$route', function() {
4-
var $httpBackend;
4+
var $httpBackend,
5+
element;
56

67
beforeEach(module('ngRoute'));
78

@@ -18,6 +19,57 @@ describe('$route', function() {
1819
};
1920
}));
2021

22+
afterEach(function() {
23+
dealoc(element);
24+
});
25+
26+
it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
27+
module(function($routeProvider) {
28+
$routeProvider.when('/Edit', {
29+
id: 'edit', template: 'Some edit functionality'
30+
});
31+
$routeProvider.when('/Home', {
32+
id: 'home'
33+
});
34+
});
35+
module(provideLog);
36+
inject(function($route, $location, $rootScope, $compile, log) {
37+
$rootScope.$on('$routeChangeStart', function(event, next, current) {
38+
if (next.id === 'home' && current.scope.unsavedChanges) {
39+
event.preventDefault();
40+
}
41+
});
42+
element = $compile('<div><div ng-view></div></div>')($rootScope);
43+
$rootScope.$apply(function() {
44+
$location.path('/Edit');
45+
});
46+
$rootScope.$on('$routeChangeSuccess', log.fn('routeChangeSuccess'));
47+
$rootScope.$on('$locationChangeSuccess', log.fn('locationChangeSuccess'));
48+
49+
// aborted route change
50+
$rootScope.$apply(function() {
51+
$route.current.scope.unsavedChanges = true;
52+
});
53+
$rootScope.$apply(function() {
54+
$location.path('/Home');
55+
});
56+
expect($route.current.id).toBe('edit');
57+
expect($location.path()).toBe('/Edit');
58+
expect(log).toEqual([]);
59+
60+
// successful route change
61+
$rootScope.$apply(function() {
62+
$route.current.scope.unsavedChanges = false;
63+
});
64+
$rootScope.$apply(function() {
65+
$location.path('/Home');
66+
});
67+
expect($route.current.id).toBe('home');
68+
expect($location.path()).toBe('/Home');
69+
expect(log).toEqual(['locationChangeSuccess', 'routeChangeSuccess']);
70+
});
71+
});
72+
2173
it('should route and fire change event', function() {
2274
var log = '',
2375
lastRoute,
@@ -481,7 +533,7 @@ describe('$route', function() {
481533

482534

483535
describe('events', function() {
484-
it('should not fire $after/beforeRouteChange during bootstrap (if no route)', function() {
536+
it('should not fire $routeChangeStart/success during bootstrap (if no route)', function() {
485537
var routeChangeSpy = jasmine.createSpy('route change');
486538

487539
module(function($routeProvider) {
@@ -498,6 +550,10 @@ describe('$route', function() {
498550
$location.path('/no-route-here');
499551
$rootScope.$digest();
500552
expect(routeChangeSpy).not.toHaveBeenCalled();
553+
554+
$location.path('/one');
555+
$rootScope.$digest();
556+
expect(routeChangeSpy).toHaveBeenCalled();
501557
});
502558
});
503559

0 commit comments

Comments
 (0)