How to inject services into a provider with angular 1.3 - javascript

In every piece of information I can find (including the angular documentation), the way to inject a service into a provider is through the $get method:
var myApp = angular.module('myApp', []);
myApp.provider('helloWorld', function() {
this.$get = function() {
return {
sayHello: function() {
return "Hello, World!"
}
}
};
});
function MyCtrl($scope, helloWorld) {
$scope.hellos = [helloWorld.sayHello()];
}
This would work perfectly in angular 1.2 and below: http://jsfiddle.net/1kjL3w13/
Switch to angular 1.3 though, and the $get function completely breaks. It seems that whatever's returned from the $get function is no longer used to instantiate the provider, and thus is now useless for injecting f.e. services.
Same example as above, but using angular 1.3: http://jsfiddle.net/duefnz47/
This is exactly the behavior provided in the angular documentation. So either the documentation is wrong or I've completely misunderstood it. I don't really care if the $get method works as before or not though, I just need to be able to inject services reliably into my provider.

Problem is you are using global controller which is not valid according to angular 1.3
So use
angular.module('myApp').controller('MyCtrl',function ($scope, helloWorld) {
$scope.hellos = [helloWorld.sayHello()];
});
Here is updated fiddle
**
Migration Document official
**
Hope it help :)

Related

Scope variable not visible within ng-click handler

I'm pretty new to Angular and am trying to figure out what's wrong here. There is a controller defined like this:
(function(){
function myController($scope, CommsFactory) {
$scope.doSomething = function() {
var x = $scope; // <- Doesn't work because $scope is not defined
}
}
angular
.module('aModule')
.controller('myController', myController);
})();
The doSomething() method is then called by a button click like:
<input type="button" ng-click="doSomething()" class="btn--link" value="do it"/>
This seems straightforward to me but the problem is that, when I break within the method, $scope is not defined. This is different from most of the examples I've seen, and I can't figure out why. Shouldn't it be visible here? Obviously a lot of code is missing - I've tried to show only the relevant bits - could I be missing something somewhere else?
You're declaring a module then you need to add [].
Something like this:
angular.module('aModule', [])
.controller('myController', myController);
Usage
angular.module(name, [requires], [configFn]);
Arguments
name.- The name of the module to create or retrieve.
requires (optional).- If specified then new module is being created. If unspecified then the module is being retrieved for further
configuration.
configFn (optional).- Optional configuration function for the module. Same as Module#config().
Please, I would to recommend to read this guide about Angular Module:
angular.module
(function() {
function myController($scope) {
$scope.doSomething = function() {
var x = $scope;
console.log(x);
}
}
angular
.module('aModule', [])
.controller('myController', myController);
})();
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div data-ng-app="aModule">
<div data-ng-controller="myController">
<input type="button" ng-click="doSomething()" class="btn--link" value="do it" />
</div>
</div>
Your code is generally working fine as demonstrated in this fiddle.
Your main problem seems to be in the usage of $scope. $scope is an object containing all variables and methods which should be available in the corresponding template. For this reason, you would always reference a member of $scope, instead of the whole object.
Furthermore, John Papas AngularJS style guide recommends the usage of controllerAs in favor of $scope for multiple reasons as stated in Y030
By convention, you should also give your controllers uppercase names and use explicit Dependency Injection
A typical use case would rather look like:
(function(){
angular
.module('aModule', [])
.controller('myController', MyController);
MyController.$inject = ['$scope', 'CommsFactory'];
function MyController($scope, CommsFactory) {
var vm = this;
vm.doSomething = doSomething;
function doSomething() {
var $scope.x = "Did it!";
}
}
})();
SOLVED: It turns out that what I was experiencing had something to do with the way in which the Chrome debugger works. It appears to do some kind of lazy loading of variables defined outside of the function in which you break (or at least this as far as I've characterized it). What this means, at least in my case, is that if I break inside of the method, and $scope isn't actually used within that method (which, unfortunately, I was doing a lot because I was trying to verify that $scope was visible), then the debugger will report that $scope is unavailable.

Pass jQuery dependency to angular js controller

I am using angularjs 1.4, and in one of my angular controller I have to use jQuery. but when I am trying to pass it as dependency, it is not working.
I tried below code, but no success
(function () {
'use strict';
var app= angular.module('app');
app.controller('getUserInfo', ['jQuery',
function($) {
// some logic
}]);
})();
I also tried below code, but no success
(function () {
'use strict';
var app= angular.module('app');
app.controller('getUserInfo', ['$',
function($) {
// some logic
}]);
})();
Can some please guide what I am doing wrong.
You could create your own constant inside your app module & then you can inject that dependency where ever you want.
app.constant('jQuery', window.jQuery)
I chosen constant, because It would be available to inject its dependency inside config phase of angular.
(function () {
'use strict';
var app= angular.module('app');
app.controller('getUserInfo', ['jQuery',
function($) {
// $ will provide you all jQuery method available in it.
//but don't do DOM manipulation from controller.
//controller is not the correct place to play with DOM.
}]);
})();
But as you wanted to inject dependency of jQuery inside a controller, I'd say NO. Don't do that. Basically you shouldn't do any DOM manipulation from the controller. You can do that from the directive, which has capability to playing in better way with DOM.
If you load jQuery.js before angular.js, AngularJS will make it available as angular.element and add Angular specific methods as well.
app.controller('getUserInfo', function() {
var $ = angular.element;
// some logic
}]);
From the Docs:
If jQuery is available, angular.element is an alias for the jQuery function. If jQuery is not available, angular.element delegates to Angular's built-in subset of jQuery, called "jQuery lite" or jqLite.
For more information see the AngularJS angular.element API Reference.

AngularJS ControllerAs syntax and controller injected variables

I'm writing a directive and trying to stick to the John Papa style guide. So I've also decided to jump on the ControllerAs syntax wagon and I've got a tiny directive like below:
(function() {
angular
.module('htApp')
.directive('newsletterSignup', newsletter_signup);
function newsletter_signup($http) {
var directive = {
templateUrl: '../whatever.tpl.html',
restrict: 'EA',
controller : controller,
controllerAs: 'vm',
bindToController: true
};
return directive;
controller.$inject = ['$http'];
function controller($http) {
var vm = this;
// $http is here ... all is good, but I don't need it
function doSubmit(form) {
// I need $http here, but it is null
debugger;
};
vm.doSubmit = doSubmit;
}
}
})();
This is a newsletter signup service. I'm going to have to do an HTTP request, therefore I'm injecting it into the controller. All is fine - but calling the vm.doSubmit(--formname-here--) function from the template results in me not being able to find the $http service.
So my question: how can I access the injected $http from the doSubmit() function?
EDIT
I'll include the view code - but no worries - the plumbing works:
<button class="btn btn-yellow" ng-click="vm.doSubmit(newsletterform)" translate>
footer.ok_button_text
</button>
EDIT 2
As it turns out, #Tek was right - the code works. I think the reason I didn't see it was because (I think) the JS runtime in Chrome optimizes the $http away when it knows it's not going to be called.
This code works fine. I think this is because the runtime aniticipated the usage of $http in the console.log() function call. However - if I remove that line I get this ( which was why I had this problem in the first place ):
Notice that I commented out the console.log - thus the doSubmit() call never uses $http. Now - when I call $http in the console - it's not defined.
The problem is here:
return directive;
controller.$inject = ['$http'];
function controller($http) {
...
controller function is defined when return statement is executed. But controller.$inject will never be defined. Also, newsletter_signup function misses the corresponding $inject.
This won't be minified properly. While this will be minified.
Your example works just fine: example.
But as for me John Papa has pretty strange vision of angular style, I prefer this style guide.

Testing Modal Controller: unknown provider: $modalInstanceProvider <- $modalInstance, TypeError: Attempted to assign to a readonly property

I am a bit new to AngularJs. I am using Angular UI bootstrap (0.10.0) for modal implementation. I am getting the following errors while testing a modal controller
using AngularJs 1.2.7: TypeError: Attempted to assign to a readonly property
using AngularJs 1.2.12: unknown provider: $modalInstanceProvider <- $modalInstance.
I have gone through a lot of similar questions but couldn't understand what's the problem.
As pointed in the comments by Mahery, $modalInstance is made available for injection in the controller by AngularUI Bootstrap implementation. So, we don't need any effort to "resolve" or make it available somehow.
Modal Window Issue (Unknown Provider: ModalInstanceProvider)
This is my main controller that leads to creation of modal instance on clicking open on the partial page.
var SomeCtrl = function($scope, $modal){
$scope.open = function(){
$scope.modalInstance = $modal.open({
templateUrl: '/templates/simpleModal.html',
controller: 'simpleModalController',
});
$scope.modalInstance.result.then(
function(){
console.log("clicked OK");
},
function(){
console.log("clicked Cancel");
});
};
};
someCtrl.$inject = ["$scope", "$modal"];
angular.module('angularApp').controller("someCtrl", SomeCtrl);
This is the modal controller I wish to test if it contains the necessary functions (which I intend to add later)
(function(){
var SimpleModalController = function($scope, $modalInstance){
$scope.ok = function(){
$modalInstance.close('ok');
};
$scope.cancel = function(){
$modalInstance.dismiss('cancel');
};
};
SimpleModalController.$inject = ["$scope", "$modalInstance"];
angular.module('angularApp').controller("simpleModalController", SimpleModalController);
})();
This is the test I have written for the modal controller
describe('Testing simpleModalController',function() {
var ctrlScope;
var modalInstance;
var ctrl;
beforeEach(function() {
module('angularApp');
inject(function($rootScope, $modalInstance, $controller) {
ctrlScope = $rootScope.new();
modalInstance = $modalInstance;
ctrl = $controller('simpleModalController',
{
$scope : ctrlScope,
$modalInstance : modalInstance
});
});
});
it('should check existence of scope variables and functions when created', function() {
console.log('pending test');
});
});
I have no troubles in the functionality of modal in the app, testing the main controller and integration of modal. But I am unable to test the modal controller. I think the problem is with injection of $modalInstance in the test (of simple modal controller). But as mentioned earlier, angular-ui bootstrap makes it available.
Any help is appreciated. Thanks.
Having the next Modal Controller definition:
angular.module('module').controller('ModalInstanceController',ModalInstanceController);
function ModalInstanceController($timeout, $modalInstance, $scope) {
//controller across a bunch of modals
$scope.closeModal = function(){
$modalInstance.dismiss('cancel');
};
$scope.action = function(){
$modalInstance.dismiss();
};
}
You can create an spy Object with the needed methods using jasmine, and pass that object to the controller when you create the instance:
beforeEach(inject(($controller, $timeout, $rootScope) => {
modalInstance = jasmine.createSpyObj('modalInstance', ['dismiss']);
scope = $rootScope.$new();
controller = $controller('ModalInstanceController', {
$modalInstance: modalInstance,
$scope: scope
});
}));
Later in your test scenarios you can check the spied object:
it('should defined the required methods on the scope', () => {
expect(scope.closeModal).toBeDefined();
expect(scope.action).toBeDefined();
scope.closeModal();
expect(modalInstance.dismiss).toHaveBeenCalledWith('cancel');
scope.action();
expect(modalInstance.dismiss).toHaveBeenCalledWith();
});
So.. This is one way of testing it..
describe('Testing',function() {
it('test',function() {
inject(function($rootScope, $modal) {
var fakeModal = { };
//Basically, what you want is for your modal's controller to get
//initalized and then returned to you, so the methods in it can be unit tested
spyOn(modal, 'open').andReturn(fakeModal);
ctrl = $controller('Controller',
{
$scope : ctrlScope,
$modal: modal
});
});
});
});
I've been fighting with this issue too. The problem is that the controller your trying to instantiate in your test is a total different instance the $modal service instantiates internally passing in the actual modal window as $modalInstance. See the js code comments at the example http://angular-ui.github.io/bootstrap/#/modal:
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
So how do you test your controller then? I've not found the solution yet, im sorry. The diffulty resides in that you can't have access to your controller's scope as the $modal service creates a new scope from whether is the $rootScope or a scope you pass in with your options. So you loose track.
The least you can do is test the functions you passed in to the result promise. This is done by spying on the $modal.open function and returning a mock. And this is shown here https://stackoverflow.com/a/21370703/2202143. And complement with integration tests using tools like protractor.
Take a look at the answer that was selected as correct on the question: Unit testing a modalInstance controller with Karma / Jasmine.
I was struggling with the same issue for some time, and that question (and answer) helped me test my modals (and functions to open/close them) in a really clean way!
I didn't like any answer given here so I am adding my own.
the reason I didn't like the answers above is that they don't hold once a project is a bit bigger.
for me, the solution was to simply implement an angular service called $modalInstance...
so under spec I create a folder named shims that I use for small items such as these. (make sure to add it to karma.conf)
and there I implement
angular.module(..).service('$modalInstance', function(){
this.dismiss = jasmine.createSpy('$modalInstance.dismiss');
...
});
I find this method much cleaner and much more maintainable and straightforward.
sometimes, I like making sure my shim is only loaded for specific tests, in that case I simply give it a specific module name and then I have to add a module call for it otherwise it won't load.
I also highly recommend using a different library for modals, I recommend ng-dialog for many reasons but in this context I can say it is much more test friendly, been using it for a while now.

Unit test when loading things at app run with AngularJS

I need my app to run some configuration at runtime vi an HTTP endpoint.
I wrote a simple service to do that:
module.factory('config', function ($http, analytics) {
return {
load: function () {
$http.get('/config').then(function (response) {
analytics.setAccount(response.googleAnalyticsAccount);
});
}
}
});
Next, I call this module in a run block of my app module:
angular.module('app').***.run(function(config) {
config.load();
});
All is working well when the app is running but in my unit tests, I get this error: "Error: Unexpected request: GET /config"
I know what it means but I don't know how to mock it when it happens from a run block.
Thanks for your help
EDIT to add spec
Calling this before each
beforeEach(angular.mock.module('app'));
Tried this to mock $httpBackend:
beforeEach(inject(function($httpBackend) {
$httpBackend.expectGET('/config').respond(200, {'googleAnalyticsAccount':});
angular.mock.module('app')
$httpBackend.flush();
}));
But got:
TypeError: Cannot read property 'stack' of null
at workFn (/Users/arnaud/workspace/unishared-dredit/test/lib/angular/angular-mocks.js:1756:55)
TypeError: Cannot read property 'stack' of null
at workFn (/Users/arnaud/workspace/unishared-dredit/test/lib/angular/angular-mocks.js:1756:55)
TypeError: Cannot read property 'stack' of null
at workFn (/Users/arnaud/workspace/unishared-dredit/test/lib/angular/angular-mocks.js:1756:55)
EDIT since update to AngularJS 1.0.6
Since I've updated to AngularJS 1.0.6, advised by Igor from the Angular team, the issue is gone but now I've now got this one, which sounds more "normal" but I still can't figure out how to make it works.
Error: Injector already created, can not register a module!
I struggled with this error for a little while, but managed to come up with an sensible solution.
What I wanted to achieve is to successfully stub the Service and force a response, on controllers it was possible to use $httpBackend with a request stub or exception before initiating the controller.
In app.run() when you load the module it initialises the object and it's connected Services etc.
I managed to stub the Service using the following example.
describe('Testing App Run', function () {
beforeEach(module('plunker', function ($provide) {
return $provide.decorator('config', function () {
return {
load: function () {
return {};
}
};
});
}));
var $rootScope;
beforeEach(inject(function (_$rootScope_) {
return $rootScope = _$rootScope_;
}));
it("defines a value I previously could not test", function () {
return expect($rootScope.value).toEqual('testing');
});
});
I hope this helps your app.run() testing in the future.
I don't know if you are still looking for an answer to this question. But here is some information that might help.
$injector is a singleton for an application and not for a module. However, angular.injector will actually try to create a new injector for each module (I suppose you have a
beforeEach(module("app"));
at the beginning.
I had the same problem while using Angular, RequireJS, Karma and Jasmine and I figured out two ways to solve it. I created a provider for the injector function as a separate dependency in my tests. For example MyInjectorProvider which provides a singleton instance of $injector.
The other way was to move the following statements:
beforeEach(module("app"));
beforeEach(inject(function($injector){
...
})
inside the test suite description. So here is how it looked before:
define(['services/SignupFormValidator'], function(validator){
var validator;
beforeEach(module("app"));
beforeEach(inject(function($injector){
validator = $injector.get("SignupFormValidator");
})
describe("Signup Validation Tests", function(){
it("...", function(){...});
});
});
After applying the fix it looks like this:
define(['services/SignupFormValidator'], function(validator){
var validator;
describe("Signup Validation Tests", function(){
beforeEach(module("app"));
beforeEach(inject(function($injector){
validator = $injector.get("SignupFormValidator");
});
it("...", function(){...});
});
});
Both the solutions worked in my case.
You should mock every HTTP request with ngMock.$httpBackend. Also, here is a guide.
Update
You don't need the angular.mock.module thing, just need to inject your app module. Something like this:
var httpBackend;
beforeEach(module('app'));
beforeEach(inject(function($httpBackend) {
httpBackend = $httpBackend;
$httpBackend.expectGET('/config').respond(200, {'googleAnalyticsAccount': 'something'});
}));
In your tests, when you need the mocked http to answer, you will call httpBackend.flush(). This is why we have a reference to it, so you don't need to inject it in every single test you have.
Note you will need to load angular-mock.js in order to it work.

Categories

Resources