Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS : service $broadcast and $watch not triggering on receiving controller

Tags:

AngularJS noob here, on my path to the Angular Enlightenment :)

Here's the situation:

I have implemented a service 'AudioPlayer' inside my module 'app' and registered like so:

 app.service('AudioPlayer', function($rootScope) {       // ...       this.next = function () {          // loads the next track in the playlist          this.loadTrack(playlist[++playIndex]);           };       this.loadTrack = function(track) {          // ... loads the track and plays it          // broadcast 'trackLoaded' event when done          $rootScope.$broadcast('trackLoaded', track);       };  } 

and here's the 'receiver' controller (mostly for UI / presentation logic)

app.controller('PlayerCtrl', function PlayerCtrl($scope, AudioPlayer) {       // AudioPlayer broadcasts the event when the track is loaded      $scope.$on('trackLoaded', function(event, track) {         // assign the loaded track as the 'current'          $scope.current = track;     });      $scope.next = function() {         AudioPlayer.next();     }; } 

in my views I show the current track info like so:

<div ng-controller="PlayerCtrl">     <button ng-click="next()"></button>     // ...     <p id="info">{{current.title}} by {{current.author}}</p> </div> 

the next() method is defined in the PlayerCtrl, and it simply invokes the same method on the AudioPlayer service.

The problem

This works fine when there is a manual interaction (ie when I click on the next() button) - the flow is the following:

  1. PlayerCtrl intercepts the click and fires its own next() method
  2. which in turn fires the AudioPlayer.next() method
  3. which seeks the next track in the playlist and calls the loadTrack() method
  4. loadTrack() $broadcasts the 'trackLoaded' event (sending out the track itself with it)
  5. the PlayerCtrl listens the broadcast event and assigns the track to the current object
  6. the view updates correctly, showing the current.title and current.author info

However, when the next() method is called from within the AudioService in the 'background' (ie, when the track is over), all the steps from 1 to 5 do happen, but the view doesn't get notified of the change in the PlayerCtrl's 'current' object.

I can see clearly the new track object being assigned in the PlayerCtrl, but it's as if the view doesn't get notified of the change. I'm a noob, and I'm not sure if this is of any help, but what I've tried is adding a $watch expression in the PlayerCtrl

$scope.$watch('current', function(newVal, oldVal) {     console.log('Current changed'); }) 

which gets printed out only during the 'manual' interactions...

Again, like I said, if I add a console.log(current) in the $on listener like so:

$scope.$on('trackLoaded', function(event, track) {     $scope.current = track;     console.log($scope.current); }); 

this gets printed correctly at all times.

What am I doing wrong?

(ps I'm using AudioJS for the HTML5 audio player but I don't think this is the one to blame here...)

like image 364
Pierlo Upitup Avatar asked Oct 17 '12 07:10

Pierlo Upitup


2 Answers

When you have a click event the $scope is updated, without the event you'll need to use $apply

$scope.$apply(function () {     $scope.current = track; }); 
like image 146
Fuzzpedal Avatar answered Oct 02 '22 23:10

Fuzzpedal


As it's not safe to peek into the the digest internals, the easiest way is to use $timeout:

$timeout(function () {     $scope.current = track; }, 0); 

The callback is executed always in the good environment.

EDIT: In fact, the function that should be wrapped in the apply phase is

 this.loadTrack = function(track) {      // ... loads the track and plays it      // broadcast 'trackLoaded' event when done      $timeout(function() { $rootScope.$broadcast('trackLoaded', track); });  }; 

Otherwise the broadcast will get missed.

~~~~~~

Actually, an alternative might be better (at least from a semantic point of view) and it will work equally inside or outside a digest cycle:

$scope.$evalAsync(function (scope) {     scope.current = track; }); 
  • Advantage with respect to $scope.$apply: you don't have to know whether you are in a digest cycle.
  • Advantage with respect to $timeout: you are not really wanting a timeout, and you get the simpler syntax without the extra 0 parameter.
like image 31
rewritten Avatar answered Oct 02 '22 23:10

rewritten