aboutsummaryrefslogtreecommitdiffstats
path: root/src/Scope.js
diff options
context:
space:
mode:
authorIgor Minar2011-08-02 16:33:20 -0700
committerIgor Minar2011-08-24 15:01:50 -0700
commit08a33e7bb377a4d47917dbf5fabbe59b562f1e04 (patch)
tree136537a62b5da24bc40ebde75d0862a501247303 /src/Scope.js
parent30753cb1310893841fdb0b17c075b6a72e8c8d8a (diff)
downloadangular.js-08a33e7bb377a4d47917dbf5fabbe59b562f1e04.tar.bz2
feat(scope): support for events
- 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
Diffstat (limited to 'src/Scope.js')
-rw-r--r--src/Scope.js175
1 files changed, 172 insertions, 3 deletions
diff --git a/src/Scope.js b/src/Scope.js
index e978b659..12ac2833 100644
--- a/src/Scope.js
+++ b/src/Scope.js
@@ -99,6 +99,7 @@ function Scope() {
this.$destructor = noop;
this['this'] = this.$root = this;
this.$$asyncQueue = [];
+ this.$$listeners = {};
}
/**
@@ -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.
*
@@ -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 = [];
@@ -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;
@@ -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
@@ -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)