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 1:
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
Solution 2:
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.
Solution 3:
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');
}
}
});
Solution 4:
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.