in Javascript

Keeping Angular “service” list data in sync with controllers.

Learning Angular has been one of the greatest productivity boosts for rapid application development in my career. However, some of the common strategies implemented can be improved in my opinion.

In AngularJS, there is a great deal of importance placed on separation of concerns. One of the most practiced patterns for holding application state is move this data into angular services or factories. Which one of the former to use is totally a personal preference in my opinion (I opt for factories most of the time).

The purpose of this post is aimed at services that hold collections that update from remote data calls.

Targeted AngularJS version at time of writing: 1.2.19

Too many times, I have run across angular controllers bringing in the $scope service just to $watch a service collection:

The bad:

I know what you are thinking “why doesn’t this just work by default?” We are clearly updating the DataFactory.items.

The reason that angular does not “watch” this value is because when it set’s up the implicit $watch from a view, it references the original array from the DataFactory. But when the data comes back through $http, it replaces the property with a different array reference. Thus Angular can’t watch the collection without adding nasty $watch functions in your controllers.

So why is the $watch a bad thing in the controller?

It adds to the cognitive load needed to understand what is going on in the controller. When we look at code that others (or ourselves 6 months from now) wrote, being able to easily understand the what without spending a lot of time parsing the how is very beneficial. Any bindings in you view cause implicit watches to be set. Also, all  $watch  functions are executed for every  $digest  which may occur many times in a “digest cycle” (angular kicks these off with most interactions). Adding yet another watch is a pattern that may get you into performance issues in the future.

Surely we can remove the dependency on $scope just to watch this collection. Let us look at a very simple example of this separation adapted from Todd Motto.

The better:

Now I’m not hating on this format. Here after every call to update the service data source, both the source and controller reference get updated. It works for what it is intended for and it is fairly easy to understand what is going on. However, it does not really address controllers other than the one calling the service updated being notified.

How about this:

Doesn’t this just read so much cleaner?

Enter angular.copy. What angular.copy does when given a new array and a source array is empty the source (by setting length to 0 i think..) and then repopulate the array with the new array items.

Our end result:

Need a hand?
  • I love that solution. Maybe I am misunderstanding something, but the docs state ”
    If source is identical to ‘destination’ an exception will be thrown.” Does this mean that if the data source remains the same then it won’t update? Or is it talking about object id and I am thinking contents?

  • Sulayman Mohmand

    Thanks Justin for nice tutorial. I have tried to get my customers data but still I get empty customers array. Can you please check what I am doing wrong???
    Thanks

    //My Factory

    app.factory(‘CustomerService’,function($http,$q){
    var customers={};
    customers.all=[];
    customers.getDataStream = function() {

    return $q.when($http.get(“/customers/getCustomersJSON”))
    .then(function(data) {
    angular.copy(data, customers.all);

    });

    };

    return customers;

    });

    //My Controller

    app.controller(‘ReportsCtrl’,function($scope,$http,CustomerService,$timeout){

    $scope.customers=CustomerService.all;

    CustomerService.getDataStream();

    });

    //My View

    {{customers}}

    • Justin Obney

      Sulayman, you do not need to use $q.when. I was simply using it to simulate an ajax call.

      • Sulayman Mohmand

        I removed the $q but still customers.all array doesn’t get updated 🙁

  • Guilherme Cardoso

    Because you’re using this inside the controller i’m not sure if you’re using ControllerAs. In that case i can’t access this inside $watch function like you’re showing. Instead, saving the this to a var.
    var self = this;
    $scope.$watch(watchSource, function(current, previous){
    self.items = current;
    });

    Only this way the controller is updating properly.

  • Nathan Power

    thanks, this helped me out a lot this evening

  • TweetsOfSumit

    Okay man, this is straight up sorcery. Very nice!

    BUT I do have a problem with this:

    Let’s say I want to do something with the service data before it’s being used in different controllers. Ctrl1 is just displaying it while Ctrl2 does some funky stuff and then displays it (differently).

    Ctrl2 can’t re-run the funky stuff as it never knows when the service data changes. I have to use $watch for that. And the problem is, because of angular.copy, $watch has to deep-check for changes (third parameter to true), which degrades performance way more (for large objects/arrays) than just using another watcher in Ctrl1 and don’t use angular.copy.

    So my conclusion is to use your solution whenever I can – but In case I need a $watch task (an onChange event, if you will), I’d opt for all non-deep $watchers regarding that service.

    • Justin Obney

      If you just need to “display” it differently, I would use a filter in the display code.

      • TweetsOfSumit

        I need to wrap each array item into an object and save that as another array. (It’s displaying the geojson part on a map via leaflet).

        I’m still checking if I can solve this another way to make use of your witchery.