1

I'm trying to write an AngularJS library for Pusher (http://pusher.com) and have run into some problems with my understanding of the digest loop and how it works. I am writing what is essentially an Angular wrapper on top of the Pusher javascript library.

The problem I'm facing is that when a Pusher event is triggered and my app is subscribed to it, it receives the message but doesn't update the scope where the subscription was setup.

I have the following code at the moment:

angular.module('pusher-angular', [])

.provider('PusherService', function () {
  var apiKey = '';
  var initOptions = {};

  this.setOptions = function (options) {
    initOptions = options || initOptions;
    return this;
  };

  this.setToken = function (token) {
    apiKey = token || apiKey;
    return this;
  };

  this.$get = ['$window',
  function ($window) {
    var pusher = new $window.Pusher(apiKey, initOptions);
    return pusher;
  }];

})

.factory('Pusher', ['$rootScope', '$q', 'PusherService', 'PusherEventsService',
  function ($rootScope, $q, PusherService, PusherEventsService) {
    var client = PusherService;

    return {
      subscribe: function (channelName) {
        return client.subscribe(channelName);
      }
    }
  }
]);

.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {

  $scope.items = [];

  var channel = Pusher.subscribe('items')
  channel.bind('new', function(item) {
    console.log(item);
    $scope.items.push(item);
  })
}]);

and in another file that sets the app up:

angular.module('myApp', [
'pusher-angular'
]).
config(['PusherServiceProvider',
  function(PusherServiceProvider) {

    PusherServiceProvider
      .setToken('API KEY')
      .setOptions({});
  }
]);

I've removed some of the code to make it more concise.

In the ItemListController the $scope.items variable doesn't update when a message is received from Pusher.

My question is how can I make it such that when a message is received from Pusher that it then triggers a digest such that the scope updates and the changes are reflected in the DOM?

Edit: I know that I can just wrap the subscribe callback in a $scope.$apply(), but I don't want to have to do that for every callback. Is there a way that I can integrate it with the service?

2 Answers 2

4

On the controller level: Angular doesn't know about the channel.bind event, so you have to kick off the cycle yourself.

All you have to do is call $scope.$digest() after the $scope.items gets updated.

.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {

  $scope.items = [];

  var channel = Pusher.subscribe('items')
  channel.bind('new', function(item) {
    console.log(item);
    $scope.items.push(item);
    $scope.$digest(); // <-- this should be all you need
  })

Pusher Decorator Alternative:

.provider('PusherService', function () {
  var apiKey = '';
  var initOptions = {};

  this.setOptions = function (options) {
    initOptions = options || initOptions;
    return this;
  };

  this.setToken = function (token) {
    apiKey = token || apiKey;
    return this;
  };

  this.$get = ['$window','$rootScope',
  function ($window, $rootScope) {
    var pusher = new $window.Pusher(apiKey, initOptions),
    oldTrigger = pusher.trigger; // <-- save off the old pusher.trigger

    pusher.trigger = function decoratedTrigger() {
        // here we redefine the pusher.trigger to:

        // 1. run the old trigger and save off the result
        var result = oldTrigger.apply(pusher, arguments);

        // 2. kick off the $digest cycle
        $rootScope.$digest();

        // 3. return the result from the the original pusher.trigger
        return result;
    };

    return pusher;
  }];
Sign up to request clarification or add additional context in comments.

7 Comments

Is there a way to do it inside the service as opposed to in the Controller where the actual scope is that needs to be updated?
@hamchapman You can inject $rootScope into the service and call $rootScope.$apply() or $rootScope.$digest(). You should almost always call $apply() to ensure everything is updated properly.
@m59 Where should that be called? When I'm trying it at the moment I'm getting an error that says that there's already an apply or digest action in progress (docs.angularjs.org/error/$rootScope/inprog?p0=$digest)
The problem is that you will need to hook into Pusher's function that actually emits the events. It won't work on the subscribe because that is too early in the series of events. I'm unsure how the Pusher API works though.
@hamchapman I have updated the answer with an example on how to decorate the trigger function to force off a $digest()
|
2

I found that I can do something like this and it works:

  bind: function (eventName, callback) {
    client.bind(eventName, function () {
      callback.call(this, arguments[0]);
      $rootScope.$apply();
    });
  },

  channelBind: function (channelName, eventName, callback) {
    var channel = client.channel(channelName);
    channel.bind(eventName, function() {
      callback.call(this, arguments[0]);
      $rootScope.$apply();
    })
  },

I'm not really happy with this though, and it feels as though there must be something bigger than I'm missing that would make this better.

1 Comment

I had only glanced at the Pusher APIs. I didn't realize most of the events are actually triggered from the server so decorating Pusher.trigger would not work. This looks like it would be more correct than my answer. What are you unhappy about this solution?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.