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

Commit bd131bb

Browse files
committed
fix($rootScope): avoid unstable reference in $emit event
The event.currentScope in `$emit` method was a reference to an iterator. When accessing asynchronously, the event.currentScope was the $rootScope (end of the do/while loop) instead of the scope which listen the event. The `$emit` and `$broadcast` methods use the same syntax now. Both methods are tested with : - sync/async access to `event.currentScope` and `event.targetScope`. - a test case for `preventDefault()` and `defaultPrevented`.
1 parent 814f0e0 commit bd131bb

File tree

2 files changed

+45
-15
lines changed

2 files changed

+45
-15
lines changed

src/ng/rootScope.js

+25-13
Original file line numberDiff line numberDiff line change
@@ -1029,25 +1029,33 @@ function $RootScopeProvider(){
10291029
* @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}).
10301030
*/
10311031
$emit: function(name, args) {
1032+
function Event(currentScope) {
1033+
this.name = name;
1034+
this.currentScope = currentScope;
1035+
this.targetScope = target;
1036+
this.defaultPrevented = globalDefaultPrevented;
1037+
}
1038+
Event.prototype.preventDefault = function() {
1039+
// Deals with the current instance (this) and also the next ones with globalDefaultPrevented.
1040+
this.defaultPrevented = true;
1041+
globalDefaultPrevented = true;
1042+
};
1043+
Event.prototype.stopPropagation = function() {
1044+
stopPropagation = true;
1045+
};
1046+
10321047
var empty = [],
10331048
namedListeners,
1049+
target = this,
10341050
scope = this,
10351051
stopPropagation = false,
1036-
event = {
1037-
name: name,
1038-
targetScope: scope,
1039-
stopPropagation: function() {stopPropagation = true;},
1040-
preventDefault: function() {
1041-
event.defaultPrevented = true;
1042-
},
1043-
defaultPrevented: false
1044-
},
1045-
listenerArgs = concat([event], arguments, 1),
1052+
globalDefaultPrevented = false,
1053+
event = new Event(scope),
1054+
listenerArgs,
10461055
i, length;
10471056

10481057
do {
10491058
namedListeners = scope.$$listeners[name] || empty;
1050-
event.currentScope = scope;
10511059
for (i=0, length=namedListeners.length; i<length; i++) {
10521060

10531061
// if listeners were deregistered, defragment the array
@@ -1059,6 +1067,8 @@ function $RootScopeProvider(){
10591067
}
10601068
try {
10611069
//allow all listeners attached to the current scope to run
1070+
event = new Event(scope);
1071+
listenerArgs = concat([event], arguments, 1);
10621072
namedListeners[i].apply(null, listenerArgs);
10631073
} catch (e) {
10641074
$exceptionHandler(e);
@@ -1069,7 +1079,6 @@ function $RootScopeProvider(){
10691079
//traverse upwards
10701080
scope = scope.$parent;
10711081
} while (scope);
1072-
10731082
return event;
10741083
},
10751084

@@ -1100,16 +1109,19 @@ function $RootScopeProvider(){
11001109
this.name = name;
11011110
this.currentScope = currentScope;
11021111
this.targetScope = target;
1103-
this.defaultPrevented = false;
1112+
this.defaultPrevented = globalDefaultPrevented;
11041113
}
11051114
Event.prototype.preventDefault = function() {
1115+
// Deals with the current instance (this) and also the next ones with globalDefaultPrevented.
11061116
this.defaultPrevented = true;
1117+
globalDefaultPrevented = true;
11071118
};
11081119

11091120
var target = this,
11101121
current = target,
11111122
next = target,
11121123
listenerArgs,
1124+
globalDefaultPrevented = false,
11131125
event = new Event(current),
11141126
listeners, i, length;
11151127

test/ng/rootScopeSpec.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,10 @@ describe('Scope', function() {
15721572
});
15731573
grandChild.$emit('myEvent');
15741574
expect(event).toBeDefined();
1575+
1576+
// Asynchronous expectation : current & target should be the same as above
1577+
expect(event.targetScope).toBe(grandChild);
1578+
expect(event.currentScope).toBe(child);
15751579
});
15761580

15771581

@@ -1693,6 +1697,18 @@ describe('Scope', function() {
16931697
expect(result.name).toBe('some');
16941698
expect(result.targetScope).toBe(child1);
16951699
});
1700+
1701+
it('should have preventDefault method and defaultPrevented property', function() {
1702+
var event = child2.$broadcast('myEvent');
1703+
expect(event.defaultPrevented).toBe(false);
1704+
1705+
grandChild21.$on('myEvent', function(event) {
1706+
event.preventDefault();
1707+
});
1708+
event = child2.$broadcast('myEvent');
1709+
expect(event.defaultPrevented).toBe(true);
1710+
});
1711+
16961712
});
16971713

16981714

@@ -1704,16 +1720,18 @@ describe('Scope', function() {
17041720
event;
17051721

17061722
child.$on('fooEvent', function(e) {
1723+
expect(e.targetScope).toBe(scope);
1724+
expect(e.currentScope).toBe(child);
1725+
expect(e.name).toBe('fooEvent');
17071726
event = e;
17081727
});
17091728
scope.$broadcast('fooEvent');
17101729

1711-
expect(event.name).toBe('fooEvent');
1730+
// Asynchronous expectation : current & target should be the same as above
17121731
expect(event.targetScope).toBe(scope);
17131732
expect(event.currentScope).toBe(child);
17141733
}));
17151734

1716-
17171735
it('should support passing messages as varargs', inject(function($rootScope) {
17181736
var scope = $rootScope,
17191737
child = scope.$new(),

0 commit comments

Comments
 (0)