0

I am working on a utility service for working with SharePoint items in angular. I am updating my code to use angular promises for the async. communication instead of callback functions so please pardon my code being a bit messy during the transition. I have written some code to update a single list items and then I use that function repeatedly to update multiple items in a batch. The code is working and the http request is changing the items in my list but I can't seem the get the promise to bubble back to the top when updating multiple items. Here is my code:

this.UpdateListItem = function (webUrl, listName, itemId, itemProperties, success, failure) {
    if (typeof lists[listName] === 'undefined') {
        lists[listName] = [];
    }
    var deferred = $q.defer();

    var post = angular.copy(itemProperties);
    DataUtilitySvc.ConvertDatesJson(post);
    this.GetListItemById(webUrl, listName, itemId)
        .then(function (item) {
            $http({
                url: item.__metadata.uri,
                method: 'POST',
                contentType: 'application/json',
                processData: false,
                headers: {
                    "Accept": "application/json;odata=verbose",
                    "X-HTTP-Method": "MERGE",
                    "If-Match": item.__metadata.etag
                },
                data: JSON.stringify(post),
                dataType: "json",
            }).then(function SuccessCB(response) {
                var temp = [];
                temp.push(itemProperties);
                DataUtilitySvc.MergeByProperty(lists[listName], temp, 'Id');
                deferred.resolve(response);
            }, function FailureCB(response) {
                this.GetListItems(webUrl, listName);
                deferred.reject(response);
            });
        }, function (error) {
            deferred.reject(error);
        });
    return deferred.promise;
};


this.UpdateListItems = function (webUrl, listName, itemsJson, success, failure) {
    if (numItems == -1) {
        numItems = itemsJson.length;
        c = 0;
        f = 0;
    }
    var promises = [];

    itemsJson.forEach(function (itemProps) {
        var deferred = $q.defer();
        this.UpdateListItem(webUrl, listName, itemProps.Id, itemProps)
            .then(function () {
                c++;
                if (c == numItems && f == 0) {
                    numItems = -1;
                    deferred.resolve(itemsJson[listName]);
                }
            }, function (error) {
                c++; f++;
                if (c == numItems) {
                    numItems = -1;
                    deferred.reject(error);
                }
            });
        promises.push(deferred.promise);
    }, this);
    return $q.all(promises);
};

And then here is my where I call the service functions from my angular controller. The animateAlert call makes a bootstrap alert appear and then disappear with the specified text.

$scope.UpdateListItem = function (webUrl, listName, itemId, itemProperties, success, failure) {
    SPListUtility.UpdateListItem(webUrl, listName, itemId, itemProperties, success, failure)

        // The following then clause works and the animations show as intended

        .then(function success(data) {
            console.log(JSON.stringify(data));
            $scope.animateAlert("myAlert", "alertText", "Operation Successful!", "success");
        }, function failure(error) {
            console.log("Error " + error.status + ": " + error.statusText);
            $scope.animateAlert("myAlert", "alertText", "Error " + error.status + ": " + error.statusText, "danger");
        });
};

$scope.UpdateListItems = function (webUrl, listName, itemsJson, success, failure) {
    SPListUtility.UpdateListItems(webUrl, listName, itemsJson, success, failure)

        // The following section is what never seems to get called. These animations never show up

        .then(function success() {
            $scope.animateAlert("myAlert", "alertText", "Items Saved", "success");
        }, function failure(error) {
            console.log("Error " + error.status + ": " + error.statusText);
            $scope.animateAlert("myAlert", "alertText", "Error " + error.status + ": " + error.statusText, "danger");
        });
};
BrTkCa
  • 4,703
  • 3
  • 24
  • 45
Nathan Kamenar
  • 824
  • 7
  • 30
  • Your promises are only resolved if `(c == numItems && f == 0)` is true. Otherwise, they're never resolved nor rejected. – JB Nizet Mar 08 '16 at 21:33
  • Avoid the [deferred antipattern](http://stackoverflow.com/q/23803743/1048572)! You can "bubble promises back to the top" by [simply `return`ing them from the `then` callback](http://stackoverflow.com/a/22562045/1048572). – Bergi Mar 09 '16 at 00:35
  • Hi @Bergi, could you perhaps change some of my code from above to demonstrate what you mean. I read the articles you posted but I am having trouble figuring out how to apply it to my situation. Thanks. – Nathan Kamenar Mar 09 '16 at 16:56

2 Answers2

0

I think @Bergi is suggesting I do something like the following:

this.GetListItemById = function (webUrl, listName, itemId, loading) {
    var url = webUrl + "/_vti_bin/listdata.svc/" + listName + "(" + itemId + ")";
    return $http({
        url: url,
        method: 'GET',
        processData: false,
        headers: { "Accept": "application/json;odata=verbose" },
    }).success(function (data) {
        console.log("This Executes");
        return data.d;
    }).error( function (response) {
        return response;
    });
};

This seems to kind of do what I want. It does the asynchronous call but it always returns the entire success/then object with data, status, headers, config, and statusText. Even though I specifically say return data.d it still returns the entire object. Anyone know what I am missing?

Nathan Kamenar
  • 824
  • 7
  • 30
  • That is [to be expected](https://stackoverflow.com/questions/35415659/angularjs-then-behaves-differently-than-success-error) - use `then`/`catch` instead of `success`/`error` (or don't use `catch` at all here, since you're not doing anything about the error) – Bergi Mar 09 '16 at 20:11
0

Thanks for the reply @Bergi. I want to return the response to the user if the request fails. Here is the code in my controller:

$scope.GetListItemById = function (webUrl, listName, itemId) {
    SPListUtility.GetListItemById(webUrl, listName, itemId)
        .then(function success(data) {
            console.log(JSON.stringify(data));
            $scope.animateAlert("myAlert", "alertText", "Item: " + data.Title + " loaded", "success");
        }).catch( function failure(error) {
            console.log("Error " + error.status + ": " + error.statusText);
            $scope.animateAlert("myAlert", "alertText", "Error " + error.status + ": " + error.statusText, "danger");
        });
};

This is all mostly for experimenting and testing now but I want this service to be robust for the future. The then seems to work for items that exist but the problem is that if I try to retrieve an item that doesn't exist and it throws a 404 the then function listed above still gets called when it returns instead of the catch like I intend. I can force it to work by changing the service function as follows by manually returning and Promise.reject but is that following best practices?

this.GetListItemById = function (webUrl, listName, itemId, loading) {
    var url = webUrl + "/_vti_bin/listdata.svc/" + listName + "(" + itemId + ")";
    //var deferred = $q.defer();
    return $http({
        url: url,
        method: 'GET',
        processData: false,
        headers: { "Accept": "application/json;odata=verbose" },
    }).then(function SuccessCB(response) {
        //console.log(JSON.stringify(response));
        return response.data.d;
    }, function FailureCB(response) {
        return Promise.reject(response);
    });
    //return deferred.promise;
};
Nathan Kamenar
  • 824
  • 7
  • 30