Search code examples
javascriptangularjsangularjs-scopeangularjs-service

Data-binding from external HTTP API wrapped in a service does not work


I have a problem binding data from an external HTTP API. I tried to follow best practices and tried several approaches (reading the Internet and SO), but the "magic" of data-binding doesn't work and I can't find/understand why. Here's the code :

angular.module('myApp', [])
    .service('externalAPI', ['$q', function($q) {
        var data = [], deferred = $q.defer();

        // The HTTP calls are made through the provided SDK lib
        // It uses promises too
        var pullData = function() {
            externalLib.get('somePath').then(function(newData) {
                // I expect this assignment to help the data-binding
                data = newData
                deferred.resolve(newData)
            })
            return deferred.promise;
        };

        return {
            data: data,
            pullData: pullData
        }
    }])
    .controller('myController', ['$scope', 'externalAPI', function($scope, externalAPI) {
        // Here I tried a lot of approaches to make the "magic" of databinding works, with no luck

        // 1. Bind the whole service, so that externalAPI.data is always updated in templates
        $scope.externalAPI = externalAPI;
        externalAPI.pullData()

        // 2. Bind and expose just the data
        $scope.data = externalAPI.data;
        externalAPI.pullData()

        // 3. Do #1 + $watch the whole externalAPI object
        // Note: I put console.log() calls inside the first function, it always return the initial version
        $scope.externalAPI = externalAPI;
        $scope.$watch(function() { return externalAPI }, function(newVersion) {
            $scope.externalAPI = newValue;
        });
        externalAPI.pullData()

        // 4. Do #2 + $watch just the data of the externalAPI
        $scope.data = externalAPI.data;
        $scope.$watch(function() { return externalAPI.data }, function(newData) {
            $scope.data = newData;
        });
        externalAPI.pullData()

        // 5. Only thing that works fine : manually wire $scope data from the call
        $scope.data = externalAPI.data
        externalAPI.pullData().then(function(newData) {
            $scope.data = newData
        })
    }])

Manually wiring as in #5 defeats the purpose of data-binding, since I will be using the externalAPI service and pulling data from other parts of the app, and I want this data to always be fresh and reflected in the UI. What did I do wrong ?

Note: I'm using Angularsjs 1.2.5, no jQuery or other js lib, just the SDK lib to access data (I have to, it does several unrelated but necessary things in the background).


Solution

  • Beware : since 1.2.0 Angular promises are not automatically unwrapped in the templates. So you can't directly display a promise in the template anymore. this is due to this breaking change: https://github.com/angular/angular.js/commit/5dc35b527b3c99f6544b8cb52e93c6510d3ac577

    This change has been made to remove some 'black magic' and keep a symmetrical behaviour in both templates or regular javascript.

    So your pullData method should update service.data directly, then you can use externalAPI.data as in your #1.

    The problem in your actual #1 is that you update the data variable but service.data still points to the old value.

    You can use this pattern in your service instead :

    function pullData() {
        service.data = 'newValue';
    }
    var service = {
      data: null,
      pullData:pullData
    }
    return service;
    

    So now we update service.data directly, not another variable, and your templates should automagically update :)

    Another point, if for some reason you use a 3rd party lib and not the builtin $http, then you need to kick a $digest cycle manually, and you can do so wrapping your callback code in a scope.$apply(). $apply is prefered to $digest as it handles some corner cases and catch exceptions:

    function pullData() {
        // this comes from an external lib so we must
        // manually tell angular we've modified some data
        $rootScope.$apply(function() {
            service.data = 'newValue';
        });
    }