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:
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...)
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; });
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; });
$scope.$apply
: you don't have to know whether you are in a digest cycle.$timeout
: you are not really wanting a timeout, and you get the simpler syntax without the extra 0
parameter.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With