AngularJS : Initialize service with asynchronous data
Asked Answered
D

10

487

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.

Disciplinary answered 29/4, 2013 at 19:25 Comment(1)
angular - bootstrap asynchronously walks through code to pull data from a server with $http, then save data in a service, then bootstrap an app.Demon
F
329

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

Fiveandten answered 29/4, 2013 at 21:23 Comment(11)
Thank you so much for your response! It would work for me if I didn't already have a service in the resolve map that uses MyService. I updated your plunker with my situation: plnkr.co/edit/465Cupaf5mtxljCl5NuF?p=preview. Is there any way make MyOtherService wait for MyService to get initialized?Disciplinary
I guess I would chain the promises in MyOtherService - I've updated the plunker with chaining and some comments - how does this look? plnkr.co/edit/Z7dWVNA9P44Q72sLiPjW?p=previewFiveandten
I tried this out and still ran into some issues because I have directives and other controllers (the controller I use with $routeProvider is handling the a primary, secondary navigation stuff...that is 'MyOtherService') that need to wait until 'MyService' is resolved. I will keep trying and update this with any success I have. I just wish there was a hook in angular where I could wait for data to return before initializing my controllers and directives. Thanks again for your help. If I had a main controller that wrapped everything this would have worked.Disciplinary
A question here - how will you assign the resolve property to a controller that is not mentioned in $routeProvider. For example, <div ng-controller="IndexCtrl"></div>. Here, the controller is explicitly mentioned and not loaded via routing. In such a case, how would one delay instantiation of the controller then?Rationale
There's a catch with this approach though. If any one of the dependencies defined in resolve is rejected, the view doesn't get updated (that's what we want) but now the URL in the browser's address bar gets updated to the new path (weird). In short, at this point, the view and the url are out-of-sync. This would lead to several problems. For more details, refer to the open issue, Issue 2100Bedder
Ummm what if you aren't using routing? This is almost like saying you can't write an angular app with asynchronous data unless you use routing. The recommended way to get data into an app is to load it asynchronously, but as soon as you have more than one controller and you throw in services, BOOM it's impossible.Donavon
How do you do something like this without using routes? I'm trying to this on my navigation, but it is above the routes.Glendaglenden
Why do yo have to return MyService.promise? Is it useful for some reason? because I didn't see you are using it anywhere. If I just wanted it to load the service without return something from resolve, it would work but bizarre. What should I do in that case?Grisby
Would anybody be able to tell me why .data is called on this line? I am getting problems when using this. console.log('Promise is now resolved: '+MyService.doStuff().data)Chak
Can you elaborate on your comment '... if you don't want this you could create a new promise with the $q service'? How would this be done, what would be the benefit? I merely want to wait until some objects are inserted into the rootscope, hence I'm not even interested in the return value of the promise, just that it's execution is complete.Chukchi
An issue with the resolve approach, is that if you call $state.reload at any point in your controller, the resolve function will execute again. So if you're loading once-off data when your app starts, but then reloading the state because you want to update the page, then you'll end up reloading the once-off data...Mayhem
E
89

Based on Martin Atkins' solution, here is a complete, concise pure-Angular solution:

(function() {
  var initInjector = angular.injector(['ng']);
  var $http = initInjector.get('$http');
  $http.get('/config.json').then(
    function (response) {
      angular.module('config', []).constant('CONFIG', response.data);

      angular.element(document).ready(function() {
          angular.bootstrap(document, ['myApp']);
        });
    }
  );
})();

This solution uses a self-executing anonymous function to get the $http service, request the config, and inject it into a constant called CONFIG when it becomes available.

Once completely, we wait until the document is ready and then bootstrap the Angular app.

This is a slight enhancement over Martin's solution, which deferred fetching the config until after the document is ready. As far as I know, there is no reason to delay the $http call for that.

Unit Testing

Note: I have discovered this solution does not work well when unit-testing when the code is included in your app.js file. The reason for this is that the above code runs immediately when the JS file is loaded. This means the test framework (Jasmine in my case) doesn't have a chance to provide a mock implementation of $http.

My solution, which I'm not completely satisfied with, was to move this code to our index.html file, so the Grunt/Karma/Jasmine unit test infrastructure does not see it.

Ediva answered 17/1, 2014 at 15:4 Comment(4)
Rule such as 'don't pollute the global scope' should be followed only to the extent they make our code better (less complex, more maintainable, more secure etc). I can't see how this solution is better than simply loading the data into single global variable. What am I missing?Carefree
It allows you to use Angular's dependency injection system to access the 'CONFIG' constant in modules that need it, but you don't risk clobbering other modules that don't. For example, if you used a global 'config' variable, there is a chance other 3rd party code might also be looking for the same variable.Ediva
I'm an angular newbie, here are some notes on how I got the config module dependency resolved in my app: gist.github.com/dsulli99/0be3e80db9b21ce7b989 ref: tutorials.jenkov.com/angularjs/… Thank you for this solution.Chak
It is mentioned in a comment in one of the other manual bootstrap solutions below but as an angular newbie who didn't spot it can I just point out that you need to delete your ng-app directive in your html code for this to work properly - it is replacing the automatic bootstrap (via ng-app) with this manual method. If you don't take the ng-app out, the application may actually work but you will see various unknown providers errors in the console.Centaur
B
49

I used a similar approach to the one described by @XMLilley but wanted to have the ability to use AngularJS services like $http to load the configuration and do further initialization without the use of low level APIs or jQuery.

Using resolve on routes was also not an option because I needed the values to be available as constants when my app is started, even in module.config() blocks.

I created a small AngularJS app that loads the config, sets them as constants on the actual app and bootstraps it.

// define the module of your app
angular.module('MyApp', []);

// define the module of the bootstrap app
var bootstrapModule = angular.module('bootstrapModule', []);

// the bootstrapper service loads the config and bootstraps the specified app
bootstrapModule.factory('bootstrapper', function ($http, $log, $q) {
  return {
    bootstrap: function (appName) {
      var deferred = $q.defer();

      $http.get('/some/url')
        .success(function (config) {
          // set all returned values as constants on the app...
          var myApp = angular.module(appName);
          angular.forEach(config, function(value, key){
            myApp.constant(key, value);
          });
          // ...and bootstrap the actual app.
          angular.bootstrap(document, [appName]);
          deferred.resolve();
        })
        .error(function () {
          $log.warn('Could not initialize application, configuration could not be loaded.');
          deferred.reject();
        });

      return deferred.promise;
    }
  };
});

// create a div which is used as the root of the bootstrap app
var appContainer = document.createElement('div');

// in run() function you can now use the bootstrapper service and shutdown the bootstrapping app after initialization of your actual app
bootstrapModule.run(function (bootstrapper) {

  bootstrapper.bootstrap('MyApp').then(function () {
    // removing the container will destroy the bootstrap app
    appContainer.remove();
  });

});

// make sure the DOM is fully loaded before bootstrapping.
angular.element(document).ready(function() {
  angular.bootstrap(appContainer, ['bootstrapModule']);
});

See it in action (using $timeout instead of $http) here: http://plnkr.co/edit/FYznxP3xe8dxzwxs37hi?p=preview

UPDATE

I would recommend to use the approach described below by Martin Atkins and JBCP.

UPDATE 2

Because I needed it in multiple projects, I just released a bower module that takes care of this: https://github.com/philippd/angular-deferred-bootstrap

Example that loads data from the back-end and sets a constant called APP_CONFIG on the AngularJS module:

deferredBootstrapper.bootstrap({
  element: document.body,
  module: 'MyApp',
  resolve: {
    APP_CONFIG: function ($http) {
      return $http.get('/api/demo-config');
    }
  }
});
Billfish answered 9/11, 2013 at 1:27 Comment(1)
deferredBootstrapper is the way to goOutbid
O
44

The "manual bootstrap" case can gain access to Angular services by manually creating an injector before bootstrap. This initial injector will stand alone (not be attached to any elements) and include only a subset of the modules that are loaded. If all you need is core Angular services, it's sufficient to just load ng, like this:

angular.element(document).ready(
    function() {
        var initInjector = angular.injector(['ng']);
        var $http = initInjector.get('$http');
        $http.get('/config.json').then(
            function (response) {
               var config = response.data;
               // Add additional services/constants/variables to your app,
               // and then finally bootstrap it:
               angular.bootstrap(document, ['myApp']);
            }
        );
    }
);

You can, for example, use the module.constant mechanism to make data available to your app:

myApp.constant('myAppConfig', data);

This myAppConfig can now be injected just like any other service, and in particular it's available during the configuration phase:

myApp.config(
    function (myAppConfig, someService) {
        someService.config(myAppConfig.someServiceConfig);
    }
);

or, for a smaller app, you could just inject the global config directly into your service, at the expense of spreading knowledge about the configuration format throughout the application.

Of course, since the async operations here will block the bootstrap of the application, and thus block the compilation/linking of the template, it's wise to use the ng-cloak directive to prevent the unparsed template from showing up during the work. You could also provide some sort of loading indication in the DOM , by providing some HTML that gets shown only until AngularJS initializes:

<div ng-if="initialLoad">
    <!-- initialLoad never gets set, so this div vanishes as soon as Angular is done compiling -->
    <p>Loading the app.....</p>
</div>
<div ng-cloak>
    <!-- ng-cloak attribute is removed once the app is done bootstrapping -->
    <p>Done loading the app!</p>
</div>

I created a complete, working example of this approach on Plunker, loading the configuration from a static JSON file as an example.

Oneidaoneil answered 8/12, 2013 at 1:13 Comment(4)
I don't think you need to defer the $http.get() until after the document is ready.Ediva
@Ediva yes, you are right that it works just as well if you swap the events so that we don't wait for the document to become ready until after the HTTP response is returned, with the advantage of possibly being able to begin the HTTP request faster. Only the bootstrap call needs to wait until the DOM is ready.Oneidaoneil
I created a bower module with your approach: github.com/philippd/angular-deferred-bootstrapBillfish
@MartinAtkins, I just found that your great approach doesn't work with Angular v1.1+. It looks like early versions of Angular just don't understand "then" until application is bootstrapped. To see it in your Plunk replace Angular URL with code.angularjs.org/1.1.5/angular.min.jsSelfsatisfaction
B
16

I had the same problem: I love the resolve object, but that only works for the content of ng-view. What if you have controllers (for top-level nav, let's say) that exist outside of ng-view and which need to be initialized with data before the routing even begins to happen? How do we avoid mucking around on the server-side just to make that work?

Use manual bootstrap and an angular constant. A naiive XHR gets you your data, and you bootstrap angular in its callback, which deals with your async issues. In the example below, you don't even need to create a global variable. The returned data exists only in angular scope as an injectable, and isn't even present inside of controllers, services, etc. unless you inject it. (Much as you would inject the output of your resolve object into the controller for a routed view.) If you prefer to thereafter interact with that data as a service, you can create a service, inject the data, and nobody will ever be the wiser.

Example:

//First, we have to create the angular module, because all the other JS files are going to load while we're getting data and bootstrapping, and they need to be able to attach to it.
var MyApp = angular.module('MyApp', ['dependency1', 'dependency2']);

// Use angular's version of document.ready() just to make extra-sure DOM is fully 
// loaded before you bootstrap. This is probably optional, given that the async 
// data call will probably take significantly longer than DOM load. YMMV.
// Has the added virtue of keeping your XHR junk out of global scope. 
angular.element(document).ready(function() {

    //first, we create the callback that will fire after the data is down
    function xhrCallback() {
        var myData = this.responseText; // the XHR output

        // here's where we attach a constant containing the API data to our app 
        // module. Don't forget to parse JSON, which `$http` normally does for you.
        MyApp.constant('NavData', JSON.parse(myData));

        // now, perform any other final configuration of your angular module.
        MyApp.config(['$routeProvider', function ($routeProvider) {
            $routeProvider
              .when('/someroute', {configs})
              .otherwise({redirectTo: '/someroute'});
          }]);

        // And last, bootstrap the app. Be sure to remove `ng-app` from your index.html.
        angular.bootstrap(document, ['NYSP']);
    };

    //here, the basic mechanics of the XHR, which you can customize.
    var oReq = new XMLHttpRequest();
    oReq.onload = xhrCallback;
    oReq.open("get", "/api/overview", true); // your specific API URL
    oReq.send();
})

Now, your NavData constant exists. Go ahead and inject it into a controller or service:

angular.module('MyApp')
    .controller('NavCtrl', ['NavData', function (NavData) {
        $scope.localObject = NavData; //now it's addressable in your templates 
}]);

Of course, using a bare XHR object strips away a number of the niceties that $http or JQuery would take care of for you, but this example works with no special dependencies, at least for a simple get. If you want a little more power for your request, load up an external library to help you out. But I don't think it's possible to access angular's $http or other tools in this context.

(SO related post)

Berstine answered 20/8, 2013 at 13:33 Comment(0)
N
8

What you can do is in your .config for the app is create the resolve object for the route and in the function pass in $q (promise object) and the name of the service you're depending on, and resolve the promise in the callback function for the $http in the service like so:

ROUTE CONFIG

app.config(function($routeProvider){
    $routeProvider
     .when('/',{
          templateUrl: 'home.html',
          controller: 'homeCtrl',
          resolve:function($q,MyService) {
                //create the defer variable and pass it to our service
                var defer = $q.defer();
                MyService.fetchData(defer);
                //this will only return when the promise
                //has been resolved. MyService is going to
                //do that for us
                return defer.promise;
          }
      })
}

Angular won't render the template or make the controller available until defer.resolve() has been called. We can do that in our service:

SERVICE

app.service('MyService',function($http){
       var MyService = {};
       //our service accepts a promise object which 
       //it will resolve on behalf of the calling function
       MyService.fetchData = function(q) {
             $http({method:'GET',url:'data.php'}).success(function(data){
                 MyService.data = data;
                 //when the following is called it will
                 //release the calling function. in this
                 //case it's the resolve function in our
                 //route config
                 q.resolve();
             }
       }

       return MyService;
});

Now that MyService has the data assigned to it's data property, and the promise in the route resolve object has been resolved, our controller for the route kicks into life, and we can assign the data from the service to our controller object.

CONTROLLER

  app.controller('homeCtrl',function($scope,MyService){
       $scope.servicedata = MyService.data;
  });

Now all our binding in the scope of the controller will be able to use the data which originated from MyService.

Neisse answered 25/5, 2013 at 21:41 Comment(7)
I will give this a shot when I have more time. This looks similar to what others were trying to do in ngModules.Disciplinary
I like this approach and I've used it before but I am currently trying to work out how to do this in a clean way when I have several routes, each of which may or may not depend on prefetched data. Any thoughts on that?Brummell
btw, I am leaning towards have each service that requires prefetched data make the request when it's initialized and return a promise and then set up resolve-objects with the services required by the different routes. I just wish there was a less verbose way.Brummell
@Brummell Each route is configured differently, so you can specify whether or not you wish to prefetch data. In a route you don't want to prefetch data, just adjust the resolve property function for the route, e.g. don't pass in $q and don't perform any kind of promise (return defer.promise). If the route is not dependant on any "pre" work, just leave out the resolve property function altogether.Neisse
@Neisse That's what I was aiming for, but I would much prefer if there was some way to just say "fetch all this stuff first regardless of which route is loaded" without having to repeat my resolve-blocks. They all have something they depend on. But it's not a huge deal, it would just feel a little more D.R.Y. :)Brummell
@Brummell How about setting a property ot the top of your service object? e.g MyService.loaded Set it to false initially, or at any time you want to force an update. Then check for this property in your resolve function. If it's false: load data and set MyService.loaded = true.Neisse
this is the route I ended up taking except I had to make resolve an object with a property being the function. so it ended up being resolve:{ dataFetch: function(){ // call function here } }Measures
D
5

So I found a solution. I created an angularJS service, we'll call it MyDataRepository and I created a module for it. I then serve up this javascript file from my server-side controller:

HTML:

<script src="path/myData.js"></script>

Server-side:

@RequestMapping(value="path/myData.js", method=RequestMethod.GET)
public ResponseEntity<String> getMyDataRepositoryJS()
{
    // Populate data that I need into a Map
    Map<String, String> myData = new HashMap<String,String>();
    ...
    // Use Jackson to convert it to JSON
    ObjectMapper mapper = new ObjectMapper();
    String myDataStr = mapper.writeValueAsString(myData);

    // Then create a String that is my javascript file
    String myJS = "'use strict';" +
    "(function() {" +
    "var myDataModule = angular.module('myApp.myData', []);" +
    "myDataModule.service('MyDataRepository', function() {" +
        "var myData = "+myDataStr+";" +
        "return {" +
            "getData: function () {" +
                "return myData;" +
            "}" +
        "}" +
    "});" +
    "})();"

    // Now send it to the client:
    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.add("Content-Type", "text/javascript");
    return new ResponseEntity<String>(myJS , responseHeaders, HttpStatus.OK);
}

I can then inject MyDataRepository where ever I need it:

someOtherModule.service('MyOtherService', function(MyDataRepository) {
    var myData = MyDataRepository.getData();
    // Do what you have to do...
}

This worked great for me, but I am open to any feedback if anyone has any. }

Disciplinary answered 2/5, 2013 at 14:15 Comment(2)
I like your modular approach. I've found that $routeScope is available to the service requesting data and you can assign data to it in the $http.success callback. However, using $routeScope for non-global items creates a smell and data should really be assigned to the controller $scope. Unfortunately, I think your approach, whilst innovative, isn't ideal (respect though for finding something which works for you). I'm just sure there must be a client-side only answer which somehow waits for the data and allows assignment to scope. The search continues!Neisse
In case it is useful to someone, I recently saw some different approaches looking at modules other people have written and added to the ngModules website. When I get more time I will have to either start using one of those, or figure out what they did and add it to my stuff.Disciplinary
U
2

Also, you can use the following techniques to provision your service globally, before actual controllers are executed: https://mcmap.net/q/80977/-run-controllers-only-after-initialization-is-complete-in-angularjs. Just resolve your data globally and then pass it to your service in run block for example.

Urion answered 25/11, 2014 at 15:21 Comment(0)
M
1

You can use JSONP to asynchronously load service data. The JSONP request will be made during the initial page load and the results will be available before your application starts. This way you won't have to bloat your routing with redundant resolves.

You html would look like this:

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>

function MyService {
  this.getData = function(){
    return   MyService.data;
  }
}
MyService.setData = function(data) {
  MyService.data = data;
}

angular.module('main')
.service('MyService', MyService)

</script>
<script src="/some_data.php?jsonp=MyService.setData"></script>
Mither answered 6/7, 2016 at 13:55 Comment(0)
B
-1

Easiest way to fetch any initialize use ng-init directory.

Just put ng-init div scope where you want to fetch init data

index.html

<div class="frame" ng-init="init()">
    <div class="bit-1">
      <div class="field p-r">
        <label ng-show="regi_step2.address" class="show-hide c-t-1 ng-hide" style="">Country</label>
        <select class="form-control w-100" ng-model="country" name="country" id="country" ng-options="item.name for item in countries" ng-change="stateChanged()" >
        </select>
        <textarea class="form-control w-100" ng-model="regi_step2.address" placeholder="Address" name="address" id="address" ng-required="true" style=""></textarea>
      </div>
    </div>
  </div>

index.js

$scope.init=function(){
    $http({method:'GET',url:'/countries/countries.json'}).success(function(data){
      alert();
           $scope.countries = data;
    });
  };

NOTE: you can use this methodology if you do not have same code more then one place.

Brambly answered 16/3, 2016 at 13:59 Comment(1)
It's not advised to use ngInit according the docs: docs.angularjs.org/api/ng/directive/ngInitBandwidth

© 2022 - 2024 — McMap. All rights reserved.