diff --git a/src/ng/directive/ngEventDirs.js b/src/ng/directive/ngEventDirs.js index c14b173a2b48..3a8f3d034a60 100644 --- a/src/ng/directive/ngEventDirs.js +++ b/src/ng/directive/ngEventDirs.js @@ -60,8 +60,12 @@ forEach( var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); return function ngEventHandler(scope, element) { element.on(eventName, function(event) { - var callback = function() { - fn(scope, {$event:event}); + var callback = function(scope, locals) { + fn(scope, { + $event:event, + $abortApply: locals && locals.$abortApply, + $partialDigest: locals && locals.$partialDigest + }); }; if (forceAsyncEvents[eventName] && $rootScope.$$phase) { scope.$evalAsync(callback); diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 1ee04ed15770..ace548cef3b3 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1022,18 +1022,31 @@ function $RootScopeProvider() { * @returns {*} The result of evaluating the expression. */ $apply: function(expr) { + var abortApply = false; + var scopeForDigest = $rootScope; + var currentScope = this; + try { beginPhase('$apply'); - return this.$eval(expr); + return this.$eval(expr, { + $abortApply: function() { + abortApply = true; + }, + $partialDigest: function(scope) { + scopeForDigest = scope || currentScope; + } + }); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); - try { - $rootScope.$digest(); - } catch (e) { - $exceptionHandler(e); - throw e; + if (!abortApply) { + try { + scopeForDigest.$digest(); + } catch (e) { + $exceptionHandler(e); + throw e; + } } } }, diff --git a/test/ng/directive/ngClickSpec.js b/test/ng/directive/ngClickSpec.js index 3e997c462b20..a8f3acf163af 100644 --- a/test/ng/directive/ngClickSpec.js +++ b/test/ng/directive/ngClickSpec.js @@ -23,4 +23,24 @@ describe('ngClick', function() { browserTrigger(element, 'click'); expect($rootScope.event).toBeDefined(); })); + + it('should abort apply if abort argument is invoked', inject(function($rootScope, $compile) { + element = $compile('
{{a}}
')($rootScope); + $rootScope.$digest(); + + browserTrigger(element, 'click'); + expect(element.text()).toBe(''); + })); + + it('should digest locally if $partialDigest is invoked', inject(function($rootScope, $compile) { + var scope = $rootScope.$new(); + element = $compile('
{{a}}
')(scope); + $rootScope.$digest(); + + var watchSpy = jasmine.createSpy('watchSpy'); + $rootScope.$watch(watchSpy); + browserTrigger(element, 'click'); + expect(element.text()).toBe('1'); + expect(watchSpy).not.toHaveBeenCalled(); + })); }); diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 6ff240d6330c..9a8308f60046 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -1333,6 +1333,39 @@ describe('Scope', function() { expect(log).toEqual('1'); })); + it('should abort apply if abort argument is invoked', inject(function($rootScope) { + var watchSpy = jasmine.createSpy('watchSpy'); + $rootScope.$watch(watchSpy); + $rootScope.$apply('$abortApply()'); + expect(watchSpy).not.toHaveBeenCalled(); + })); + + it('should perform digest locally if $partialDigest is invoked', inject(function($rootScope) { + var watchRootSpy = jasmine.createSpy('watchRootSpy'); + var watchSpy = jasmine.createSpy('watchSpy'); + var child = $rootScope.$new(); + + $rootScope.$watch(watchRootSpy); + child.$watch(watchSpy); + child.$apply('$partialDigest()'); + + expect(watchRootSpy).not.toHaveBeenCalled(); + expect(watchSpy).toHaveBeenCalled(); + })); + + it('should perform digest on specific scope', inject(function($rootScope) { + var watchRootSpy = jasmine.createSpy('watchRootSpy'); + var watchSpy = jasmine.createSpy('watchSpy'); + var child = $rootScope.$new(); + var grandChild = child.$new(); + + $rootScope.$watch(watchRootSpy); + child.$watch(watchSpy); + grandChild.$apply('$partialDigest($parent)'); + + expect(watchRootSpy).not.toHaveBeenCalled(); + expect(watchSpy).toHaveBeenCalled(); + })); it('should catch exceptions', function() { module(function($exceptionHandlerProvider) {