diff options
| author | Igor Minar | 2011-08-02 16:33:20 -0700 | 
|---|---|---|
| committer | Igor Minar | 2011-08-24 15:01:50 -0700 | 
| commit | 08a33e7bb377a4d47917dbf5fabbe59b562f1e04 (patch) | |
| tree | 136537a62b5da24bc40ebde75d0862a501247303 /src/Scope.js | |
| parent | 30753cb1310893841fdb0b17c075b6a72e8c8d8a (diff) | |
| download | angular.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.js | 175 | 
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) | 
