Skip to content

Commit 3db5ff1

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 add/remove elements. This lead to errors as the Angular model was not in a consistent state. This change executes the expression of those events now asynchronously. Fixes angular#4979 Fixes angular#5945 Closes angular#8803 Closes angular#6910 Closes angular#5402
1 parent 2efe1c2 commit 3db5ff1

File tree

3 files changed

+57
-21
lines changed

3 files changed

+57
-21
lines changed

src/ng/directive/ngEventDirs.js

+27-3
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]) {
64+
scope.$evalAsync(callback);
65+
} else {
66+
scope.$apply(callback);
67+
}
5468
});
5569
};
5670
}
@@ -367,6 +381,11 @@ forEach(
367381
* @description
368382
* Specify custom behavior on focus event.
369383
*
384+
* Note: As the `blur` event is executed synchronously also during DOM manipulations
385+
* (e.g. adding an input with the html `autofocus` attribute),
386+
* AngularJS executes the expression asynchronously (using `scope.$evalAsync`) to ensure
387+
* a consistent model.
388+
*
370389
* @element window, input, select, textarea, a
371390
* @priority 0
372391
* @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
@@ -383,6 +402,11 @@ forEach(
383402
* @description
384403
* Specify custom behavior on blur event.
385404
*
405+
* Note: As the `blur` event is executed synchronously also during DOM manipulations
406+
* (e.g. removing a focussed input),
407+
* AngularJS executes the expression asynchronously (using `scope.$evalAsync`) to ensure
408+
* a consistent model.
409+
*
386410
* @element window, input, select, textarea, a
387411
* @priority 0
388412
* @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon

test/ng/directive/ngEventDirsSpec.js

+30
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,34 @@ describe('event directives', function() {
3939
expect($rootScope.formSubmitted).toEqual('foo');
4040
}));
4141
});
42+
43+
describe('sync events that might be triggered by DOM changes', function() {
44+
45+
it('should call the blur and focus listener asynchronously', inject(function($rootScope, $compile) {
46+
element = $compile('<input type="text" ng-focus="focus()" ng-blur="blur()">')($rootScope);
47+
$rootScope.blur = jasmine.createSpy('blur');
48+
$rootScope.focus = jasmine.createSpy('focus');
49+
// need to add to document so that blur/focus fires
50+
document.body.appendChild(element[0]);
51+
52+
element[0].focus();
53+
54+
expect($rootScope.blur).not.toHaveBeenCalled();
55+
expect($rootScope.focus).not.toHaveBeenCalled();
56+
57+
$rootScope.$apply();
58+
59+
expect($rootScope.blur).not.toHaveBeenCalled();
60+
expect($rootScope.focus).toHaveBeenCalledOnce();
61+
62+
element[0].blur();
63+
64+
$rootScope.$apply();
65+
66+
expect($rootScope.blur).toHaveBeenCalledOnce();
67+
expect($rootScope.focus).toHaveBeenCalledOnce();
68+
69+
}));
70+
71+
});
4272
});

test/ng/directive/ngKeySpec.js

-18
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)