I'd like to create an utility class in Angular.js that can be used by several controllers.
So far I created this:
'use strict';
angular
.module('App')
.factory('AppUtils', AppUtils);
function AppUtils() {
var vm = this;
vm.getPersonOf = getPersonOf;
function getPersonOf(personId, allPersons) {
var person = {};
person.personId = {personId: personId};
allPersons.forEach(function(p) {
if (p.personId === personId) {
person = p;
}
});
return person;
}
}
And I tried to use it in a controller like this:
'use strict';
angular
.module('App')
.controller('AppController', AppController);
function AppController(personId, allPersons, AppUtils) {
var vm = this;
vm.personId = personId;
vm.person = AppUtils.getPersonOf(vm.personId, allPersons);
...
}
But I get this:
PhantomJS 1.9.8 (Windows 7 0.0.0) App should dismiss modal FAILED
Error: [$injector:undef] Provider 'AppUtils' must return a value from $get factory method.
http://errors.angularjs.org/1.5.0/$injector/undef?p0=InvoiceUnitsUtils
(The real names have been renamed to make it easier.)
Why am I getting that error? Am I not declaring properly the Utility module?
The factory is in charge of creating a service and handing its instance to you. To do this, you need to return your utility class:
function AppUtils() {
return {
getPersonOf: getPersonOf
// pass other utility functions...
}
function getPersonOf(personId, allPersons) {
var person = {};
person.personId = {personId: personId};
allPersons.forEach(function(p) {
if (p.personId === personId) {
person = p;
}
});
return person;
}
}
I removed the vm part because we are handing a service which usually has no view model (the controller is in charge of that, service is more of a business logic expert).
Here's some more information about the $get function in Angular's providers:
https://docs.angularjs.org/guide/providers
Related
I am trying to test an Angular service which I know works functionally as its been in place for a while. I've just come to retro fit some tests around it now but I am getting some strange issue where I get a message "must return a value from $get factory method" from the jasmine test.
The service does work in the website and the service returns an API
var service = {
setStartLanguage: setStartLanguage,
setLanguage: setLanguage,
supportedLanguages: supportedLanguages,
currentLanguage: etCurrentLanguage.currentLanguage,
getLanguage: getLanguage
};
return service;
I believe this is enough to get it working but I can't figure out why the test is complaining about it not returning a $get.
Oh and all I'm doing for the test is trying to expect that the service is defined.
EDIT:
Full service code,
(function () {
'use strict';
angular.module('app.core').factory('etLanguageService', ['$translate', '$route', 'etCurrentLanguage', 'supportedLanguages', 'localStorageService', etLanguageService]);
function etLanguageService($translate, $route, etCurrentLanguage, supportedLanguages, localStorageService) {
function setLanugageFromLocalStorage(storedLanguage) {
etCurrentLanguage.currentLanguage = storedLanguage;
$translate.preferredLanguage(etCurrentLanguage.currentLanguage.code);
localStorageService.set('currentLanguage', etCurrentLanguage.currentLanguage);
}
function setLanguageFromBrowser() {
var language = etCurrentLanguage.get();
if (!language) {
language = '';
}
var cleanLanguage = language.substr(0, 2).toLowerCase();
var supportedLanguage = _.find(supportedLanguages, function(language) {
return language.code === cleanLanguage;
});
$translate.preferredLanguage(supportedLanguage.code);
localStorageService.set('currentLanguage', supportedLanguage);
}
function setStartLanguage() {
// first check if we have a stored language
var storedLanguage = localStorageService.get('currentLanguage');
if (storedLanguage) {
setLanugageFromLocalStorage(storedLanguage);
} else {
setLanguageFromBrowser();
}
}
function setLanguage(language) {
if (etCurrentLanguage.currentLanguage !== language) {
etCurrentLanguage.currentLanguage = language;
$translate.use(etCurrentLanguage.currentLanguage.code).then(function () {
localStorageService.set('currentLanguage', etCurrentLanguage.currentLanguage);
$route.reload();
});
}
}
function getLanguage() {
return localStorageService.get('currentLanguage');
}
var service = {
setStartLanguage: setStartLanguage,
setLanguage: setLanguage,
supportedLanguages: supportedLanguages,
currentLanguage: etCurrentLanguage.currentLanguage,
getLanguage: getLanguage
};
return service;
}
}());
That's hard to tell without more test code.
$get is the method from the provider of your service. For each service, there is a provider which is responsible of creating an instance of your service through the $get method. If you have a service called myService, then there is a provider myServiceProvider. Internaly, when bootstraping your app, Angular calls mysServiceProvider.$get to get an instance of your service.
If you're tinkering with providers and mocks of providers, that could be a reason...
Inside the controller I am trying to breakup my code into named functions for readability. However, in the parameterized named functions the scope and the injected dependency are all null. How do access these inside the named functions. Thanks for you help.
(
function() {
'use strict';
var moduleName = 'ufsrAppModule';
var controllerName = 'ufsrController';
var dependencyInjection = ['api', 'appHost', 'userAccount', 'userProfileFactory', 'fsrFactory', 'userFsrFactory', internalFunc];
angular.module(moduleName)
.controller(controllerName, dependencyInjection);
function internalFunc(api, appHost, userAccount, userProfileFactory, fsrFactory, userFsrFactory) {
var vm = this; //controller AS in ng-controller, do not use $scope
init(api, appHost, userAccount, userProfileFactory, fsrFactory, userFsrFactory, vm);
}
function init(api, appHost, userAccount, userProfileFactory, fsrFactory, userFsrFactory, vm) {
vm.facilityChanged = facilityChanged;
...
...
function facilityChanged(vm, fsrFactory) {
/*update UI then retrieve services*/
vm.postStatus = undefined;
vm.services = undefined;
vm.roles = undefined;
vm.services = fsrFactory.service().query({
/*parameters*/
FacilityID: vm.facility
})
.$promise.then(
function(data) {
vm.services = data;
});
}
}
})();
Strict DI can be done separately in this style
angular.module(moduleName)
.controller(controllerName, controllerFunction);
controllerFunction.$inject = ['$scope', '$http'];
function controllerFunction($scope, $http) {
...
}
This style is also recommended by John Papa's Angular style guide.
The facilityChanged is not working because its parameters are overwriting those that are passed into init
It can be fixed by changing
function facilityChanged(vm, fsrFactory) {
to
function facilityChanged() {
Edit: Attached jsbin
I strongly recommend putting the init function inside your controller function to save the parameter passing, just like the activate function in John Papa's guide.
Refined jsbin
I'm trying to use inheritance in angular services, as explained here:
http://blog.mgechev.com/2013/12/18/inheritance-services-controllers-in-angularjs/, I want to use the "Inject the parent" method.
However, it doesn't seem to work, and I can't see why.
var myApp = angular.module('myApp',[]);
angular.module('myApp').controller('MyCtrl', MyCtrl);
angular.module('myApp').factory('BaseModel', BaseModel);
angular.module('myApp').factory('ThreadModel', ThreadModel);
angular.module('myApp').factory('PostModel', PostModel);
function MyCtrl($scope, ThreadModel, PostModel) {
$scope.tableNameForThreads = ThreadModel.getTableName();
$scope.tableNameForPosts = PostModel.getTableName();
}
function BaseModel() {
var tableName = "";
var service = {
init: init,
getTableName: getTableName
};
return service;
function getTableName() {
return tableName;
}
function init(theTableName) {
tableName = theTableName;
}
}
function ThreadModel(BaseModel) {
var service = Object.create(BaseModel);
service.init("threads");
return service;
}
function PostModel(BaseModel) {
var service = Object.create(BaseModel);
service.init("posts");
return service;
}
The result is that ThreadModel.getTableName() returns "posts" in stead of "threads".
I tried both Object.create(...) and angular.copy(BaseModel, this), but both don't seem to make a deep copy.
JSFIDDLE: http://jsfiddle.net/dirkpostma/Lvc0u55v/3989/
What am I doing wrong here?
The problem is that with this set up using Object.create you produce services with the tableName variable stored in the same common closure (BaseModel function). To put it simply, init method modifies the same local tableName variable.
You could fix it like this:
function BaseModel() {
var service = {
init: init,
getTableName: getTableName
};
return service;
function getTableName() {
return this._tableName;
}
function init(theTableName) {
this._tableName = theTableName;
}
}
Note, that getTableName and init methods now work with instance property this._tableName which is not shared between TableModel and PostModel instances.
Demo: http://jsfiddle.net/Lvc0u55v/3991/
#dfsq has already well explained and given a simple solution. I put here what I am thinking about this issue.
In your code Object.create(BaseModel) creates a new object whose prototype is a returned value of BaseModel function. In those children models init method modifies tableName within the local scope of BaseModel function. If you replace tableName with this.tableName, that will work as you expected: both init and getTableName methods will actually modify/call tableName property of service variable within ThreadModel or PostModel functions. But it looks complicated.
In your case I would like suggest the following service inheritance solution, which would be clearer. There is an other post that can be interesting.
var myApp = angular.module('myApp', []);
angular.module('myApp').controller('MyCtrl', MyCtrl);
angular.module('myApp').service('BaseModel', BaseModel);
angular.module('myApp').service('ThreadModel', ['BaseModel', ThreadModel]);
angular.module('myApp').service('PostModel', ['BaseModel', PostModel]);
function MyCtrl($scope, ThreadModel, PostModel) {
$scope.tableNameForThreads = ThreadModel.getTableName();
$scope.tableNameForPosts = PostModel.getTableName();
}
function BaseModel() {
this.tableName = "";
this.getTableName = function() {
return this.tableName;
}
this.init = function(theTableName) {
this.tableName = theTableName;
}
}
function ThreadModel(BaseModel) {
angular.extend(ThreadModel.prototype, BaseModel);
this.tableName = "threads";
}
function PostModel(BaseModel) {
angular.extend(PostModel.prototype, BaseModel);
this.tableName = "posts";
}
JSFiddle: http://jsfiddle.net/Lvc0u55v/3993/
i'm currently just trying to test if getTodaysHours function on my controller has been called. ultimately the function should get hours from the mock JSON data and pass if parameters match, but i'm stuck on the first part.
vendor.controller
export class VendorController {
constructor($rootScope, data, event, toastr, moment, _, distanceService, vendorDataService, userDataService, stateManagerService) {
'ngInject';
//deps
this.$rootScope = $rootScope;
this.toastr = toastr;
this._ = _;
this.userDataService = userDataService;
this.vendorDataService = vendorDataService;
this.stateManagerService = stateManagerService;
this.event = event;
//bootstrap
data.isDeepLink = true;
this.data = data;
this.data.last_update = moment(this.data.updated_at).format('MM/DD/YY h:mm A');
this.data.distance = distanceService.getDistance(this.data.loc.lng, this.data.loc.lat);
this.data.todaysHours = this.getTodaysHours();
this.data.rating_num = Math.floor(data.rating);
this.hasReviewed = (userDataService.user.reviewed[data._id]) ? true : false;
this.isGrid = false;
this.isSearching = false;
this.hideIntro = true;
this.menuCollapsed = true;
this.filterMenuCollapsed = true;
this.selectedCategory = 'All';
this.todaysHours = '';
this.type = '';
this.searchString = '';
this.reviewScore = 0;
this.today = new Date().getDay();
this.vendorDataService.currentVendor = data;
//load marker onto map
$rootScope.$broadcast(event.ui.vendor.pageLoad, data);
//get menu
vendorDataService.getVendorMenu(data._id)
.then((res)=> {
this.data.menu = res.menu;
this.menuContainer = this.data.menu;
this.totalResults = this.getTotalResults();
this.availableMenuCategories = this.getAvailableMenuCategories();
})
.catch(() => {
this.toastr.error('Whoops, Something went wrong! We were not able to load the menu.', 'Error');
});
}
//get todays hours
getTodaysHours() {
let today = this.data.hours[new Date().getDay()];
return (today.opening_time || '9:00am') + ' - ' + (today.closing_time || '5:00pm');
}
}
the first test passes when I mock the JSON data with $provide constant
describe('vendor controller', () => {
let vm,
data = {"_id":"56b54f9368e685ca04aa0b87","lat_lon":"33.713018,-117.841101","hours":[{"day_of_the_week":"sun","closing_time":" 7:00pm","opening_time":"11:00am","day_order":0,"id":48880},...];
beforeEach(angular.mock.module('thcmaps-ui', ($provide) => {
$provide.constant('data', new data);
}));
//first test
it('should pass', () => {
expect(data._id).toEqual('56b54f9368e685ca04aa0b87');
});
//second test
it('should call getTodaysHours', () => {
expect(vm.getTodaysHours()).toHaveBeenCalled();
});
});
then I tried to inject the controller (not sure if correct syntax):
beforeEach(angular.mock.module('thcmaps-ui', ($provide) => {
$provide.constant('data', new data);
}));
beforeEach(inject(($controller) => {
vm = $controller('VendorController');
spyOn(vm,'getTodaysHours').and.callThrough();
}));
and it gives me some kind of forEach error. the second test gives me a undefined error when evaluating vm.getTodaysHours():
PhantomJS 2.1.1 (Mac OS X 0.0.0) vendor controller should pass FAILED
forEach#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:341:24
loadModules#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4456:12
createInjector#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4381:22
workFn#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular-mocks/angular-mocks.js:2507:60
/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4496:53
PhantomJS 2.1.1 (Mac OS X 0.0.0) vendor controller should call getTodaysHours FAILED
forEach#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:341:24
loadModules#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4456:12
createInjector#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4381:22
workFn#/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular-mocks/angular-mocks.js:2507:60
/Users/adminuser/Documents/workspace/thcmaps-ui/bower_components/angular/angular.js:4496:53
TypeError: undefined is not an object (evaluating 'vm.getTodaysHours') in /Users/adminuser/Documents/workspace/thcmaps-ui/.tmp/serve/app/index.module.js (line 9)
/Users/adminuser/Documents/workspace/thcmaps-ui/.tmp/serve/app/index.module.js:9:244419
You need to inject the dependencies of your controller when instantiating it with $controller. For example, consider the following controller:
class MyController {
constructor($rootScope, $log) {
// Store the controllers dependencies
this.$rootScope = $rootScope;
this.$log = $log;
}
// Return obituary value from the $rootScope
getValue() {
this.$log.debug('Retrieving value');
return this.$rootScope.foobar;
}
// Get the current date
getDate() {
this.$log.debug('Getting date');
return Date.now()
}
static get $inject() {
return ['$scope', '$log'];
}
}
I've written this controller using ES6, note that the dependencies are defined within the static $injectgetter at the foot of the class declaration. This will be picked up by AngularJS upon instantiation.
As you can see, the controller depends upon the $rootScope and the $log provider. When mocking this controller for testing purposes, you must inject the controllers dependencies like this:
describe('Spec: MyController', () => {
var controller;
beforeEach(inject(($rootScope, $log, $controller) => {
controller = $controller('MyController', {
$rootScope,
$log
});
});
it('should return a value from the $rootScope', () => {
var value = controller.getValue();
// ... perform checks
});
it('should return the current date', () => {
var date = controller.getDate();
// ... perform checks
});
});
More recent versions of Jasmine enable developers to leverage the this keyword throughout their tests.
Any beforeEach, afterEach, and it declarations will all share the same reference to this, allowing you to avoid creating enclosed variables (like var controller, as seen above) and also avoid creating unnecessary globals. For example:
beforeEach(inject(function ($rootScope, $log, $controller) {
this.controller = $controller('MyController', {
$rootScope,
$log
});
});
it('should return a value from the $rootScope', function () {
this.value = controller.getValue();
// ... perform checks
});
Note the second argument in the call to $controller, this must be an object containing the expected dependencies that your controller ('MyController', in this case) relies upon.
The reasoning behind this is simply to allow developers to pass mock services, factories, providers, etc to the controller as an alternative to spies.
$controller: https://docs.angularjs.org/guide/unit-testing
this: http://jasmine.github.io/2.0/introduction.html
Apologies for the unuseful link to the Jasmine documentation regarding the usage of this with tests, I couldn't add a direct link to the correct section of the page due to how their anchor tags are set out (the anchor tag contains a <code></code> block, doh!).
I am trying to include a library of functions, held in a factory, into a controller.
Similar to questions like this:
Creating common controller functions
My main controller looks like this:
recipeApp.controller('recipeController', function ($scope, groceryInterface, ...){
$scope.groceryList = [];
// ...etc...
/* trying to retrieve the functions here */
$scope.groceryFunc = groceryInterface; // would call ng-click="groceryFunc.addToList()" in main view
/* Also tried this:
$scope.addToList = groceryInterface.addToList();
$scope.clearList = groceryInterface.clearList();
$scope.add = groceryInterface.add();
$scope.addUp = groceryInterface.addUp(); */
}
Then, in another .js file, I have created the factory groceryInterface. I've injected this factory into the controller above.
Factory
recipeApp.factory('groceryInterface', function(){
var factory = {};
factory.addToList = function(recipe){
$scope.groceryList.push(recipe);
... etc....
}
factory.clearList = function() {
var last = $scope.prevIngredients.pop();
.... etc...
}
factory.add = function() {
$scope.ingredientsList[0].amount = $scope.ingredientsList[0].amount + 5;
}
factory.addUp = function(){
etc...
}
return factory;
});
But in my console I keep getting ReferenceError: $scope is not defined
at Object.factory.addToList, etc. Obviously I'm guessing this has to do with the fact that I'm using $scope in my functions within the factory. How do I resolve this? I notice that in many other examples I've looked at, nobody ever uses $scope within their external factory functions. I've tried injecting $scope as a parameter in my factory, but that plain out did not work. (e.g. recipeApp.factory('groceryInterface', function(){ )
Any help is truly appreciated!
Your factory can't access your $scope, since it's not in the same scope.
Try this instead:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.addToList = groceryInterface.addToList;
$scope.clearList = groceryInterface.clearList;
$scope.add = groceryInterface.add;
$scope.addUp = groceryInterface.addUp;
}
recipeApp.factory('groceryInterface', function () {
var factory = {};
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
factory.clearList = function() {
var last = this.prevIngredients.pop();
}
});
Alternatively, you can try using a more object oriented approach:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.groceryFunc = new groceryInterface($scope);
}
recipeApp.factory('groceryInterface', function () {
function Factory ($scope) {
this.$scope = $scope;
}
Factory.prototype.addToList = function (recipe) {
this.$scope.groceryList.push(recipe);
}
Factory.prototype.clearList = function() {
var last = this.$scope.prevIngredients.pop();
}
return Factory;
});
You cannot use $scope in a factory as it is not defined. Instead, in your factory functions change the properties of the object the factory is returning, e.g.
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
these will then get passed on to your $scope variable
$scope.addToList = groceryInterface.addToList;
// ... = groceryInterface.addToList(); would assign to `$scope.addToList` what is returned, instead of the function itself.
This isn't the exact answer for this question, but I had a similar issues that I solved by simply passing $scope as an argument to a function in my factory. So it won't be the normal $scope, but $scope at the time the function in the factory is called.
app.controller('AppController', function($scope, AppService) {
$scope.getList = function(){
$scope.url = '/someurl'
// call to service to make rest api call to get data
AppService.getList($scope).then(function(res) {
// do some stuff
});
}
});
app.factory('AppService', function($http, $q){
var AppService = {
getList: function($scope){
return $http.get($scope.url).then(function(res){
return res;
});
},
}
return AppService;
});