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.
Based on Martin Atkins' solution, here is a complete, concise pure-Angular solution:
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.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:You can, for example, use the
module.constant
mechanism to make data available to your app:This
myAppConfig
can now be injected just like any other service, and in particular it's available during the configuration phase: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:I created a complete, working example of this approach on Plunker, loading the configuration from a static JSON file as an example.
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:
Now, your
NavData
constant exists. Go ahead and inject it into a controller or service: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 simpleget
. 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)
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:
Server-side:
I can then inject MyDataRepository where ever I need it:
This worked great for me, but I am open to any feedback if anyone has any. }
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:
Add
resolve
to your route config:Your controller won't get instantiated before all dependencies are resolved:
I've made an example at plnkr: http://plnkr.co/edit/GKg21XH0RwCMEQGUdZKH?p=preview
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
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
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
Now all our binding in the scope of the controller will be able to use the data which originated from MyService.