Skip to content

Commit

Permalink
fix(ngEventDirs): execute blur and focus expression using `scope.…
Browse files Browse the repository at this point in the history
…$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.

Related to angular#5945
Fixes angular#4979
  • Loading branch information
tbosch committed Aug 27, 2014
1 parent 2efe1c2 commit 8d6c7ed
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 21 deletions.
30 changes: 27 additions & 3 deletions src/ng/directive/ngEventDirs.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
* Events that are handled via these handler are always configured not to propagate further.
*/
var ngEventDirectives = {};

// For events that might fire synchronously during DOM manipulation
// we need to execute their event handlers asynchronously using $evalAsync,
// so that they are not executed in an inconsistent state.
var forceAsyncEvents = {
'blur': true,
'focus': true
};
forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(name) {
Expand All @@ -47,10 +55,16 @@ forEach(
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
var eventName = lowercase(name);
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event:event});
});
};
if (forceAsyncEvents[eventName]) {
scope.$evalAsync(callback)
} else {
scope.$apply(callback);
}
});
};
}
Expand Down Expand Up @@ -367,6 +381,11 @@ forEach(
* @description
* Specify custom behavior on focus event.
*
* Note: As the `blur` event is executed synchronously also during DOM manipulations
* (e.g. adding an input with the html `autofocus` attribute),
* AngularJS executes the expression asynchronously (using `scope.$evalAsync`) to ensure
* a consistent model.
*
* @element window, input, select, textarea, a
* @priority 0
* @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
Expand All @@ -383,6 +402,11 @@ forEach(
* @description
* Specify custom behavior on blur event.
*
* Note: As the `blur` event is executed synchronously also during DOM manipulations
* (e.g. removing a focussed input),
* AngularJS executes the expression asynchronously (using `scope.$evalAsync`) to ensure
* a consistent model.
*
* @element window, input, select, textarea, a
* @priority 0
* @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon
Expand Down
30 changes: 30 additions & 0 deletions test/ng/directive/ngEventDirsSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,34 @@ describe('event directives', function() {
expect($rootScope.formSubmitted).toEqual('foo');
}));
});

describe('sync events that might be triggered by DOM changes', function() {

it('should call the blur and focus listener asynchronously', inject(function($rootScope, $compile) {
element = $compile('<input type="text" ng-focus="focus()" ng-blur="blur()">')($rootScope);
$rootScope.blur = jasmine.createSpy('blur');
$rootScope.focus = jasmine.createSpy('focus');
// need to add to document so that blur/focus fires
document.body.appendChild(element[0]);

element[0].focus();

expect($rootScope.blur).not.toHaveBeenCalled();
expect($rootScope.focus).not.toHaveBeenCalled();

$rootScope.$apply();

expect($rootScope.blur).not.toHaveBeenCalled();
expect($rootScope.focus).toHaveBeenCalledOnce();

element[0].blur();

$rootScope.$apply();

expect($rootScope.blur).toHaveBeenCalledOnce();
expect($rootScope.focus).toHaveBeenCalledOnce();

}));

});
});
18 changes: 0 additions & 18 deletions test/ng/directive/ngKeySpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,5 @@ describe('ngKeyup and ngKeydown directives', function() {
expect($rootScope.touched).toEqual(true);
}));

it('should get called on focus', inject(function($rootScope, $compile) {
element = $compile('<input ng-focus="touched = true">')($rootScope);
$rootScope.$digest();
expect($rootScope.touched).toBeFalsy();

browserTrigger(element, 'focus');
expect($rootScope.touched).toEqual(true);
}));

it('should get called on blur', inject(function($rootScope, $compile) {
element = $compile('<input ng-blur="touched = true">')($rootScope);
$rootScope.$digest();
expect($rootScope.touched).toBeFalsy();

browserTrigger(element, 'blur');
expect($rootScope.touched).toEqual(true);
}));

});

0 comments on commit 8d6c7ed

Please sign in to comment.