I've got a simple factory passing data gathered from Mongolab into one controller, but I can't get it in another.
app.factory('User', function ($mongolabResourceHttp) {
return $mongolabResourceHttp('users');
});
I've also got a Goal factory just like this. Both work fine as-is.
The above factory is working fine in my userCtrl, but I can't add it into my goalCtrl. I've got a userguid that I want to attach to goals. Basically: click link to user goals, create goals, userguid becomes part of saved goal.
app.controller('goalCtrl', function ($scope, $location, goals, users) {
$scope.goals = goals;
$scope.users = users;
$scope.new = function () {
$location.path('/');
};
...
});
Any clues would be much appreciated. Thanks.
(I asked a similar question earlier today, but I think it had too many issues going on and it didn't get any responses. I have since worked out one of the issues and this question is to narrow it down further. So this isn't a complete repeat. Sorry if it bugs anyone though.)
You need to inject the service into your controller using the actual service name of 'User':
app.controller('goalCtrl', function ($scope, $location, goals, User) {
$scope.users = User;
I'd also read up on angular service and dependency injection when you get a chance.
Related
I'm brand new to testing, and I've been trying to find the best strategy for unit testing an AngularJS controller with a service dependency. Here's the source code:
app.service("StringService", function() {
this.addExcitement = function (str) {
return str + "!!!";
};
});
app.controller("TestStrategyController", ["$scope", "StringService", function ($scope, StringService) {
$scope.addExcitement = function (str) {
$scope.excitingString = StringService.addExcitement(str);
};
}]);
And the test I'm using currently:
describe("Test Strategy Controller Suite", function () {
beforeEach(module("ControllerTest"));
var $scope, MockStringService;
beforeEach(inject(function ($rootScope, $controller) {
$scope = $rootScope.$new();
MockStringService = jasmine.createSpyObj("StringService", ["addExcitement"]);
$controller("TestStrategyController", {$scope: $scope, StringService: MockStringService});
}));
it("should call the StringService.addExcitement method", function () {
var boringString = "Sup";
$scope.addExcitement(boringString);
expect(MockStringService.addExcitement).toHaveBeenCalled();
});
});
This test passes, but I'm confused about something: if I change the name of the method in the service (let's say I call it addExclamations instead of addExcitement but not where it is used in the controller (still says $scope.excitingString = StringService.addExcitement(str);), my tests still pass even though my controller is now broken. However, once I change the method name in the controller as well, so as to fix the actual breakage caused by changing the service's method name, my tests break because it's trying to call the old addExcitement method.
This would indicate that I would need to manually keep the method names in sync with the service by changing the jasmine spy object line to MockStringService = jasmine.createSpyObj("StringService", ["addExclamations"]);.
All of this seems backwards to me, since I feel like my test should break when I change the service's method name without changing how the controller references that service name. But I'm not sure how to get the best of both worlds here, because if I'm expecting my test to keep track of that service name somehow, there's no way for it to pass again when I change the method name in both the service and the controller because the spyObj still has the old name.
Any insight or advice about the strategy behind this would be greatly appreciated. I'm going to be teaching this to some students, and am mostly trying to make sure I'm following best practices with this.
I'd say that's the expected result of the way your test code works, simply because you created a "brand new" mock service object. I guess you know what I am talking about.
What I usually do is get the service instance and mock the method, instead of creating a completely new mock object.
beforeEach(inject(function ($rootScope, $controller, $injector) {
$scope = $rootScope.$new();
MockStringService = $injector.get('StringService');
spyOn(MockStringService , 'addExcitement').andReturn('test');
$controller("TestStrategyController", {$scope: $scope, StringService: MockStringService});
}));
please note that andReturn() is a jasmine 1.x method, depends on the version you are using, you may want to change the code a little bit.
Having it this way, if you change the method name in StringService, you should get errors from spyOn() method, as the method doesn't not exist any more.
Another thing is that, you don't have to use $injector as I did to get the service instance, you can just inject your service instead in fact. I don't recall why I did it this way. :)
So, over the last month I've been diving pretty hard into AngularJS and my controllers got extremely big. I decided to learn about factories and services so I could separate some of my logic...which leads me to my question.
I see a lot of examples online where people are using ajax calls to the server from their controllers. Is this a good practice? The more I study AngularJS and compare it to what I already know about software design, I see the controller's job pass data into the view. Much like a controller in MVC, where business logic shouldn't exist. Is that a safe assumption?
Also -- If I were to move my ajax calls over to a factory or service, would the functionality be the same? Something like below?
Service
app.service('orderService', ['$http', function ($http) {
var orderdata = {};
// Gets orders from WebAPI
this.getRecentOrders = function () {
$http.get('/api/orders').success(function (data) {
this.orderdata = data;
});
return this.orderdata;
};
}]);
Controller
$scope.recentOrders = orderService.getRecentOrders();
edit
I had to modify the code in the controller slightly from the answer to get it to work correctly:
orderService.getRecentOrders().then(function(result) {
$scope.recentOrders = result.data;
});
Services should return an object with methods. Also, the method you invoke should return the promise as it's an async call, so the return this.orderdata will always be undefined (or at least the first time):
app.service('orderService', ['$http', function ($http) {
return {
getRecentOrders: function () {
return $http.get('/api/orders');
}
};
}]);
No with this, you can chain to that promise in order to get the data in your controller:
orderService.getRecentOrders().then(function(data) {
$scope.recentOrders = data;
});
Now as per the question itself, basically, controllers should be "dumb", what does this mean? it should not have any heavy logic within. It should only be the layer that gets data from the services, and use them to show them in the $scope. Everything related to heavy logic (data processing and such) should be apart from the controller. That's why angular provides you with Filters, Services and Directives.
I have a sidebar that contains a feed from various social medias, along with a service in AngularJS that queries my API for the data. Below is the controller, along with the service (in that order). Why isn't it being executed on page load, and how should I rewrite my code to make it load the data when the page is rendered to the client?
angular.module('HomeCtrl', ['MediaServ']).controller('HomeController', [
'$scope',
'MediaServ',
'$rootScope',
function($scope, $rootScope, MediaServ){
if ($rootScope){
$scope = $rootScope;
}
function handleRes(response){
console.log('hello!');
}
angular.element(document).ready(function () {
$scope.SocialMedia = function(){
MediaServ.feed()
.then(handleRes, handleRes);
}
});
}]);
angular.module('MediaServ', []).service('MediaServ', [
'$http',
function($http){
this.feed = function(){
return $http.get('/api/social/feed');
}
}]);
You can only use things (be it services, factories, filters, etc) from another module if you have first injected that module into your current one. As the code above is written your modules don't know about each other, and so you can't inject MediaServ into HomeController.
To fix it, inject the MediaServ module into the HomeCtrl module like this:
angular.module('HomeCtrl', ['MediaServ'])...
I will also suggest not shortening names (minifiers should shorten things, developers should not) and not using the same name for services and apps. The last one in particular can cause a lot of confusion in a large project. Personally I prefer to name modules things like "media.services" and services "MediaService" but that is personal taste, just keep a clear naming convention so that you always know what is what.
You shouldn't need to wrap your code in angular.element(document).ready(function () { });
The controller will execute on page load automatically, provided it is reference in the page.
So, UncleDave and Erik Honns' responses both led me to realize what was wrong (on top of having a typo in handleRes). Here is my new code:
angular.module('HomeCtrl', ['MediaServ']).controller('HomeController', [
'$scope',
'$rootScope',
'MediaServ',
function($scope, $rootScope, MediaServ){
if ($rootScope){
$scope = $rootScope;
}
function handleRes(response){
if (response.data.tweets){
$scope.SocialMedia = response.data.tweets;
}
}
MediaServ.feed()
.then(handleRes, handleRes);
}]);
It is now working. Thank you everybody for your help. I won't pick a best answer, and instead will let others vote. Feel free to flag this question to be deleted, as the errors were more on my side (kind of being lazy about reading through my code, but I thought that this was one of those Angular things you just kind of have to learn).
Thanks everyone for your help!
So, this isn't the typical question of HOW to do it. I know it can be done with a service or a factory but I was wondering if someone could share what the advantages/disadvantages would be of just creating a basic service, injecting it into each controller, and then extending the service with functions from each controller. Something similar to the following example..
app.service('HelperService', function() {
return {};
});
app.controller('Controller1', function($scope, HelperService) {
$scope.somefunc = function() {
//do stuff here
};
HelperService.somefunc = $scope.somefunc;
});
app.controller('Controller2', function($scope, HelperService) {
HelperService.somefunc();
});
This works, and works well. I feel a bit stupid for asking this but it just seems like I'm missing something here as to why this isn't used or recommended?
It may work, but its a bad idea.
Controller2 HelperService.somefunc() won't exist until Controller1 has been instantiated. So theres an implicit dependency of Controller2 on Controller1
the code on HelperService isn't in one place where it can be understood together
if you are doing some sort of data manipulation in that function, it really should be operating on data encapsulated by the HelperService.
The service is a singleton and it will be instantiated once calling new on the function itself -- the function you pass in is essentially a constructor. This will create the empty object you are returning for use everywhere, but if you want to return an object in such a way it makes more sense to use .factory, but this is not a big deal.
At any rate, you can consider your code to conceptually do this:
var HelperService = function () {}
var helperService = new HelperService;
function Controller1() {
helperService.someFunc = function () {}
}
function Controller2() {
helperService.someFunc();
}
I would consider this a dangerous thing to do for a couple of reasons:
Controller1 must be instantiated before Controller2 or else somefunc won't be available to Controller2. Ideally the controllers would have no knowledge of each other.
You are coupling Controller/ViewModel (since you're using scope) with service level logic, but these should be decoupled. HelperService shouldn't know about the controllers either. Instead, you should be injecting a service that has an API that the controllers expect to use. This doesn't always have to be HelperService, it just has to look like HelperService to the controllers and its API shouldn't change.
Without knowing specifics about what you're trying to do, it's hard to advise. In general you may rethink what you want to do, but you can extend functionality of services with other services. Consider services to be in their own layer.
The code below represents a situation where the same code pattern repeats in every controller which handles data from the server. After a long research and irc talk at #angularjs I still cannot figure how to abstract that code, inline comments explain the situations:
myApp.controller("TodoCtrl", function($scope, Restangular,
CalendarService, $filter){
var all_todos = [];
$scope.todos = [];
Restangular.all("vtodo/").getList().then(function(data){
all_todos = data;
$scope.todos = $filter("calendaractive")(all_todos);
});
//I can see myself repeating this line in every
//controller dealing with data which somehow relates
//and is possibly filtered by CalendarService:
$scope.activeData = CalendarService.activeData;
//also this line, which triggers refiltering when
//CalendarService is repeating within other controllers
$scope.$watch("activeData", function(){
$scope.todos = $filter("calendaractive")(all_todos);
}, true);
});
//example. another controller, different data, same relation with calendar?
myApp.controller("DiaryCtrl", function($scope, Restangular,
CalendarService, $filter){
//this all_object and object seems repetitive,
//isn't there another way to do it? so I can keep it DRY?
var all_todos = [];
$scope.todos = [];
Restangular.all("diary/").getList().then(function(data){
all_diaries = data;
$scope.diaries = $filter("calendaractive")(all_diaries);
});
$scope.activeData = CalendarService.activeData;
$scope.$watch("activeData", function(){
$scope.todos = $filter("calendaractive")(all_diaries);
}, true);
});
DRY should be followed purposefully, not zealously. Your code is fine, the controllers are doing what they are supposed to be doing: connecting different pieces of the app. That said, you can easily combine repeated code in a factory method that returns a function reference.
For example,
myApp.factory('calendarScopeDecorator', function(CalendarService, Restangular, $filter) {
return function($scope, section) {
$scope.todos = [];
$scope.activeData = CalendarService.activeData;
Restangular.all(section+"/").getList().then(function(data){
$scope.all_data = data;
$scope.filtered_data = $filter("calendaractive")(data);
});
$scope.$watch("activeData", function(){
$scope.todos = $filter("calendaractive")($scope.all_data);
}, true);
}
});
And then incorporate this into your controller:
myApp.controller("DiaryCtrl", function($scope, calendarScopeDecorator){
calendarScopeDecorator($scope, 'diary');
});
I wouldn't do this kind of thing with a watcher and local reference like in this controller. Instead, I would use $on() / $emit() to establish a pub-sub pattern from the service out to the controllers that care about its updates. This is an under-used pattern IMO that provides a more "DRY" mechanism. It's also extremely efficient - often more so than a watcher, because it doesn't need to run a digest to know something has changed. In network-based services you almost always know this for certain, and you don't need to go from knowing it for certain to implying it in other locations. This would let you avoid the cost of Angular's deep inspection of objects:
$rootScope.$on('calendarDiariesUpdated', function() {
// Update your $scope.todos here.
}, true);
In your service:
// When you have a situation where you know the data has been updated:
$rootScope.$emit('calendarDiariesUpdated');
Note that emit/on are more efficient than using broadcast, which will go through all nested scopes. You can also pass data from the service to listening controllers this way.
This is a really important technique that does a few things:
You no longer need to take a local reference to activeData, since you aren't actually using it (it's DRY).
This is more efficient in most/many cases than a watcher. Angular doesn't need to work out that you need to be told of an update - you know you do. This is also kind of a DRY principle - why use a framework tool to do something you don't actually need? It's an extra step to put the data somewhere and then wait for Angular to digest it and say "whoah, you need to know about this."
You may even be able to reduce your injections. There's no need to take CalendarService because that service can pass a reference to the array right in its notification. That's nice because you don't need to refactor this later if you change the storage model within the service (one of the things DRY advocates also advocate is abstracting these things).
You do need to take $rootScope so you can register the watcher, but there's nothing in pub-sub concepts that violate DRY. It's very common and accepted (and most important: it performs very well). This isn't the same thing as a raw global variable, which is what scares people off from using $rootScope in the first place (and often rightly so).
If you want to be "Super DRY" you can re-factor the call to $filter into a single method that does the filtering, and call it both from your REST promise resolution and from the calendar-update notification. That actually adds a few lines of code... but doesn't REPEAT any. :) That's probably a good idea in principle since that particular line is something you're likely to maintain (it takes that static "calendaractive" parameter...)
It looks like the code in the 2 controllers is identical except for the path to which the API call is made: "vtodo/" or "diary/".
One way to achieve something closer to DRY-ness is to pass the API path as an option to the controller as an attribute. So, assuming we call the controller ApiController, this can be used as
<div ng-controller="ApiController" api-controller-path="vtodo/">
<!-- Todo template -->
</div>
and
<div ng-controller="ApiController" api-controller-path="diary/">
<!-- Diary template -->
</div>
Which is then accessible in the controller by the injected $attrs parameter:
myApp.controller("ApiController", function($scope, $attrs, Restangular, CalendarService, $filter) {
// "vtodo/" or "diary/"
var apiPath = $attrs.apiControllerPath;
As a caution I would beware of over-architecting, not everything needs to be factored out, and there is an argument that you are just following a design pattern rather than copy+pasting code. However, I have used the above method of passing options to a controller for a similar situation myself.
If you are worried about making multiple calls to the same resource CalendarService, I'd recommend finding a way to cache the result, such as defining a variable in that service or using Angular's $cacheFactory.
Otherwise, I don't see anything wrong with your patterns.