76

I have the following service in my app:

uaInProgressApp.factory('uaProgressService', 
    function(uaApiInterface, $timeout, $rootScope){
        var factory = {};
        factory.taskResource = uaApiInterface.taskResource()
        factory.taskList = [];
        factory.cron = undefined;
        factory.updateTaskList = function() {
            factory.taskResource.query(function(data){
                factory.taskList = data;
                $rootScope.$digest
                console.log(factory.taskList);
            });
            factory.cron = $timeout(factory.updateTaskList, 5000);
        }

        factory.startCron = function () {
            factory.cron = $timeout(factory.updateTaskList, 5000);
        }

        factory.stopCron = function (){
            $timeout.cancel(factory.cron);
        }
        return factory;
});

Then I use it in a controller like this:

uaInProgressApp.controller('ua.InProgressController',
    function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
        uaContext.getSession().then(function(){
            uaContext.appName.set('Testing house');
            uaContext.subAppName.set('In progress');
            uaProgressService.startCron();

            $scope.taskList = uaProgressService.taskList;
        });
    }
);

So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.

It remains empty in my template. Do you know how I can propagate these changes ?

Jeroen
  • 60,696
  • 40
  • 206
  • 339
Alex Grs
  • 3,231
  • 5
  • 39
  • 58

6 Answers6

79

While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.

The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.

Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.

You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.

This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.

This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.

Piacenti
  • 1,188
  • 10
  • 9
  • 1
    What is the reason for the 'new' + iife in the return of the service? – lloop Nov 27 '14 at 14:19
  • 1
    In this case it would allow you to return a class, removing the new would break the code. I tend to prefer this structure than the object structure (eg. "{attr:value, attr2:value2...}") since I find it more flexible. [Here](http://plnkr.co/edit/TX6w3elHy1bU92XrCRw3?p=preview) is the plunker for it, and [here](https://docs.angularjs.org/guide/providers) is a little info about it from angular team. As you can see you can use service instead of factory for this but either one works fine. – Piacenti Dec 02 '14 at 15:01
  • 4
    hey @GabrielPiacenti - you have a really good answer here, I think it would help if you cleaned up the content/formatting a bit...make it more readable... – sfletche May 07 '15 at 00:47
  • Gabriel you should delete most of the code and just have the answer focus on what is needed. You are correct, but this code looks over complicated. Look at the 2 answers below, much cleaner (but they lack the info you have about the memory heap). Nicely done either way! – Mark Pieszak - Trilon.io Jan 30 '16 at 16:14
  • I know this is a really old answer, but I've been doing this a ton in my apps and it works like a dream. In Angular 1.5, two way data binding has been removed (mostly), so does this technique fall into the same performance pitfalls that removing two-way data binding set out to solve? – Tyler Sep 09 '16 at 06:28
  • @Tyler I don't think this should have any performance pitfalls by itself. This is just how object references work in JavaScript and actually in Java also. – Piacenti Dec 10 '16 at 00:03
72

Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.

For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:

$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
    if (typeof newVal !== 'undefined') {
        $scope.taskList = uaProgressService.taskList;
    }
});

The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.

musically_ut
  • 34,028
  • 8
  • 94
  • 106
  • 1
    Ok I understand so. I tried your snippet and now my app is working as expected :) Does a lot of watch impact app performance? – Alex Grs Nov 02 '13 at 17:36
  • 1
    Though sometimes [people have had to gone to extreme measures to optimise it](http://blog.scalyr.com/2013/10/31/angularjs-1200ms-to-35ms/), it works reasonably well for most apps. – musically_ut Nov 02 '13 at 17:42
  • 1
    I was blind for so long, thanks. I thought that because it was a singleton the properties would spread in a magical way :S – Dvid Silva Apr 14 '14 at 08:46
  • I thought that was not the answer, but I have forget to also watch for change in my model (I have a Controller->Model->Service System)... so when Service Data changed, Controller did not know and doesn't show the change. Also I did think that $scope.$watch('service.var'... would also cause an update :-/. – Sebastian Jun 27 '14 at 13:46
  • 4
    I took me literally days to find this answer. Thanks. I'm surprised this use case isn't better documented as this is the only reason I use a service (rpc/pubsub). – Senica Gonzalez Dec 17 '14 at 16:35
  • 5
    $watch is a hack in this case. The correct technique is described by the other two answers, specially @Gabriel Piacenti – Bernard Apr 22 '15 at 13:46
  • @Alkaline I would consider that a matter of opinion/design/taste. In this particular case (i.e. when the programmer has control over what the service provides), I too would consider keeping the controller light weight and changing the service to provide a nested dictionary. However, in some cases, the other solutions are not available to the users and adding a watch may be **least-code-to-maintain** fix. – musically_ut Apr 23 '15 at 09:15
  • 1
    $watch is being abused to death by Angular newcomers and the case described is classic. It's actually due to how javascript works. The binding would work if the user would not replace the reference to the array (factory.taskList = data;) but instead modifies factory.taskList (using slice, lodashjs, etc). The updating is done automatically by Angular. No need to add an additional watch. – Bernard Apr 23 '15 at 10:27
45

I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example

angular
  .module("testApp", [])
  .service("myDataService", function(){
    this.dataContainer = {
      valA : "car",
      valB : "bike"
    }
  })
  .controller("testCtrl", [
    "$scope",
    "myDataService",
    function($scope, myDataService){
      $scope.data = function(){
        return myDataService.dataContainer;
      };
  }]);

Then I just bind it in DOM as

<li ng-repeat="(key,value) in data() "></li>

This way you can avoid to using $watch in your code.

Eduard Jacko
  • 1,974
  • 1
  • 16
  • 29
  • 4
    **BEST ANSWER** ignore the $watch answers this is how it should be done. The above has it correct as well, but is extremely confusing. – Mark Pieszak - Trilon.io Jan 30 '16 at 16:12
  • 1
    Sorry to bring up a an old answer, but this solution works perfectly for me and I'm wondering if there are any drawbacks to it that I need to be aware of? I've tried various ways of linking scopes and services but no solution is any where near as elegant as this. – CT14.IT Feb 05 '16 at 15:36
  • 4
    Sorry for late response. This shouldn't be use in cases where your data function is quite complex. Each time when the $digest run, the method will be invoked. – Eduard Jacko Mar 31 '16 at 13:51
  • To resolve that problem use a factory, it returns the value and it updates itself when the data is changed. – Tiago Sousa Apr 20 '16 at 13:27
  • I wish I could thank you more than just saying thanks..!! couldn't find any solution, even watch didn't work for me for some weird reason but this one does its job! THANK YOU! – Kiss Koppány Oct 18 '16 at 19:48
  • late but wondering why when we do console.log($scope.data) it does not get logged, but the DOM element still shows the updated value. Is it because the service is a separate scope from the controller scope? would we need to define data as a ng-model to make console log work? – uniXVanXcel Feb 22 '18 at 21:08
  • @uniXVanXcel not sure what you mean, but if you set log in your js outside of method which is executed under digest/apply then this runs only once. Angular execute $scope.data because is bind to DOM which is recalculated on apply/digest – Eduard Jacko Jul 05 '18 at 08:09
5

No $watch or etc. is required. You can simply define the following

uaInProgressApp.controller('ua.InProgressController',
  function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
    uaContext.getSession().then(function(){
        uaContext.appName.set('Testing house');
        uaContext.subAppName.set('In progress');
        uaProgressService.startCron();
    });

    $scope.getTaskList = function() {
      return uaProgressService.taskList;
    };
  });

Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList

Alexander Elgin
  • 6,796
  • 4
  • 40
  • 50
3

Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.

Something like:

app.controller('YourCtrl'['yourSvc', function(yourSvc){
    yourSvc.awaitUpdate('YourCtrl',function(){
        $scope.someValue = yourSvc.someValue;
    });
}]);

And the service has something like:

app.service('yourSvc', ['$http',function($http){
    var self = this;
    self.notificationSubscribers={};
    self.awaitUpdate=function(key,callback){
        self.notificationSubscribers[key]=callback;
    };
    self.notifySubscribers=function(){
        angular.forEach(self.notificationSubscribers,
            function(callback,key){
                callback();
            });
    };
    $http.get('someUrl').then(
        function(response){
            self.importantData=response.data;
            self.notifySubscribers();
        }
    );
}]);

This can let you fine tune more carefully when your controllers refresh from a service.

Bon
  • 1,083
  • 12
  • 23
  • As a note, I've since found a much more effective solution to this. Use nested routes with Angular UI router and place all the long lived event notifiers in the root controller. – Bon Nov 22 '16 at 17:18
  • 1
    I wanted to be able to place a list of data anywhere, and got inspired by your answer to write a directive that makes use of a service based on your subscribe principle. I think that at directive is a much more simpel solution than nested routes. But I guess it depends... anyway, thank you for providing a very nice and useful piece of code :-) – Jette Nov 24 '16 at 09:48
2

Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.

BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.

The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.

The Service:

app.factory('nlsService', ['$http', function($http) {
  var data = {
    get: {
      ressources        : "gdc.ressources",
      maintenance       : "gdc.mm.maintenance",
      prewarning        : "gdc.mobMaint.prewarning",
    }
  };
// ... asynchron change the data.get = ajaxResult.data... 
  return data;
}]);

Controller and scope expression

app.controller('MenuCtrl', function($scope, nlsService)
  {
    $scope.NLS = nlsService;
  }
);

<div ng-controller="MenuCtrl">
  <span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>

The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.

app.controller('MenuCtrl', function($scope, nlsService)
  {
    $scope.NLS = nlsService.get;
  }
);

<div ng-controller="MenuCtrl">
  <span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>
Ruwen
  • 3,008
  • 1
  • 19
  • 16