Skip to content

Commit 719c747

Browse files
committed
fix(ngEventDirs): execute blur and focus expression using scope.$evalAsync
BREAKING CHANGE: The `blur` and `focus` event fire synchronously, also during DOM operations that remove elements. This lead to errors as the Angular model was not in a consistent state. See this [fiddle](http://jsfiddle.net/fq1dq5yb/) for a demo. This change executes the expression of those events using `scope.$evalAsync` if an `$apply` is in progress, otherwise keeps the old behavior. Fixes angular#4979 Fixes angular#5945 Closes angular#8803 Closes angular#6910 Closes angular#5402
1 parent 2137542 commit 719c747

File tree

3 files changed

+86
-21
lines changed

3 files changed

+86
-21
lines changed

src/ng/directive/ngEventDirs.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
* Events that are handled via these handler are always configured not to propagate further.
3838
*/
3939
var ngEventDirectives = {};
40+
41+
// For events that might fire synchronously during DOM manipulation
42+
// we need to execute their event handlers asynchronously using $evalAsync,
43+
// so that they are not executed in an inconsistent state.
44+
var forceAsyncEvents = {
45+
'blur': true,
46+
'focus': true
47+
};
4048
forEach(
4149
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
4250
function(name) {
@@ -47,10 +55,16 @@ forEach(
4755
compile: function($element, attr) {
4856
var fn = $parse(attr[directiveName]);
4957
return function ngEventHandler(scope, element) {
50-
element.on(lowercase(name), function(event) {
51-
scope.$apply(function() {
58+
var eventName = lowercase(name);
59+
element.on(eventName, function(event) {
60+
var callback = function() {
5261
fn(scope, {$event:event});
53-
});
62+
};
63+
if (forceAsyncEvents[eventName] && scope.$$phase) {
64+
scope.$evalAsync(callback);
65+
} else {
66+
scope.$apply(callback);
67+
}
5468
});
5569
};
5670
}
@@ -367,6 +381,10 @@ forEach(
367381
* @description
368382
* Specify custom behavior on focus event.
369383
*
384+
* Note: As the `focus` event is executed synchronously when calling `input.focus()`
385+
* AngularJS executes the expression using `scope.$evalAsync` if the event is fired
386+
* during an `$apply` to ensure a consistent state.
387+
*
370388
* @element window, input, select, textarea, a
371389
* @priority 0
372390
* @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
@@ -383,6 +401,11 @@ forEach(
383401
* @description
384402
* Specify custom behavior on blur event.
385403
*
404+
* Note: As the `blur` event is executed synchronously also during DOM manipulations
405+
* (e.g. removing a focussed input),
406+
* AngularJS executes the expression using `scope.$evalAsync` if the event is fired
407+
* during an `$apply` to ensure a consistent state.
408+
*
386409
* @element window, input, select, textarea, a
387410
* @priority 0
388411
* @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon

test/ng/directive/ngEventDirsSpec.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,64 @@ describe('event directives', function() {
3939
expect($rootScope.formSubmitted).toEqual('foo');
4040
}));
4141
});
42+
43+
describe('focus', function() {
44+
45+
it('should call the listener asynchronously during $apply',
46+
inject(function($rootScope, $compile) {
47+
element = $compile('<input type="text" ng-focus="focus()">')($rootScope);
48+
$rootScope.focus = jasmine.createSpy('focus');
49+
50+
$rootScope.$apply(function() {
51+
element.triggerHandler('focus');
52+
expect($rootScope.focus).not.toHaveBeenCalled();
53+
});
54+
55+
expect($rootScope.focus).toHaveBeenCalledOnce();
56+
}));
57+
58+
it('should call the listener synchronously inside of $apply if outside of $apply',
59+
inject(function($rootScope, $compile) {
60+
element = $compile('<input type="text" ng-focus="focus()" ng-model="value">')($rootScope);
61+
$rootScope.focus = jasmine.createSpy('focus').andCallFake(function() {
62+
$rootScope.value = 'newValue';
63+
});
64+
65+
element.triggerHandler('focus');
66+
67+
expect($rootScope.focus).toHaveBeenCalledOnce();
68+
expect(element.val()).toBe('newValue');
69+
}));
70+
71+
});
72+
73+
describe('blur', function() {
74+
75+
it('should call the listener asynchronously during $apply',
76+
inject(function($rootScope, $compile) {
77+
element = $compile('<input type="text" ng-blur="blur()">')($rootScope);
78+
$rootScope.blur = jasmine.createSpy('blur');
79+
80+
$rootScope.$apply(function() {
81+
element.triggerHandler('blur');
82+
expect($rootScope.blur).not.toHaveBeenCalled();
83+
});
84+
85+
expect($rootScope.blur).toHaveBeenCalledOnce();
86+
}));
87+
88+
it('should call the listener synchronously inside of $apply if outside of $apply',
89+
inject(function($rootScope, $compile) {
90+
element = $compile('<input type="text" ng-blur="blur()" ng-model="value">')($rootScope);
91+
$rootScope.blur = jasmine.createSpy('blur').andCallFake(function() {
92+
$rootScope.value = 'newValue';
93+
});
94+
95+
element.triggerHandler('blur');
96+
97+
expect($rootScope.blur).toHaveBeenCalledOnce();
98+
expect(element.val()).toBe('newValue');
99+
}));
100+
101+
});
42102
});

test/ng/directive/ngKeySpec.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,5 @@ describe('ngKeyup and ngKeydown directives', function() {
3434
expect($rootScope.touched).toEqual(true);
3535
}));
3636

37-
it('should get called on focus', inject(function($rootScope, $compile) {
38-
element = $compile('<input ng-focus="touched = true">')($rootScope);
39-
$rootScope.$digest();
40-
expect($rootScope.touched).toBeFalsy();
41-
42-
browserTrigger(element, 'focus');
43-
expect($rootScope.touched).toEqual(true);
44-
}));
45-
46-
it('should get called on blur', inject(function($rootScope, $compile) {
47-
element = $compile('<input ng-blur="touched = true">')($rootScope);
48-
$rootScope.$digest();
49-
expect($rootScope.touched).toBeFalsy();
50-
51-
browserTrigger(element, 'blur');
52-
expect($rootScope.touched).toEqual(true);
53-
}));
54-
5537
});
5638

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy