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

Commit 54f0bc0

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 #4979 Fixes #5945 Closes #8803 Closes #6910 Closes #5402
1 parent 2ece4d0 commit 54f0bc0

File tree

3 files changed

+86
-21
lines changed

3 files changed

+86
-21
lines changed

src/ng/directive/ngEventDirs.js

+26-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) {
@@ -46,10 +54,16 @@ forEach(
4654
compile: function($element, attr) {
4755
var fn = $parse(attr[directiveName]);
4856
return function ngEventHandler(scope, element) {
49-
element.on(lowercase(name), function(event) {
50-
scope.$apply(function() {
57+
var eventName = lowercase(name);
58+
element.on(eventName, function(event) {
59+
var callback = function() {
5160
fn(scope, {$event:event});
52-
});
61+
};
62+
if (forceAsyncEvents[eventName] && scope.$$phase) {
63+
scope.$evalAsync(callback);
64+
} else {
65+
scope.$apply(callback);
66+
}
5367
});
5468
};
5569
}
@@ -366,6 +380,10 @@ forEach(
366380
* @description
367381
* Specify custom behavior on focus event.
368382
*
383+
* Note: As the `focus` event is executed synchronously when calling `input.focus()`
384+
* AngularJS executes the expression using `scope.$evalAsync` if the event is fired
385+
* during an `$apply` to ensure a consistent state.
386+
*
369387
* @element window, input, select, textarea, a
370388
* @priority 0
371389
* @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
@@ -382,6 +400,11 @@ forEach(
382400
* @description
383401
* Specify custom behavior on blur event.
384402
*
403+
* Note: As the `blur` event is executed synchronously also during DOM manipulations
404+
* (e.g. removing a focussed input),
405+
* AngularJS executes the expression using `scope.$evalAsync` if the event is fired
406+
* during an `$apply` to ensure a consistent state.
407+
*
385408
* @element window, input, select, textarea, a
386409
* @priority 0
387410
* @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon

test/ng/directive/ngEventDirsSpec.js

+60
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

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