diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js
index bfddc720644c..c93f64a25c32 100644
--- a/src/ng/directive/ngEventDirs.js
+++ b/src/ng/directive/ngEventDirs.js
@@ -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) {
@@ -46,10 +54,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.$$phase) {
+ scope.$evalAsync(callback);
+ } else {
+ scope.$apply(callback);
+ }
});
};
}
@@ -366,6 +380,10 @@ forEach(
* @description
* Specify custom behavior on focus event.
*
+ * Note: As the `focus` event is executed synchronously when calling `input.focus()`
+ * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
+ * during an `$apply` to ensure a consistent state.
+ *
* @element window, input, select, textarea, a
* @priority 0
* @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
@@ -382,6 +400,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 using `scope.$evalAsync` if the event is fired
+ * during an `$apply` to ensure a consistent state.
+ *
* @element window, input, select, textarea, a
* @priority 0
* @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon
diff --git a/test/ng/directive/ngEventDirsSpec.js b/test/ng/directive/ngEventDirsSpec.js
index 5b73c2dd6a8b..1e1d5c92be55 100644
--- a/test/ng/directive/ngEventDirsSpec.js
+++ b/test/ng/directive/ngEventDirsSpec.js
@@ -39,4 +39,64 @@ describe('event directives', function() {
expect($rootScope.formSubmitted).toEqual('foo');
}));
});
+
+ describe('focus', function() {
+
+ it('should call the listener asynchronously during $apply',
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.focus = jasmine.createSpy('focus');
+
+ $rootScope.$apply(function() {
+ element.triggerHandler('focus');
+ expect($rootScope.focus).not.toHaveBeenCalled();
+ });
+
+ expect($rootScope.focus).toHaveBeenCalledOnce();
+ }));
+
+ it('should call the listener synchronously inside of $apply if outside of $apply',
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.focus = jasmine.createSpy('focus').andCallFake(function() {
+ $rootScope.value = 'newValue';
+ });
+
+ element.triggerHandler('focus');
+
+ expect($rootScope.focus).toHaveBeenCalledOnce();
+ expect(element.val()).toBe('newValue');
+ }));
+
+ });
+
+ describe('blur', function() {
+
+ it('should call the listener asynchronously during $apply',
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.blur = jasmine.createSpy('blur');
+
+ $rootScope.$apply(function() {
+ element.triggerHandler('blur');
+ expect($rootScope.blur).not.toHaveBeenCalled();
+ });
+
+ expect($rootScope.blur).toHaveBeenCalledOnce();
+ }));
+
+ it('should call the listener synchronously inside of $apply if outside of $apply',
+ inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.blur = jasmine.createSpy('blur').andCallFake(function() {
+ $rootScope.value = 'newValue';
+ });
+
+ element.triggerHandler('blur');
+
+ expect($rootScope.blur).toHaveBeenCalledOnce();
+ expect(element.val()).toBe('newValue');
+ }));
+
+ });
});
diff --git a/test/ng/directive/ngKeySpec.js b/test/ng/directive/ngKeySpec.js
index ef5addd507ed..c7b989a48b14 100644
--- a/test/ng/directive/ngKeySpec.js
+++ b/test/ng/directive/ngKeySpec.js
@@ -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('')($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('')($rootScope);
- $rootScope.$digest();
- expect($rootScope.touched).toBeFalsy();
-
- browserTrigger(element, 'blur');
- expect($rootScope.touched).toEqual(true);
- }));
-
});