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

Commit

Permalink
feat(scope): support for events
Browse files Browse the repository at this point in the history
- register listeners with $on
- remove listeners with $removeListener
- fire event that bubbles to root with $emit
- fire event that propagates to all child scopes with $broadcast
  • Loading branch information
IgorMinar committed Aug 24, 2011
1 parent 30753cb commit 08a33e7
Show file tree
Hide file tree
Showing 2 changed files with 451 additions and 77 deletions.
175 changes: 172 additions & 3 deletions src/Scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function Scope() {
this.$destructor = noop;
this['this'] = this.$root = this;
this.$$asyncQueue = [];
this.$$listeners = {};
}

/**
Expand Down Expand Up @@ -155,8 +156,8 @@ Scope.prototype = {
* the scope and its child scopes to be permanently detached from the parent and thus stop
* participating in model change detection and listener notification by invoking.
*
* @param {function()=} constructor Constructor function which the scope should behave as.
* @param {curryArguments=} ... Any additional arguments which are curried into the constructor.
* @param {function()=} Class Constructor function which the scope should be applied to the scope.
* @param {...*} curryArguments Any additional arguments which are curried into the constructor.
* See {@link guide/dev_guide.di dependency injection}.
* @returns {Object} The newly created child scope.
*
Expand All @@ -169,6 +170,7 @@ Scope.prototype = {
Child.prototype = this;
child = new Child();
child['this'] = child;
child.$$listeners = {};
child.$parent = this;
child.$id = nextUid();
child.$$asyncQueue = [];
Expand Down Expand Up @@ -392,12 +394,15 @@ Scope.prototype = {
* scope and its children. Removal also implies that the current scope is eligible for garbage
* collection.
*
* The destructing scope emits an `$destroy` {@link angular.scope.$emit event}.
*
* The `$destroy()` is usually used by directives such as
* {@link angular.widget.@ng:repeat ng:repeat} for managing the unrolling of the loop.
*
*/
$destroy: function() {
if (this.$root == this) return; // we can't remove the root node;
this.$emit('$destroy');
var parent = this.$parent;

if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
Expand All @@ -413,7 +418,7 @@ Scope.prototype = {
* @function
*
* @description
* Executes the expression on the current scope returning the result. Any exceptions in the
* Executes the `expression` on the current scope returning the result. Any exceptions in the
* expression are propagated (uncaught). This is useful when evaluating engular expressions.
*
* # Example
Expand Down Expand Up @@ -520,9 +525,173 @@ Scope.prototype = {
} finally {
this.$root.$digest();
}
},

/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$on
* @function
*
* @description
* Listen on events of a given type. See {@link angular.scope.$emit $emit} for discussion of
* event life cycle.
*
* @param {string} name Event name to listen on.
* @param {function(event)} listener Function to call when the event is emitted.
*
* The event listener function format is: `function(event)`. The `event` object passed into the
* listener has the following attributes
* - `targetScope` - {Scope}: the scope on which the event was `$emit`-ed or `$broadcast`-ed.
* - `currentScope` - {Scope}: the current scope which is handling the event.
* - `name` - {string}: Name of the event.
* - `cancel` - {function=}: calling `cancel` function will cancel further event propagation
* (available only for events that were `$emit`-ed).
*/
$on: function(name, listener) {
var namedListeners = this.$$listeners[name];
if (!namedListeners) {
this.$$listeners[name] = namedListeners = [];
}
namedListeners.push(listener);
},

/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$removeListener
* @function
*
* @description
* Remove the on listener registered by {@link angular.scope.$on $on}.
*
* @param {string} name Event name to remove on.
* @param {function} listener Function to remove.
*/
$removeListener: function(name, listener) {
var namedListeners = this.$$listeners[name];
var i;
if (namedListeners) {
i = namedListeners.indexOf(listener);
namedListeners.splice(i, 1);
}
},

/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$emit
* @function
*
* @description
* Dispatches an event `name` upwards through the scope hierarchy notifying the
* registered {@link angular.scope.$on} listeners.
*
* The event life cycle starts at the scope on which `$emit` was called. All
* {@link angular.scope.$on listeners} listening for `name` event on this scope get notified.
* Afterwards, the event traverses upwards toward the root scope and calls all registered
* listeners along the way. The event will stop propagating if one of the listeners cancels it.
*
* Any exception emmited from the {@link angular.scope.$on listeners} will be passed
* onto the {@link angular.service.$exceptionHandler $exceptionHandler} service.
*
* @param {string} name Event name to emit.
* @param {...*} args Optional set of arguments which will be passed onto the event listeners.
*/
$emit: function(name, args) {
var empty = [],
namedListeners,
canceled = false,
scope = this,
event = {
name: name,
targetScope: scope,
cancel: function(){canceled = true;}
},
listenerArgs = concat([event], arguments, 1),
i, length;

do {
namedListeners = scope.$$listeners[name] || empty;
event.currentScope = scope;
for (i=0, length=namedListeners.length; i<length; i++) {
try {
namedListeners[i].apply(null, listenerArgs);
if (canceled) return;
} catch (e) {
scope.$service('$exceptionHandler')(e);
}
}
//traverse upwards
scope = scope.$parent;
} while (scope);
},


/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$broadcast
* @function
*
* @description
* Dispatches an event `name` downwards to all child scopes (and their children) notifying the
* registered {@link angular.scope.$on} listeners.
*
* The event life cycle starts at the scope on which `$broadcast` was called. All
* {@link angular.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.
*
* Any exception emmited from the {@link angular.scope.$on listeners} will be passed
* onto the {@link angular.service.$exceptionHandler $exceptionHandler} service.
*
* @param {string} name Event name to emit.
* @param {...*} args Optional set of arguments which will be passed onto the event listeners.
*/
$broadcast: function(name, args) {
var targetScope = this,
currentScope = targetScope,
nextScope = targetScope,
event = { name: name,
targetScope: targetScope },
listenerArgs = concat([event], arguments, 1);

//down while you can, then up and next sibling or up and next sibling until back at root
do {
currentScope = nextScope;
event.currentScope = currentScope;
forEach(currentScope.$$listeners[name], function(listener) {
try {
listener.apply(null, listenerArgs);
} catch(e) {
currentScope.$service('$exceptionHandler')(e);
}
});

// down or to the right!
nextScope = currentScope.$$childHead || currentScope.$$nextSibling;

if (nextScope) {
// found child or sibling
continue;
}

// we have to restore nextScope and go up!
nextScope = currentScope;

while (!nextScope.$$nextSibling && (nextScope != targetScope)) {
nextScope = nextScope.$parent;
}

if (nextScope != targetScope) {
nextScope = nextScope.$$nextSibling;
}
} while (nextScope != targetScope);
}
};


function compileToFn(exp, name) {
var fn = isString(exp)
? expressionCompile(exp)
Expand Down
Loading

0 comments on commit 08a33e7

Please sign in to comment.