javascriptangularjsasynchronousserviceangular-promise

AngularJS : Initialize service with asynchronous data


I have an AngularJS service that I want to initialize with some asynchronous data. Something like this:

myModule.service('MyService', function($http) {
    var myData = null;

    $http.get('data.json').success(function (data) {
        myData = data;
    });

    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

Obviously this won't work because if something tries to call doStuff() before myData gets back I will get a null pointer exception. As far as I can tell from reading some of the other questions asked here and here I have a few options, but none of them seem very clean (perhaps I am missing something):

Setup Service with "run"

When setting up my app do this:

myApp.run(function ($http, MyService) {
    $http.get('data.json').success(function (data) {
        MyService.setData(data);
    });
});

Then my service would look like this:

myModule.service('MyService', function() {
    var myData = null;
    return {
        setData: function (data) {
            myData = data;
        },
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

This works some of the time but if the asynchronous data happens to take longer than it takes for everything to get initialized I get a null pointer exception when I call doStuff()

Use promise objects

This would probably work. The only downside it everywhere I call MyService I will have to know that doStuff() returns a promise and all the code will have to us then to interact with the promise. I would rather just wait until myData is back before loading the my application.

Manual Bootstrap

angular.element(document).ready(function() {
    $.getJSON("data.json", function (data) {
       // can't initialize the data here because the service doesn't exist yet
       angular.bootstrap(document);
       // too late to initialize here because something may have already
       // tried to call doStuff() and would have got a null pointer exception
    });
});

Global Javascript Var I could send my JSON directly to a global Javascript variable:

HTML:

<script type="text/javascript" src="data.js"></script>

data.js:

var dataForMyService = { 
// myData here
};

Then it would be available when initializing MyService:

myModule.service('MyService', function() {
    var myData = dataForMyService;
    return {
        doStuff: function () {
            return myData.getSomeData();
        }
    };
});

This would work too, but then I have a global javascript variable which smells bad.

Are these my only options? Are one of these options better than the others? I know this is a pretty long question, but I wanted to show that I have tried to explore all my options. Any guidance would greatly be appreciated.


Solution

  • Have you had a look at $routeProvider.when('/path',{ resolve:{...}? It can make the promise approach a bit cleaner:

    Expose a promise in your service:

    app.service('MyService', function($http) {
        var myData = null;
    
        var promise = $http.get('data.json').success(function (data) {
          myData = data;
        });
    
        return {
          promise:promise,
          setData: function (data) {
              myData = data;
          },
          doStuff: function () {
              return myData;//.getSomeData();
          }
        };
    });
    

    Add resolve to your route config:

    app.config(function($routeProvider){
      $routeProvider
        .when('/',{controller:'MainCtrl',
        template:'<div>From MyService:<pre>{{data | json}}</pre></div>',
        resolve:{
          'MyServiceData':function(MyService){
            // MyServiceData will also be injectable in your controller, if you don't want this you could create a new promise with the $q service
            return MyService.promise;
          }
        }})
      }):
    

    Your controller won't get instantiated before all dependencies are resolved:

    app.controller('MainCtrl', function($scope,MyService) {
      console.log('Promise is now resolved: '+MyService.doStuff().data)
      $scope.data = MyService.doStuff();
    });
    

    I've made an example at plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview