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

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 #4979 Fixes #5945 Closes #8803 Closes #6910 Closes #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

+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) {
@@ -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

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