1

Hitting the ceiling of my Angular knowledge and I have been going around in circles on this.

Basically I have video player and chapter list directives, each with a controller. The controllers use the same model service which looks like this:

  .service('VideoPlayerModel', function(){
      var model = this;
      model.chapters = {
          chapterPos: 0,
          targetChapter:null,
          data: []
      },
      model.getVideoData = function() {
          return model.videoData;
      };
      model.setVideoData = function(vData){
        ...
        ...
        ...
      };
  });

In the video player controller as the time of the player updates it finds the needed chapter data and updates the model.chapters data like this:

   updateChapter: function(currentTime){
    var chapters = VideoPlayerModel.chapters;
    var chaptersCtrl = videoPlayerCtrl.chapters;

    if (chapters.nextChapter.start <= currentTime) {
      chapters.chapterPos = chapters.chapterPos + 1;
      chaptersCtrl.setChapter(); //This finds and sets the Target Chapter
    }
  },

After setChapter runs I call console.log(VideoPlayerModel.chapters) and I can see the data model has updated with a result like this:

Object {chapterPos: 1, targetChapter: Object, data: Array[6], nextChapter: Object}

However the watch in the ChapterListCtrl doesn't fire and any of the onscreen items displaying the ChapterPos still show just the initial val of 0.

The controller looks like this:

.controller("ChapterListCtrl", ['$scope', 'VideoPlayerModel', function($scope, VideoPlayerModel) {
  $scope.chapters = VideoPlayerModel.chapters;

  $scope.$watch(function() { return VideoPlayerModel.chapters; }, function(newValue, oldValue){ 
    $scope.chapters = newValue; 
    console.log("A Change"); // Only runs at initialisation.
  });
}])

I have tried different ways and ended up with this, not sure if I am in the complete wrong direction now. Can anyone please help?

Kevin Mann
  • 721
  • 1
  • 7
  • 14
  • You can use **broadcast()** method to update all controller. – Abhilash Augustine Oct 14 '15 at 11:00
  • When ever I try to use $rootScope as in `$rootScope.$broadcast('scanner-started');` I get the same `Uncaught ReferenceError: $rootScope is not defined` error. I am using angular 1.4 do I have to do something special to get root scope I never seem to have access to it. – Kevin Mann Oct 14 '15 at 11:11
  • Yes, you need to inject `$rootScope` to your service definition. I added this code to my answer below. – Abhilash Augustine Oct 14 '15 at 11:18

3 Answers3

0

You can use $broadcast() and $on() function to achieve your requirement.

$broadcast() will flush an event to all it's child controller. So, you can $broadcast() an event with your new value to all controllers when you set a new value to your shared model.

Add a broadcast method in your shared service.

model.setVideoData = function(vData){
    UpdateYourModel();
    // Inform that your model is updated 
    $rootScope.$broadcast('modelUpdated');
}

And now you can add a listener for the event modelUpdated in all your controllers.

$scope.$on('modelUpdated', function () {
     $scope.controllerModel = VideoPlayerModel.getVideoData(); // Update your controller model
}

And also, inject $rootScope to your service,

.service("VideoPlayerModel", ["$rootScope", function($rootScope){
     // define your service here.
}] );

That's all !!!

I hope this will help you.

Abhilash Augustine
  • 4,128
  • 1
  • 24
  • 24
  • The broadcasting is working but the template value will still not update. This is my listener: `$scope.test = "Hello World"; $scope.$on('modelUpdated', function () { chapterListCtrl.setChapters(); $scope.test = chapterListCtrl.chapters.chapterPos; console.log($scope.test); });` My Template is now just this: `

    {{test}}

    ` - It does change to 0 but then sticks with 0 however the console.log increases correctly through 1,2,3 etc
    – Kevin Mann Oct 14 '15 at 16:09
  • It works if I use: `$scope.$apply( function ( ) { $scope. test = chapterListCtrl.chapters.chapterPos; });` but feel like I shouldn't have to do this and I get Error: [$rootScope:inprog] messages while initialisation. – Kevin Mann Oct 14 '15 at 16:42
  • @KevinMann, You are correct. Do not use `$scope.$apply()` inside your angular code. It is used out side your angular JS. Because, By default, your angular code will call `$scope.$apply` in every digest. – Abhilash Augustine Oct 15 '15 at 03:46
  • @KevinMann, And in your listener function, what are you trying to do? Actually, within your listener function you need to get model from your shared service and update it to your controller model. – Abhilash Augustine Oct 15 '15 at 03:49
  • @KevinMann, I think you are now get value from your own controller itself. But you has to update your model from your service, `$scope.test = YourService.GetValue()` – Abhilash Augustine Oct 15 '15 at 04:05
0

Try changing your watcher to:

$scope.$watch('chapters', function(newValue, oldValue){ 
    $scope.chapters = newValue; 
    console.log("A Change"); // Only runs at initialisation.
});

Alternatively if that doesn't achieve what you want, you can enable a deep watch by passing the third argument:

$scope.$watch('chapters', function(newValue, oldValue){ 
    $scope.chapters = newValue; 
    console.log("A Change"); // Only runs at initialisation.
}, true);

Your watcher doesn't fire because it always returns the same chapters which Angular considers as not-changed because it checks by reference. Your watcher can also be refactored as:

  $scope.$watch(function() { return VideoPlayerModel.chapters.length; }, function(newValue, oldValue){ 
    $scope.chapters = newValue; 
    console.log("A Change"); // Only runs at initialisation.
  });
Jon Snow
  • 3,682
  • 4
  • 30
  • 51
0

You don't need to use $watch, $broadcast or $on. This is best solved by regular JavaScript thinking.

Your problem is $scope.chapters = newValue; That is where you break the binding that your controllers use by introducing a new object unrelated to your service.

What you should to instead is to think about your service model.chapters = {..} and say hey! This is THE ONE object that I will use. And if I want to change the data in this object anywhere, I will switch the data inside the object and NOT assign a new object to the reference I use.

To do this I use the following methods:

    transferDataList = function (from, to) {
        /*
            http://stackoverflow.com/questions/1232040/empty-an-array-in-javascript
        */
        to.length = 0;
        for (var i = 0; i < from.length; i++) { to.push(from[i]); }
    };

    transferDataMap = function (from, to) {
        /*
            http://stackoverflow.com/questions/684575/how-to-quickly-clear-a-javascript-object
        */
        var member;
        for (member in to) { delete to[member]; }
        for (member in from) { to[member] = from[member]; }
    };

And when I want to change the data in my object I DON'T do:

$scope.chapters = newValue;

Instead I do:

transferDataMap(newValue, $scope.chapters);

Or:

transferDataList(newValue, $scope.chapters);

This way you will keep your binding and your Angular interfaces will always be updated.

Per Eriksson
  • 524
  • 1
  • 9
  • 25
  • Hi, I have tired this approach, you are right I should use getter and setters for my model, but it didn't solve the problem, if I put the data in the template, it doesn't update when the Model does still. I link in my controller with `$scope.chapters = VideoPlayerModel.getChapterData( );` but when I set the data, I get no update as you [can see here](http://i.imgur.com/dOgd17O.png) – Kevin Mann Oct 14 '15 at 15:35