7

I've created a controller in Angular that looks like this (edited for brevity):

function AppCtrl($scope, $http, $location, $dataService) {
    $scope.projects = $dataService.data.projects;
}

Which correctly loads the $scope.projects promise from my $dataService service.

app.service('$dataService', function($q, $http, $location, $rootScope) {
    var dataService = this; //Provides access 'this' inside functions below
    var projectsDeferred = $q.defer();

    $http.get('/api').success(function(data, status, headers, config) {
        projectsDeferred.resolve(data.projects);
    }).error(function(err) {
        projectsDeferred.reject(err);
    });

    this.data = {projects: projectsDeferred.promise};

    //UPDATE FUNCTION
    function updateObjectInArray(array, object, newData) {
        for(i in array) {
            if(array[i] == object) {
                if(newData != undefined) {
                    array[i] = newData;
                } else {
                    return array[i];
                }
            }
        }
        return undefined;
    }


    this.updateProject = function(project, updateData) {
        $http.put('/api/projects/' + project._id, updateData)
            .success(function(data, status, headers, config) {
            updateObjectInArray(dataService.data.projects.$$v, project, data);
        }).error(function(data, status, headers, config) {});
    };

});

I've created another controller that looks like this, that selects a single project from the array of projects based on the current URL:

function ProjectCtrl($scope, $route) {
    //Getting the current project from the array of projects
    $scope.project = $scope.projects.then(function(projects) {
        for(i in projects) {
            if(projects[i]._id == $route.current.params.projectId) {
                return projects[i];
            }
        }
    });
}

When I try to run my updateObjectInArray() function (on the success of my $http.put() request), my $scope.projects in AppCtrl is correctly updated (the array of projects) but my $scope.project in ProjectCtrl is not updated. I can log array[i] inside the updateObjectInArray() function and it will log exactly what I expect, and I can log $scope.projects in AppCtrl and it will update accordingly, but when I try to log $scope.project in my ProjectCtrl controller, it isn't updated accordingly.

I thought the reason was because I had to call $rootScope.$apply() or $rootScope.$digest() after I updated the array[i] object in updateObjectInArray(), however, I get the error that $digest is already in progress.

What do I need to do to make sure my $scope.project item in the array gets updated in my ProjectCtrl? Do I need to resolve a new promise for it?

winduptoy
  • 5,366
  • 11
  • 49
  • 67
  • Where do you call updateProject? Are you sure it is before resolving $dataService.data.projects? Try to add console.logs in several functions – asgoth Jan 14 '13 at 05:36
  • No, I want to call it after resolving `$dataService.data.projects`. The intended function is that it pulls data from the server when the page first loads, and a user can perform an action that will send the update to the server, and if successful, update the scope with the new data returned from the server. – winduptoy Jan 14 '13 at 05:44
  • I meant after :) Too early still. – asgoth Jan 14 '13 at 05:45
  • I call it in my controller when the user clicks a button. Yes, I'm positive it has already resolved. As I said, I can log `array[i]` after updating the object and it has been updated exactly as I expect, but logging my scope shows that it hasn't been updated to what `$dataService.projects` now equals. – winduptoy Jan 14 '13 at 05:48
  • (I meant to type `$dataService.data.projects`) – winduptoy Jan 14 '13 at 05:57

2 Answers2

14

As soon as the projects are loaded, you're using one of the items from the array projects[i], let's theoretically assume it's the object 0x1 in memory. The problem is that when a new item is loaded (let's assume 0x2), and you update the array, you're changing the object that resides in the array position (the 0x1), but $scope.project still references the now abandoned object 0x1. You can't just change it in the array, you have to modify it, so you don't need to rebind the $scope.project variable.

The best solution is to change array[i] = newData to angular.extend(array[i], newData). This way, you gonna just change all properties of array[i], and as your $scope.project points to the same object in memory it will be updated.

Anyway, I also believe you may want to change if(array[i] == object) { to if(array[i]._id === object._id) { and your equality comparisons (==) to strictly equality comparisons (===). This if suits better your cause, as the new created object is not the same as the old one.

Caio Cunha
  • 23,326
  • 6
  • 78
  • 74
  • Excellent answer! Thank you so much! I've never heard of `angular.extend()` before, but it works great. Is this more efficient than calling `$scope.watch('projects', ...` because calling `$watch` means that angular has to do more work? Great suggestion on the strict equality, I'll be using that. – winduptoy Jan 15 '13 at 16:47
  • It's probably best for performance, because you will only merge the objects the moment it changes. Every registered `$watch` runs on __any__ scope digest cycle. You have to pay attention to where you use it, especially when you need a recursive `$watch`, that would be your case. `angular.extends` is pretty much based on [`jQuery.extend`](http://api.jquery.com/jQuery.extend/). Anytime. – Caio Cunha Jan 15 '13 at 19:05
  • Dude you can't believe the good you have done. I had it all mixed up in my mind with binding and extending variables. Thanks a lot for this and clarifying my situation. Million thanks. – Jimmy Kane Jul 16 '13 at 21:48
  • Problem to me seems, old properties will not be cleared if the new object doesn't hold the value to override? – beku8 May 20 '14 at 11:22
3

Are you using the $scope.projects in a binding? Or somewhere else in the code?

Asking because you're returning, and a promise is never changed by it's result, although you can bind promises directly to the HTML expecting it's contents. This happens because, internally, Angular seems to watch for it's result.

So, if you're using this variable somewhere else, then you need to use $q.when($scope.projects). You will get a new promise that will be resolved instantly if it has already been resolved.

Caio Cunha
  • 23,326
  • 6
  • 78
  • 74
  • I believe I have isolated the problem down to the fact that I'm pulling it out of an array. It looks like I simplified my code a little too much. I've updated my question accordingly. See, in `AppCtrl`, the `$scope.projects` is always updated correctly without any additional work. But, when I try to pull a single item out of that array (in `ProjectCtrl`, it isn't updated. Yes, by the way, I am binding it to the HTML and it works just fine when I pass the whole array. Thank you so much for your help, you're awesome. – winduptoy Jan 14 '13 at 17:05
  • See my answer below. I've created a new one because it was too big for comment. – Caio Cunha Jan 15 '13 at 11:59