Angular communication cheatsheet

I'm learning AngularJS, and sometimes, question arises, like, what is the most appropriate way to exchange data between Angular's entities. Sometimes it's not so obvious at the first sight, so, in this article, I tried to summarize various solutions I've came up with.

Call directive controller's method from another controller

If we talk about calling directive controller's method from other (sub-) directive, then it's not a problem: Angular explicitly supports this kind of communication, see the docs: Creating directives that communicate.

Sometimes, however, I find myself wishing to talk to directive from arbitrary point of the page, not necessarily from the subdirective. There are several ways to accomplish this, and which one fits better depends on the situation.

TODO: talk about doing that through the service http://stackoverflow.com/questions/14883476/angularjs-call-method-in-directive-controller-from-other-controller

TODO: explain dfFileSel's way, like: “For example, see my easyFileSel directive (TODO: implement dragging, publish bower, and so on)”

Listen on service's data from the controller

$watch on service's function

The first way is to define a function (which returns needed value) on the service, and $watch it in the controller.

Assume our service has important data on returned by getData() function:

my_service.js
'use strict';
 
(function(){
  angular.module('app')
  .factory('myService', myService);
 
  function myService(){
 
    var ret = {
      /* ... some other stuff ... */
      getData: getData,
    };
 
    function getData() {
      //-- Angular will pass $scope as the first argument,
      //   but we don't use it in this example
      return 'some value';
    }
 
    /* .... */
 
    return ret;
  }
 
}());

Then, we can watch it in the controller, like this:

my_controller.js
'use strict';
 
(function(){
  angular.module('app')
  .factory('MyController', MyController);
 
  MyController.$inject = [ '$scope', 'myService' ];
  function MyController($scope, myService){
 
    $scope.$watch(
      myService.getData,
      function (newValue) {
        //-- value has changed: do something
        console.log('new value:', newValue);
      }
    );
 
  }
 
}());

The considerable drawback is that this is a very heavy solution: the function getData() is now called at every Angular's digest cycle. This is pretty much often: for example, if you have some two-way bindings on the page, every time you change the model in any way, the function getData() will be called (and it's quite possible that it will be called at least twice for each model change: when Angular detects any change during the digest cycle, it restarts the cycle from the beginning).

As a side note, I must mention that Angular passes $scope object to the watched function, so, it performs the following call: getData($scope). This argument is not used in the example above, but just be aware it's there.

Broadcast event from $rootScope

The second option is to inject $rootScope to the service, and use it to broadcast events. In the example that follows, we assume there is a private service function _setData(newValue), and inner service code calls it whenever value changes. Consider:

my_service.js
'use strict';
 
(function(){
  angular.module('app')
  .factory('myService', myService);
 
  myService.$inject = [ '$rootScope' ];
  function myService($rootScope) {
 
    var ret = {
      /* ... some stuff ... */
    };
 
    /**
     * @private
     *
     * Called from inside the service when our important value
     * changes
     */
    function _setData(newValue) {
      $rootScope.$broadcast('myService:myValueChanged', newValue);
    }
 
    /* other service logic */
 
    return ret;
  }
 
}());

Then, define an event handler in the controller:

my_controller.js
'use strict';
 
(function(){
  angular.module('app')
  .factory('MyController', MyController);
 
  MyController.$inject = [ '$rootScope' ];
  function MyController($rootScope){
 
    $rootScope.$on(
      'myService:myValueChanged',
      function valueChanged(event, newValue) {
        console.log('new value:', newValue);
      }
    );
 
  }
 
}());

This solution is a lot less heavy than the $watch-based one, but I consider even that as an overkill. After all, we probably have just one controller that is interested in value changes, but the event is propagated from $rootScope to each and every scope on the page.

I'd like it to be implemented differently.

Implement service's own events

Finally, we consider solution that requires a bit more work than others we've seen so far, but it is the most efficient one.

Our service's API contains two functions:

  • on(“type”, callback)
  • removeListener(“type”, callback)

And a set of event types. In the example below, we have two event types:

  • “myValueChanged”
  • “someOtherEvent”

Here's the service code:

my_service.js
'use strict';
 
(function(){
  angular.module('app')
  .factory('myService', myService);
 
  function myService() {
 
    //-- for each service's event, we have an array with callbacks
    var callbacks = {
      myValueChanged: [],
      someOtherEvent: [],
    };
 
    /**
     * Public factory API
     */
    var ret = {
      on: on,
      removeListener: removeListener,
      /* ... other service API ... */
    };
 
 
 
    /**
     * Add event listener.
     *
     * @param {string} type
     *    event type
     * @param {function} callback
     *    callback to call when new event occurs
     *
     * @throws {Error} if given event type doesn't exist
     */
    function on(type, callback) {
      _ensureEventExists(type);
 
      callbacks[type].push(callback);
    }
 
    /**
     * Remove event listener that was previously registered
     * with on()
     *
     * @param {string} type
     *    event type
     * @param {function} callback
     *    callback unregister
     *
     * @throws {Error} if given event type doesn't exist
     */
    function removeListener(type, callback) {
      _ensureEventExists(type);
 
      var cb = callbacks[type];
      for (var i = 0; i < cb.length; i++){
        if (cb[i] === callback){
          cb.splice(i, 1);
          break;
        }
      }
    }
 
 
 
 
 
    /**
     * @private
     *
     * Generic emit function, for any type of events: it takes an event
     * type and an arbitrary number of event arguments
     *
     * @param {string} eventType
     *    Type of event to emit
     *
     * @param {mixed} ...
     *    All the rest arguments will be given to each registered
     *    event handler
     *
     * @throws {Error} if given eventType doesn't exist
     */
    function _emit(eventType /*, ... */) {
      //-- self-check: make sure we have such event
      if (!(eventType in callbacks)){
        throw new Error("unknown event type: " + eventType);
      }
 
      //-- get event arguments (all other arguments after eventType)
      //   we must reference Array.prototype.slice explicitly
      //   because 0097rguments0032is not an Array, but an Array-like object.
      var eventArgs = Array.prototype.slice.call(arguments, 1);
 
      //-- call each registered event handler
      for (var i = 0; i < callbacks[eventType].length; i++){
        callbacks[eventType][i].apply(null, eventArgs);
      }
    }
 
    /**
     * @private
     *
     * Ensure given event type exists. If it doesn't exist, exception
     * is thrown. Otherwise, the function does nothing.
     */
    function _ensureEventExists(eventType) {
      if (!(eventType in callbacks)){
        throw new Error("unknown event type: " + eventType);
      }
    }
 
 
 
    /**
     * @private
     *
     * Called from inside the service when our important value
     * changes
     */
    function _setData(newValue) {
      _emit('myValueChanged', newValue);
    }
 
 
    /* ... other service logic ... */
 
    return ret;
  }
 
}());

Now, in the controller, we can subscribe on these events:

my_controller.js
'use strict';
 
(function(){
  angular.module('app')
  .factory('MyController', MyController);
 
  MyController.$inject = [ '$scope', 'myService' ];
  function MyController($scope, myService){
 
    //-- subscribe on myService's events
    myService.on("myValueChanged", _onNewValue);
 
    //-- when the scope is destroyed, remove listener
    $scope.$on('$destroy', function () {
      myService.removeListener("myValueChanged", _onNewValue);
    });
 
 
    /**
     * @private
     *
     * Called whenever myService emits "myValueChanged" event
     */
    function _onNewValue (newValue) {
      console.log('new value:', newValue);
    }
 
  }
 
}());

When we apply this solution, we've done some extra work, but the implementation, on the other hand, doesn't do any extra work: it's much more efficient than other solutions we've considered before.

Discussion

Enter your comment (please, English only). Wiki syntax is allowed:
   ____   ____  ____    _  __     __
  / __/  / __/ / __ \  | |/_/ __ / /
 / _/   / _/  / /_/ / _>  <  / // / 
/___/  /_/    \___\_\/_/|_|  \___/
 
articles/angular_data_exchange_cheatsheet.txt · Last modified: 2015/09/05 11:11 by dfrank
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0