Josh Beam
Josh Beam

Reputation: 19772

How do I keep track of events in AngularJS?

I have an app that has items, and you can do things like add new items, update the text of an item, move the item to a different folder, etc.

I have an items factory that holds all the items as plain objects inside an array, and the factory returns a singleton that has various methods, like get(), set(), etc.

To add some context to the question, I'm working with Node.js and MongoDB as well.

Anyway, due to all the various factories I have, like items, folders, and all the various controllers for different views, I am relying heavily on events. To give some examples:

// items factory
update: function(params) {
    // add to database, then...
    .then(function() {
        $rootScope.$emit('itemCreated');
    });
}

// items controller

// I need to refresh the items list in the scope
$rootScope.$on('itemCreated', function() { // when an item is added to the database
    $scope.items = items.getAll(); // retrieve all items from the items factory
});

These are their own kind of "subset" of events, in that they all pertain to "CRUD" operations on items.

But, I also have other events that I use. For example, I have an interceptor that listens to any requests and responses. I have a loading widget (an image of a spinning wheel) that uses a directive. This directive will show the loading widget when a request begins, and hide the loading widget when a request ends. This is also event based.

// on request
$rootScope.$emit(_START_REQUEST_);

// on any response
$rootScope.$emit(_END_REQUEST_);

I attempted to "modularize" these request and response events by simply making them constants.

.constant('_START_REQUEST_', '_START_REQUEST_');

I am trying to find a solution in order to "modularize" all my other events, like the events emitted on CRUD operations for items. One idea I've had is to define all of the item CRUD events inside the items factory:

events: {
    update: 'itemUpdate',
    create: 'itemCreated'
    // etc.
}

Then, I can simply inject my items factory into a controller, and reference events like so:

$rootScope.$on(items.events.update, function() {});

I also considered simply defining all events, regardless of whether they are interceptor events or item events, as constants in my app. However, it seemed like this solution directly coupled item events to the module itself, rather than to the items factory, which is where I feel they "belong".

Basically, the issue is that right now all my events definitions seem to be scattered around. My question is: what pattern or best practice would you recommend for modularizing and defining events in AngularJS?

Upvotes: 5

Views: 2710

Answers (4)

Hamid Tavakoli
Hamid Tavakoli

Reputation: 4647

You can use the following:

app.config(function($provide) {
    $provide.decorator("$rootScope", function($delegate) {
        var Scope = $delegate.constructor;
        var origBroadcast = Scope.prototype.$broadcast;
        var origEmit = Scope.prototype.$emit;

        Scope.prototype.$broadcast = function() {
            console.log("$broadcast was called on $scope " + Scope.$id + " with arguments:",
                arguments);
            return origBroadcast.apply(this, arguments);
        };
        Scope.prototype.$emit = function() {
            console.log("$emit was called on $scope " + Scope.$id + " with arguments:",
                arguments);
            return origEmit.apply(this, arguments);
        };
        return $delegate;
    });
})

example: http://plnkr.co/edit/cn3MZynbpTYIcKUWmsBi?p=preview

src: https://github.com/angular/angular.js/issues/6043

Upvotes: 0

adrobisch
adrobisch

Reputation: 336

I agree that these item events should belong to the event source. You could implement a observer pattern in the item factory that hides the dependency on $rootScope for event listeners. This way the event key itself is a private detail of the item factory, and the subscription to the event is made explicit by calling a dedicated function for it. This approach makes your code more independent of $rootScope and easier to maintain than an event name convention (thinking about usages search for the specific event subscription method vs. usages of $rootScope.$emit / $on):

angular.module('events', [])

.service('items', ['$rootScope', function($rootScope) {
  var createdEventKey = 'item.created';
    
  return {
      create: function () {
          $rootScope.$emit(createdEventKey, {"name": "aItemName"});
      },
      
      onCreated: function(callback, scope) {
          var unsubscribeFunction = $rootScope.$on(createdEventKey, function(event, payload) {
              callback(payload);
          });
          
          // allow to unsubscribe automatically on scope destroy to prevent memory leaks 
          if (scope) {
            scope.$on("$destroy", unsubscribeFunction);
          }
          
          return unsubscribeFunction;
      }   
  }
}])

.controller('TestController', function($scope, items) {
    items.onCreated(function (item) {
        console.log("Created: " + item.name);
    }, $scope);
});

complete example: http://jsfiddle.net/8LtyB/32/

Upvotes: 4

pje
pje

Reputation: 2468

If all you want is a way to create a separate object for containing the names of events, why not use a service?

myApp.service('itemEvents', function () {
  var events = {
    update: 'itemupdate',
    create: 'itemcreate',
    ...
  };
  return events;
});

This is essentially what you had before when you were suggesting using a factory to contain the event definitions, except that a service is a single object instance, and is instantiated at module start-up. In contrast, a factory creates a new instance when injected into a controller. (Here's a good SO post on the difference between services and factories)

You can inject this service into your controllers or directives:

myApp.controller('ItemController', function ($scope, itemEvents) {
  $scope.on(itemEvents.update, function () { /* something interesting */ });
});

This gives you a nice place to centralize your event name definitions. As a side note, some people hold to the convention of using all lowercase when defining event names (so itemupdate instead of itemUpdate). Hope this helps!

Upvotes: 0

Delta
Delta

Reputation: 851

assuming these $scope.$emit works like jquery events I would suggest you name your emits to be generic for example in you database update simply do this:

$rootScope.$emit('Created')

then in your items controller do this :

$rootScope.$on('Created.item', function() { // when an item is added to the database
    $scope.items = items.getAll(); // retrieve all items from the items factory
});

then you can wire to the created event in any of your controllers and its name is generic. The .item should add a namespace. if you make all of your events in your items controller have the .item name space you should be able to do a

$rootScope.$off('item')

This will clear up memory leaks

Upvotes: -1

Related Questions