From 51ca67f23ed0869f246dac10de4d6891fce6ab7b Mon Sep 17 00:00:00 2001 From: Stewart A Date: Sat, 1 Apr 2017 16:59:08 +0100 Subject: [PATCH] feat($rootScope): Implement stopPropatagion for $broadcast events $scope.$broadcast dispatches an event to all child scopes by traversing the scope tree in a depth-first manner. This change allows any scope to prevent it's children from receiving that event by calling stopPropagation on the event object. Other listeners on the scope which called stopPropagation will continue to receive the event, as will siblings of that scope and any children of those siblings. --- src/ng/rootScope.js | 17 ++++++++++++----- test/ng/rootScopeSpec.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index 27f8ab992aec..dc25e9d4d41a 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -1151,8 +1151,8 @@ function $RootScopeProvider() { * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the * event propagates through the scope hierarchy, this property is set to null. * - `name` - `{string}`: name of the event. - * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel - * further event propagation (available only for events that were `$emit`-ed). + * - `stopPropagation` - `{function}`: calling `stopPropagation` function will cancel + * further event propagation. * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag * to true. * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called. @@ -1272,7 +1272,9 @@ function $RootScopeProvider() { * The event life cycle starts at the scope on which `$broadcast` was called. All * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get * notified. Afterwards, the event propagates to all direct and indirect scopes of the current - * scope and calls all registered listeners along the way. The event cannot be canceled. + * scope and calls all registered listeners along the way. If a scope requests the event stop propagation + * then it will not be propagated to any children of that scope, but will continue to propagate to siblings of the + * that scope and children of those siblings unless each sibling independently stops the event. * * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed * onto the {@link ng.$exceptionHandler $exceptionHandler} service. @@ -1284,10 +1286,14 @@ function $RootScopeProvider() { $broadcast: function(name, args) { var target = this, current = target, + stopPropagation = false, next = target, event = { name: name, targetScope: target, + stopPropagation: function() { + stopPropagation = true; + }, preventDefault: function() { event.defaultPrevented = true; }, @@ -1301,6 +1307,7 @@ function $RootScopeProvider() { //down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { + stopPropagation = false; event.currentScope = current; listeners = current.$$listeners[name] || []; for (i = 0, length = listeners.length; i < length; i++) { @@ -1322,8 +1329,8 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $digest - // (though it differs due to having the extra check for $$listenerCount) - if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || + // (though it differs due to having the extra check for $$listenerCount and stopPropagation) + if (!(next = ((current.$$listenerCount[name] && !stopPropagation && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js index 364196a94b2b..46091a2cb332 100644 --- a/test/ng/rootScopeSpec.js +++ b/test/ng/rootScopeSpec.js @@ -2326,6 +2326,38 @@ describe('Scope', function() { expect(result.name).toBe('some'); expect(result.targetScope).toBe(child1); }); + + + it('should not descend past scopes that stop propagation', inject(function($rootScope) { + child1.$on('myEvent', function(event) { + event.stopPropagation(); + }); + + $rootScope.$broadcast('myEvent'); + expect(log).toBe('0>1>2>21>211>22>23>3>'); + })); + + it('should continue to pass through and past sibling scopes to one that stopped propagation', + inject(function($rootScope) { + grandChild21.$on('myEvent', function(event) { + event.stopPropagation(); + }); + + $rootScope.$broadcast('myEvent'); + expect(log).toBe('0>1>11>2>21>22>23>3>'); + })); + + it('should allow multiple separate subtrees to stop propagation independently', inject(function($rootScope) { + var stopPropagation = function(event) { + event.stopPropagation(); + }; + child1.$on('myEvent', stopPropagation); + grandChild21.$on('myEvent', stopPropagation); + grandChild22.$on('myEvent', stopPropagation); + + $rootScope.$broadcast('myEvent'); + expect(log).toBe('0>1>2>21>22>23>3>'); + })); });