Skip to content

Commit 46f36e1

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 angular#5581 Closes angular#5714
1 parent 8ee1ba4 commit 46f36e1

File tree

2 files changed

+114
-37
lines changed

2 files changed

+114
-37
lines changed

src/ngRoute/route.js

+56-35
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,8 @@ function $RouteProvider(){
419419
*/
420420

421421
var forceReload = false,
422+
preparedRoute,
423+
preparedRouteIsUpdateOnly,
422424
$route = {
423425
routes: routes,
424426

@@ -435,7 +437,11 @@ function $RouteProvider(){
435437
*/
436438
reload: function() {
437439
forceReload = true;
438-
$rootScope.$evalAsync(updateRoute);
440+
$rootScope.$evalAsync(function() {
441+
// Don't support cancellation of a reload for now...
442+
prepareRoute();
443+
commitRoute();
444+
});
439445
},
440446

441447
/**
@@ -469,7 +475,8 @@ function $RouteProvider(){
469475
}
470476
};
471477

472-
$rootScope.$on('$locationChangeSuccess', updateRoute);
478+
$rootScope.$on('$locationChangeStart', prepareRoute);
479+
$rootScope.$on('$locationChangeSuccess', commitRoute);
473480

474481
return $route;
475482

@@ -507,54 +514,68 @@ function $RouteProvider(){
507514
return params;
508515
}
509516

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) {
517+
function prepareRoute($locationEvent) {
518+
var lastRoute = $route.current;
519+
520+
preparedRoute = parseRoute();
521+
preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
522+
&& angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
523+
&& !preparedRoute.reloadOnSearch && !forceReload;
524+
525+
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
526+
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
527+
if ($locationEvent) {
528+
$locationEvent.preventDefault();
529+
}
530+
}
531+
}
532+
}
533+
534+
function commitRoute() {
535+
var lastRoute = $route.current;
536+
var nextRoute = preparedRoute;
537+
538+
if (preparedRouteIsUpdateOnly) {
539+
lastRoute.params = nextRoute.params;
540+
angular.copy(lastRoute.params, $routeParams);
541+
$rootScope.$broadcast('$routeUpdate', lastRoute);
542+
} else if (nextRoute || lastRoute) {
521543
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)
544+
$route.current = nextRoute;
545+
if (nextRoute) {
546+
if (nextRoute.redirectTo) {
547+
if (angular.isString(nextRoute.redirectTo)) {
548+
$location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params)
528549
.replace();
529550
} else {
530-
$location.url(next.redirectTo(next.pathParams, $location.path(), $location.search()))
551+
$location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search()))
531552
.replace();
532553
}
533554
}
534555
}
535556

536-
$q.when(next).
557+
$q.when(nextRoute).
537558
then(function() {
538-
if (next) {
539-
var locals = angular.extend({}, next.resolve),
559+
if (nextRoute) {
560+
var locals = angular.extend({}, nextRoute.resolve),
540561
template, templateUrl;
541562

542563
angular.forEach(locals, function(value, key) {
543564
locals[key] = angular.isString(value) ?
544565
$injector.get(value) : $injector.invoke(value, null, null, key);
545566
});
546567

547-
if (angular.isDefined(template = next.template)) {
568+
if (angular.isDefined(template = nextRoute.template)) {
548569
if (angular.isFunction(template)) {
549-
template = template(next.params);
570+
template = template(nextRoute.params);
550571
}
551-
} else if (angular.isDefined(templateUrl = next.templateUrl)) {
572+
} else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) {
552573
if (angular.isFunction(templateUrl)) {
553-
templateUrl = templateUrl(next.params);
574+
templateUrl = templateUrl(nextRoute.params);
554575
}
555576
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
556577
if (angular.isDefined(templateUrl)) {
557-
next.loadedTemplateUrl = templateUrl;
578+
nextRoute.loadedTemplateUrl = templateUrl;
558579
template = $templateRequest(templateUrl);
559580
}
560581
}
@@ -566,16 +587,16 @@ function $RouteProvider(){
566587
}).
567588
// after route change
568589
then(function(locals) {
569-
if (next == $route.current) {
570-
if (next) {
571-
next.locals = locals;
572-
angular.copy(next.params, $routeParams);
590+
if (nextRoute == $route.current) {
591+
if (nextRoute) {
592+
nextRoute.locals = locals;
593+
angular.copy(nextRoute.params, $routeParams);
573594
}
574-
$rootScope.$broadcast('$routeChangeSuccess', next, last);
595+
$rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
575596
}
576597
}, function(error) {
577-
if (next == $route.current) {
578-
$rootScope.$broadcast('$routeChangeError', next, last, error);
598+
if (nextRoute == $route.current) {
599+
$rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error);
579600
}
580601
});
581602
}

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)