Not an easy one to describe this, but basically I have a service which is set up as a provider so I can configure it. It has an array of APIs used in my project which is initially empty. Various config blocks can add an API object to the array and this seems to be working. I console.log the output each time and the array is growing.
I then inject my service into something else (in this case an $http interceptor function) and use a service method to return the array, but each time I get an empty array.
I thought the way this worked is that all config blocks ran first and that the $http call which is being intercepted is happening way after that, so the array should be full of APIs by the time it's intercepted.
Anyway, here's some code
angular.module('authModule').provider('authService', function(){
var _apis = [];
return({
addApi: addApi,
$get: instantiateAuth
});
function addApi(newApi){
_apis.push(newApi);
console.log("API added", _apis);
}
function instantiateAuth() {
return({
getApis: function(){
console.log("Getting APIs", _apis);
return _apis;
}
});
}
})
.config(function($httpProvider){
$httpProvider.interceptors.push(function($log, $rootScope, $q) {
return {
request: function(config) {
var injector = angular.injector(['ng', 'authModule']);
var authService = injector.get('authService');
console.log("apis", authService.getApis());
}
};
});
});
And an example config block
angular.module('myModule').config(function ($provide, authServiceProvider) {
authServiceProvider.addApi({
url: 'https://apiurl.com',
other: 'stuff'
});
authServiceProvider.addApi({
url: 'https://apiurl2.com',
other: 'stuff'
});
});
So, each time the appApi method is called in a config block (twice here), this line outputs the array console.log("API added", _apis); and it correctly outputs 1 item after the first call and two items after the second call.
When this code - authService.getApis() - fires the first time an HTTP call is intercepted, it logs an empty array to the console.
Any help would really be appreciated.
EDIT:
The problem seems to be this line
var injector = angular.injector(['ng', 'authModule']);
My provider seems to be reset/recreated each time this happens, so maybe I'm misunderstanding how to use the injector. I was originally just injecting my authService the normal way in the function parameters but I was getting a circular dependency (my auth service needs to open a modal window, but angular-ui modals rely on the http service and my http calls are being intercepted to check with my auth service that the user is authenticated :( )
Yes, angular.injector(['ng', 'authModule']) essentially creates a new injector instance (an application instance, in layman's terms):
angular.injector(['authModule']) !== `angular.injector(['authModule'])
ng module is loaded by default, it doesn't have to be specified explicitly. And singleton services are singletons only within the same injector instance:
injector.get('authService') === injector.get('authService');
But
angular.injector(['authModule']).get('authService') !== `angular.injector(['authModule']).get('authService')
To reuse current injector instance (which is a desirable behaviour in almost every situation) $injector service should be used:
$httpProvider.interceptors.push(function($log, $rootScope, $q, $injector) {
return {
request: function(config) {
var authService = $injector.get('authService');
}
};
});
$injector.get is known and straightforward solution to get around circular dependencies.
Related
I have an existing application that uses a MapProvider like so:
mapModule.factory('MapProvider', ['$injector', function($injector) {
return $injector.get('GoogleMapsService');
}]);
This MapProvider is used extensively across the application and is injected into various other controllers and services (rightly or wrongly).
I now need to add a BaiduMapsService, which I have been able to get working as a test with:
mapModule.factory('MapProvider', ['$injector', function($injector) {
if(true) {
return $injector.get('GoogleMapsService');
} else {
return $injector.get('BaiduMapsService');
}
}]);
And flipping the if value accordingly. (Both of these services are using a TypeScript interface, so have the same methods). Now, I need to add a $http call to the API, which will return which map to use, based on the provided data. How can I make my factory asynchronous, without having to change all my MapProvider.someCallHere() calls to MapProvider.then(m => m.someCallHere()).
Ideally, when MapProvider is injected across my application, it will be able to resolve using the async data (only once), and then inject the necessary service afterwards.
Alternatively, is there a way to defer / delay loading Angular at all, until I make an API call and set some global data somewhere?
Thanks.
You can postpone the application bootstrap (also, don't use ng-app, do it manually) until you get data from server. I've answered this before on this question but each case has its own specific details.
I usually see a config value being declared on the app before the application gets bootstraped, this is very useful for multi-tenant apps. So that this preference values can be used in the whole app as an injected provider.
For example:
var app = angular.module('app', []);
// retrieve the $http provider
var ngInjector = angular.injector(["ng"]);
var $http = ngInjector.get("$http");
// load config function. Returns a promise.
function loadConfig(){
return $http.get("/config.json").then(function(response) {
// declare the configuration value on your app
app.constant("Config", response.data);
}, function(err) {
console.error("Error loading the application config.", err)
});
}
// Call loadConfig then bootstrap the app
loadConfig().then(function () {
angular.element(document).ready(function() {
angular.bootstrap(document, ["app"]);
});
});
Finally from your factory, you can use the Config constant to retrieve the preferred map.
mapModule.factory('MapProvider', ['$injector', 'Config', function($injector, Config) {
if(Config.preferedMap == 'GoogleMap') {
return $injector.get('GoogleMapsService');
} else {
return $injector.get('BaiduMapsService');
}
}]);
Only way I can think is to hold initialize whole angular (and modules) until you got your "config" (and set is as global variable).
I'm defining some setup code in the config function of an Angular module that I want to unit test. It is unclear to me how I should do this. Below is a simplified testcase that shows how I'm getting stuck:
'use strict';
angular.module('myModule', []).config(['$http', '$log', function($http, $log) {
$http.get('/api/getkey').then(function success(response) {
$log.log(response.data);
});
}]);
describe('myModule', function() {
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
angular.module('myModule');
$httpBackend.flush();
}));
});
This is clearly not the right way because I get the following error:
Error: No pending request to flush !
A complete, ready-to-run Angular project with the above testing code can be found on GitHub. If you know what to do with this scenario, please answer here on Stack Overflow. Bonus points if you also submit a pull request to the GitHub repo.
Use run instead of config if your initialization requires services to be injected. The config function can only receive providers and constants as parameters, not instantiated services like $http (relevant docs).
angular.module('myModule', []).run(['$http', '$log', function($http, $log) {
...
}]);
Initialize your module for testing
beforeEach(module('myModule'));
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
$httpBackend.flush();
}));
So the full working version looks like
'use strict';
angular.module('myModule', []).run(['$http', '$log', function($http, $log) {
$http.get('/api/getkey').then(function success(response) {
$log.log(response.data);
});
}]);
describe('myModule', function() {
beforeEach(module('myModule'));
it('logs a key obtained from XHR', inject(function($httpBackend) {
$httpBackend.expectGET('/api/getkey').respond(200, '12345');
$httpBackend.flush();
}));
});
Also, here's an example of testing the config block to check that a method on a provider was called: https://medium.com/#a_eife/testing-config-and-run-blocks-in-angularjs-1809bd52977e#71e0
mzulch is right to point out that services cannot be injected in an angular.module(...).config block. He also provides the right solution for the scenario where you actually need to use services in module initialization code: use the .run block instead of the .config block. His answer works perfectly for this scenario.
The question of how to write a unit test for the .config block remains. Let's adapt the naieve code from my question to a scenario where .config is actually warranted. The following snippet injects a provider dependency instead of a service dependency:
angular.module('myModule', []).config(['$httpProvider', function($httpProvider) {
$httpProvider.useApplyAsync(true);
}]);
describe('myModule', function() {
it('configures the $http service to combine response processing via $applyAsync', inject(function($httpProvider) {
angular.module('myModule');
expect($httpProvider.useApplyAsync()).toBeTruthy();
}));
});
This time, the implementation of 'myModule' is correct. The unit test however, which is analogous to the attempt in my question, is still incorrect. Now Karma gives me the following error:
Error: [$injector:unpr] Unknown provider: $httpProviderProvider <- $httpProvider
This cryptical error is coming from the inject which is passed as the second argument to the it. Note how Provider is being stuttered. This is caused by the fact that inject is looking for the provider for $httpProvider. A "meta provider", as we may call it. Such things don't exist in the Angular framework, but inject is trying it anyway because it expects you to only ask for service dependencies. Services do have providers, for example, $http has $httpProvider.
So inject (full name: angular.mock.inject, here available globally) is not the right way to get hold of $httpProvider in the testcase. The right way is to define an anonymous module configuration function using module (angular.mock.module) which closes over a variable in which we can capture the provider. This works because providers can be injected at configuration time (see the link at the bottom of mzulch's answer as well as my own answer to my other question for details on configuration time vs run time). It looks like this:
var $httpProvider;
beforeEach(function() {
module(function(_$httpProvider_) {
// this is a .config function
$httpProvider = _$httpProvider_;
});
// after this I can use inject() to make the magic happen
});
Another mistake in my naieve testcase is that I'm trying to execute 'myModule's configuration steps by calling angular.module('myModule'). For testcase purposes, I should be using the global module (angular.mock.module) instead, and the wisest place to do so is in the beforeEach fixture. In conclusion, the following code does the job:
describe('myModule', function() {
var $httpProvider;
beforeEach(function() {
module(function(_$httpProvider_) {
$httpProvider = _$httpProvider_;
});
module('myModule');
});
it('configures the $http service to combine response processing via $applyAsync', function() {
inject(); // enforces all the module config steps
expect($httpProvider.useApplyAsync()).toBeTruthy();
});
});
I opted to put the inject() at the start of my testcase, but I could also put it at the end of the beforeEach. The advantage of the latter approach would be that I can write the call to inject in one place and not need to repeat it in every testcase. The advantage of the approach actually taken here is that more modules can be added to the injector in later beforeEaches or even in individual testcases.
I pushed this alternative solution to a new branch on GitHub.
In my application I have an orchestration service that gets the uris from different services that register with it. service_1 and service_2 could be on different machines but one registered, the uris of their machines will be stored.
In my other application which makes use of that orchestration service, I want to call to the orchestration service to get the uris to use, but then I want to set them as Angular constants, or at least be able to use the uri's values.
So this is the service that's going to be using the 'constant' which is the uri pulled from orchestration service:
(function () {
'use strict';
angular
.module('data.model-view', ['restapi'])
.factory('MVService', MVService);
MVService.$inject = ['$http', '$q', 'exception', 'logger', 'restapi'];
/* #ngInject */
function MVService($http, $q, exception, logger, restapi) {
var HOST = restapi.mvservice.HOST;
var MODULE = restapi.mvservice.MODULE;
...
//below is an example of what will use the above host/module in order to
//get the data needed for this application
function getModels() {
return $http.get(HOST + MODULE + '/model/retrieveAll')
.then(success)
.catch(fail);
function success(response) {
return response.data;
}
function fail(e) {
return exception.catcher('XHR Failed for retrieveAll')(e);
}
}
So then this is restapi module where I'd like the constants to be set, so I can have access to them throughout the application, but I need to get them from the orchestration service first.
(function() {
'use strict';
var data = '';
angular
.module('restapi', [])
.factory('restapi', function($http, exception) {
var HOST = %%ORCSERVICE%%;
var MODULE = '/orchestration/service/rest/data';
return $http.get(HOST + MODULE)
.then(success)
.catch(fail);
function success(response) {
//so at this point I can actually access the data I need
//with a console.debug(response.data);
return response.data;
}
function fail(e) {
return exception.catcher('XHR Failed to reach orc')(e);
}
}).constant('restapi', constant());
function constant() {
//set constants here based on the data pulled from above
//ideally I want the result of this to be like
//{
// mvservice: {
// 'HOST': 'http://xxxxxxxxxx.amazonaws.com'
// 'MODULE': '/rest/service/whatever'
// },
// ... //other service here
//}
}
})();
Like I say in the comment above, I can actually get the data I need (the uris) from the $http.get immediately above. I'd just like then to be able to get the data set as a constant, or at least in a form that I can access it. Because when MVService spins up, it needs the its own uir from the orchestration service in order to be able to make its rest calls. Sorry might be a little confusing, let me know if there is a need for clarification.
Try bootstrapping app after getting necessary data:
var injector = angular.injector(['ng']),
http = injector.get('$http');
http.get(HOST + MODULE).then(function (result) {
app.value('restapi', result.data);
angular.bootstrap(document, [app.name]);
});
There is multiple way :
If you want to use angular.constant you can do it by getting the url by delaying bootstrap of angular until you get your values. Because you can't set constant once the ng-app has loaded. To do this see karaxuna's answer.
Another way is to perform your angular queries in the constructor of your service. Because the routing and call of controllers won't happen until all promise of the service's instantiation phase will be resolved. So you can store the result of your request as fields of your service and then access it without any racing problems on the controllers/directives/angular.run parts of your application.
Wasn't sure what title to give this one but basically I have an authorization provider which I have created myself which need to be configured during the config phase to have a requireLogin() function which will be run at a later date. Like this...
// Configure $auth service
app.config(function ($authProvider, appConfig) {
$authProvider.setRequireLoginFunction(function($modal){
// Use the modal service
});
})
And this is the provider code
app.provider('$auth', function(){
var _requireLoginFn;
return({
setRequireLoginFunction: setRequireLoginFunction,
$get: instantiateAuth
});
function setRequireLoginFunction(fn){
_requireLoginFn = fn;
}
function instantiateAuth($http, $q) {
return({
requireLogin: requireLogin
});
function requireLogin() {
return _requireLoginFn.apply(undefined, arguments);
}
}
});
Sidenote: I'm using ng-annotate so don't use the array syntax of DI.
Anyway, as you can see, the function that is being stored in the config phase which will later be called using $auth.requireLogin.then(...) etc. needs access to the angular-ui modal service, but when I call the function later from inside the provider obviously the DI stuff isn't happening because all I've done is put $modal in the arguments of my function which isn't magic. I also can't put $modal in the dependencies of my provider because it's too early to do that, also my provider doesn't know what dependencies the function I pass in will need.
I feel the answer is probably to:
A) use the injector within the function I'm passing in to get access to $modal service or
B) Somehow run the function from inside the provider when called externally and somehow get all the supplied dependencies injected at runtime?
Sorry if I'm not able to explain this easily. I tried to make a JSfiddle but couldn't hack the code down easily.
The main question - is it possible? I tried with no luck..
main app.js
...
var app = angular.module('myApp', ['services']);
app.config(['customProvider', function (customProvider) {
}]);
...
provider itself
var services = angular.module('services', []);
services.provider('custom', function ($http) {
});
And I've got such error:
Uncaught Error: Unknown provider: $http from services
Any ideas?
Thanks!
The bottom line is:
You CANNOT inject a service into the provider configuration section.
You CAN inject a service into the section which initializes the provider's service.
Details:
Angular framework has a 2 phase initialization process:
PHASE 1: Config
During the config phase all of the providers are initialized and all of the config sections are executed. The config sections may contain code which configures the provider objects and therefore they can be injected with provider objects.
However, since the providers are the factories for the service objects and at this stage the providers are not fully initialized/configured -> you cannot ask the provider to create a service for you at this stage -> at the configuration stage you cannot use/inject services.
When this phase is completed all of the providers are ready (no more provider configuration can be done after the configuration phase is completed).
PHASE 2: Run
During run phase all the run sections are executed. At this stage the providers are ready and can create services -> during run phase you can use/inject services.
Examples:
1. Injecting the $http service to the provider initialization function WILL NOT work
//ERRONEOUS
angular.module('myModule').provider('myProvider', function($http) {
// SECTION 1: code to initialize/configure the PROVIDER goes here (executed during `config` phase)
...
this.$get = function() {
// code to initialize/configure the SERVICE goes here (executed during `run` stage)
return myService;
};
});
Since we are trying to inject the $http service into a function which is executed during the config phase we will get an error:
Uncaught Error: Unknown provider: $http from services
What this error is actually saying is that the $httpProvider which is used to create the $http service is not ready yet (since we are still in the config phase).
2. Injecting the $http service to the service initialization function WILL work:
//OK
angular.module('myModule').provider('myProvider', function() {
// SECTION 1: code to initialize/configure the PROVIDER goes here (executed during `config` phase)
...
this.$get = function($http) {
// code to initialize/configure the SERVICE goes here (executed during `run` stage)
return myService;
};
});
Since we are now injecting the service into the service initialization function, which is executed during run phase this code will work.
This might give you a little leverage:
var initInjector = angular.injector(['ng']);
var $http = initInjector.get('$http');
But be careful, the success/error callbacks might keep you in a race-condition between the app start and the server response.
This is an old question, seems we have some chicken egg thing going on if we want to rely on the core capability of the library.
Instead of solving the problem in a fundamental way, what I did is by-pass. Create a directive that wraps the whole body. Ex.
<body ng-app="app">
<div mc-body>
Hello World
</div>
</body>
Now mc-body needs to be initialized before rendering (once), ex.
link: function(scope, element, attrs) {
Auth.login().then() ...
}
Auth is a service or provider, ex.
.provider('Auth', function() {
... keep your auth configurations
return {
$get: function($http) {
return {
login: function() {
... do something about the http
}
}
}
}
})
Seems to me that I do have control on the order of the bootstrap, it is after the regular bootstrap resolves all provider configuration and then try to initialize mc-body directive.
And this directive seems to me can be ahead of routing, because routing is also injected via a directive ex. <ui-route />. But I can be wrong on this. Needs some more investigation.
In response to your question, "Any Ideas?", I would have respond with "yes". But wait, there's more!
I suggest just using JQuery in the config. For example:
var app = angular.module('myApp', ['services']);
app.config(['$anyProvider', function ($anyProvider) {
$.ajax({
url: 'www.something.com/api/lolol',
success: function (result) {
$anyProvider.doSomething(result);
}
});
}]);