AngularJS - self referencing services? - angularjs

I'm building an Angular app that will have a top-level Controller and a second-level controller. There will be n number of second-level controllers, but I want to put global-level functions someplace. I'm doing this in a service.
I'm starting down the path of creating a single service that return an api, really, containing lots of functions (below). The service is returning an object with two property branches that each contain a set of functions. How can I call one of these from the other?
globalModule.factory('global', function($http) {
var squares = MyApp.squares; // this is the *only* link from Global namespace to this app
return {
squareMgr: {
getSquaresEarned: function() {
return squares.earned;
},
getSquaresPlaced: function() {
return squares.placed;
},
setThisSquareEarned: function(value) {
squares.earned.push(value);
},
setThisSquarePlaced: function(value) {
squares.placed.push(value);
}
},
missionMgr: {
missionInfo: {},
setMissionInfo: function(missionInfo) {
this.missionInfo = missionInfo
},
complete: function(missionData) {
log('complete called on video at ' + new Date());
missionData.complete = true;
log(angular.toJson(missionData));
$http({
url: '/show/completeMission',
method: "POST",
data: missionData
})
.then(function(response) {
if (response.data.success === true) {
log('completeMission success');
// increment squares earned counter
this.squareMgr.setThisSquareEarned(missionData.id);
// above is an attempt to run a function contained in this
// same service in a different parent property branch.
// how *should* I do this?
}
});
}
}
}
});

How about something like this:
globalModule.factory('global', function($http) {
var glob = {
squareMgr: {
// ...
},
missionMgr: {
foo: function() {
glob.squareMgr.xyz();
}
}
};
return glob;
});

Related

angularjs unit testing unable to inject a service

I have a really simple service:
'use strict';
angular.module('sapphire.orders').service('deliveryDatesService', service);
function service() {
return {
clearAddressReason: clearAddressReason,
getMinDate: getMinDate
};
//////////////////////////////////////////////////
function clearAddressReason(model, dateReasons, reasons) {
if (model.manualAddress) {
model.overrideDates = true;
reasons.date = dateReasons.filter(function (item) {
return item.value === 'D4';
})[0];
} else {
reasons.address = null;
if (!reasons.date || reasons.date.value === 'D4') {
reasons.date = null;
model.overrideDates = false;
}
}
};
function getMinDate(model) {
var now = new Date();
// If we are not overriding dates, set to today
if (!model.overrideDates) return now;
// If dates are overriden, then the min date is today + daysToDispatch
return new Date(now.setDate(now.getDate() + model.daysToDispatch));
};
};
It has no dependencies, so I want to test the methods.
So I have tried to create a spec like this:
'use strict';
describe('Service: deliveryDatesService', function () {
beforeEach(module('sapphire.orders'));
var service,
reasons,
dateReasons;
beforeEach(inject(function (deliveryDatesService) {
console.log(deliveryDatesService);
service = deliveryDatesService;
reasons = {};
dateReasons = [{ value: 'D4' }];
}));
it('can create an instance of the service', function () {
expect(service).toBeDefined();
});
it('if manual delivery address is true, then override dates should be true', function () {
var model = { manualDeliveryDate: true };
service.clearAddressReason(model, dateReasons, reasons);
expect(model.overrideDates).toBe(true);
});
it('if manual delivery address is false, then override dates should be false', function () {
var model = { manualDeliveryDate: false };
service.clearAddressReason(model, dateReasons, reasons);
expect(model.overrideDates).toBe(false);
});
it('minimum date cannot be less than today', function () {
var model = { };
var minDate = service.getMinDate(model);
var now = new Date();
expect(minDate).toBeGreaterThan(now);
});
});
But my service is always undefined. Can someone tell me what I am doing wrong please?
Update
So, it turns out this is to do with one or more services interfering somehow.
In my karma.conf.js I had declared all my bower applications and then this:
'src/app/app.module.js',
'src/app/**/*module.js',
'src/app/**/*constants.js',
'src/app/**/*service.js',
'src/app/**/*routes.js',
'src/app/**/*.js',
'test/spec/**/*.js'
I created a test service in the root of my scripts directory and then created a spec file to see if it was created. It moaned at me about a reference error in a file that was not related at all. It moaned about this bit of code:
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
stating that Modernizr is not defined.
I changed the code to this:
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
and it started working. But my other test was not.
So I changed my references in karma.conf.js to this:
'src/app/app.module.js',
'src/app/orders/orders.module.js',
'src/app/orders/shared/*.js',
'test/spec/**/*.js'
and it started working.
That leads me to believe there is something wrong with my application somewhere. Maybe another reference like Modernizr. I still have an outstanding question though. How can services that are not dependant on another service interfere?
I think it's worth noting that each service, controller, directive is in it's own file and they all follow this structure:
(function () {
'use strict';
angular.module('sapphire.core').factory('options', service);
function service($rootScope) {
return {
get: get,
save: save
};
//////////////////////////////////////////////////
function get() {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
var storageData = angular.fromJson(localStorage.options);
if (storageData) {
return angular.fromJson(storageData);
}
}
return {
background: {
enabled: true,
enableSnow: true,
opacity: 0.6
}
};
};
function save(options) {
if (typeof Modernizr == 'object' && Modernizr.localstorage) {
localStorage.options = angular.toJson(options);
$rootScope.$options = get();
}
};
};
})();
I am wondering that because I wrap them in anonymous functions that execute themselves, is that what is causing this problem?
* Solution *
So in the end I found out exactly what was causing this issue. It was indeed to do with the file in karma.conf.js. I had told it to load all files and somewhere in there was something it didn't like.
After a bit of playing I finally found what it was and thought I would share it just in case someone else gets here.
The issue was routes. I am using ui.router and it appears that having them in your tests fail.
I changed my files section to this:
'src/app/app.module.js',
'src/app/**/*module.js',
'src/app/**/*constants.js',
'src/app/**/*service.js',
'src/app/**/*controller.js',
//'src/app/**/*routes.js',
'test/spec/**/*.js'
As you can see I have a routes file(s) commented out. If I bring them back in, everything fails.
I think your inner deliveryDatesService variable is hiding the external one.
To avoid that, you can try puting underscores around the inner service variable as per the spec on the website below:
https://docs.angularjs.org/api/ngMock/function/angular.mock.inject
Look for
'Resolving References (Underscore Wrapping)'
on that page.
Then your code would look like this:
beforeEach(inject(function (_deliveryDatesService_) {
console.log(_deliveryDatesService_);
service = _deliveryDatesService_;
reasons = {};
dateReasons = [{ value: 'D4' }];
}));
Also you need to make sure that all required files and directories are declared in karma.conf.js so that the test framework can use them.

Typeahead search function

So I am using the typeahead directive that's part of Angular UI project. I have a function that calls a factory (using $resource to call an api). The first function didn't work but the second one did. What's happening differently here? I assumed these would produce the exact same result but apparently I am wrong:
// this didn't work, it doesn't display a list of items in typeahead, no errors.
$scope.getLocation = function(val) {
return LocationService.search({ term: val }, function (res) {
return res.data.map(function (item) {
return item;
});
});
};
// this worked
$scope.getLocation = function(val) {
return LocationService.search({ term: val }).$promise.then(function (res){
return res.data.map(function (item) {
return item;
});
});
};
Do you have $resource wrapped in LocationService? Something like:
function LocationService($resource) {
return {
search : function(query){
var locationResource = $resource('url/',
{},
{
search : {
method: 'POST',
responseType : 'json'
}
});
return locationResource.search({ q : query });
}
};
}
If so, in your first example, you're just passing a callback as second variable to LocationService, which isn't handled in the function definition. The returned function of $resource can take a callback as a second parameter, but not if you've wrapped it. If you wanted, you could pass the callback to the service itself, like:
function LocationService($resource) {
return {
search : function(query, cb){
var locationResource = $resource('url/',
{},
{
search : {
method: 'POST',
responseType : 'json'
}
});
return locationResource.search({ q : query }, cb);
}
};
}

Backbone.js View with multiple collections and sorting

I have a view with multiple collections, implemented like this:
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
And example collection is like this:
var FolderCollection = Backbone.Collection.extend({
model: folderObj,
initialize:function (){
// this.bindAll(this);
// this.setElement(this.at(0));
},
comparator: function(model) {
return model.get("id");
},
getElement: function() {
return this.currentElement;
},
setElement: function(model) {
this.currentElement = model;
},
next: function (){
this.setElement(this.at(this.indexOf(this.getElement()) + 1));
return this;
},
prev: function() {
this.setElement(this.at(this.indexOf(this.getElement()) - 1));
return this;
}
});
As you can imagine, this View is a display for files, images, and folders. I then populate the view by calling three different functions; one to populate the view with folders, another for files, and another for images. Each of these functions is a separate ajax request. So, because these calls are asynchronous, there's no way to first load folders, then images, then files and there is no consistency when the page loads.
So, my problem is, I need to be able to order these three collections in multiple ways. The first problem is, since the calls are async, sometimes the folders load first, or maybe the files, etc. I can think of two ways to fix this:
Only call the next function after the previous is completed. Is this the best way? If so, how do I do that
After all the collections are loaded, sort them. If so, how is the best way to sort and order multiple collections?
If more code is needed (ie: model or view) please let me know and I can provide what ever is needed.
thanks
jason
EDIT - SHOWING VIEW
var FileManagementView = TemplateView.extend({
viewName: 'fileManagement',
className: 'fileManagement',
events: {
//my events
},
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
//life cycle
initialize: function (options) {
TemplateView.prototype.initialize.apply(this, [options]);
},
templateContext: function (renderOptions) {
},
postRender: function () {
//more functions to set up the view
this.repopulateViewWithFoldersAndFiles(currentFolderId);
},
template: function (renderOptions) {
return 'MyMainTemplate';
},
repopulateViewWithFoldersAndFiles: function(currentFolderId){
//code to do stuff to create view
//these functions are all async, so theres no way to know what will finish first
this.getFolders(currentFolderId);
this.getImages();
this.getFiles();
},
getFiles: function(){
try{
var that = this;
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFiles'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var file = new fileObj;
file.set('id', value.id);
file.set('fileName', value.fileName);
//...set more attributes
that.collection.files.add(file);
that.renderFile(file);
});
}
});
} catch(e){
throw e;
}
},
renderFile: function(file){
try{
if(file) {
var template = window.app.getTemplate('AnotherTemplate');
var html = $(template({
id: file.get('id'),
fileName: file.get('fileName'),
fileIconPath: file.get('fileIconPath')
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getImages: function(){
try{
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getImages'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var image = new imageObj;
image.set('id', value.id);
image.set('imgTitle', value.image_name);
//....set more attributes
that.collection.images.add(image);
that.renderImage(image);
});
}
});
} catch(e){
throw e;
}
},
renderImage: function(image){
try{
if(image) {
var template = window.app.getTemplate('myViewTemplate');
var html = $(template({
imgId: image.get('id'),
imgTitle: image.get('imgTitle'),
//..more attributes
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getFolders:function(parentId){
var that = this;
var url = '?q=...path to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFolders'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var folder = new folderObj();
folder.set('folderName', value.folder_name);
folder.set('id', value.folder_id);
//more attributes
that.collection.folders.add(folder);
that.renderFolders(folder);
});
}else{
this.renderFolders(null);
}
});
},
//renders the folders to the view
renderFolders: function(folder){
try{
if(folder) {
var template = window.app.getTemplate('myFolderTemplate');
var html = $(template({
folderId: folder.get('id'),
folderName: folder.get('folderName'),
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
}
});
What I ended up doing was rewriting my models and creating one model that the others inherit from. Example:
var DataModel =MyBaseModel.extend({
defaults: {
id: null,
//other shared fields
}
});
All my other models inherited, like this:
var folderObj = DataModel.extend({
// Whatever you want in here
urlRoot: '?q=myApp/api/myClassName/',
defaults: {
//other fields here
},
validate: function(attributes){
//validation here
}
});
I then used deferred, which I answered here: Jquery Promise and Defered with returned results

How can i add more then one functions in a service or factory?

i must write a Service who has a lot of functions inside. This i must inject into a controller.
But, then i write a factory with 3 or more functions, angular found the first one, all others are undefined. - Why?
mySystem.factory('testFactory', function($http) {
return {
checkDates: function() {
myData.VDate = myData.VDate.format('dd.MM.yyyy');
}
return {
checkrequiered: function() {
var check = true;
if (myData.locId.lenght === 0) {
check=false;
}
return check;
}
return {
updateData: function() {
'...'
}
});
Whats wrong?
What's wrong is that you have three return statements, which means all but the first will be ignored. Return all the functions in a single object:
return {
checkDates: function() {
myData.VDate = myData.VDate.format('dd.MM.yyyy');
},
checkRequired: function() {
return (myData.locId.length !== 0);
},
updateData: function() {
'...'
}
};

Should I return collection from factory to controller via function?

I'm struggling with choosing the correct way of accessing collection located inside service from controller. I see two options, both have ups and downs:
Returning function from service that returns collection:
Service:
app.factory('healthService', function () {
var healths = [{},{},{},{}];
function updateHealths() {
healths = [...];
}
return {
getHealths : function() {
return healths;
},
update : function () {
updateHealths();
}};
});
Controller:
$scope.healths = healthService.getHealths;
$scope.update = healthService.update;
View:
ng-repeat = "health in healths()"
ng-click = "update()" '
I'm not sure about efficiency here- how often will healths() be evaluated?
Giving the possibility to access collection directly from controller:
Service:
app.factory('healthService', function () {
return {
healths : [{},{},{},{}],
update :function() {
this.healths = [...];
}
});
Controller:
$scope.healthService = healthService;
View:
ng-repeat = "health in healthService.healths" '
ng-click = "healthService.update()"
Which one is better, faster? Any other tips?
Why not try wrapping your collection in an object (which acts as a service state) to allow binding to occur on the object, rather than by exposing functions. For example:
app.factory('healthService', function() {
var state = {
healths: [...]
};
return {
getState: function() { return state; },
updateHealths: function() { state.healths = [...]; }
};
});
Then inside your controller:
$scope.healthState = healthService.getState();
Then to reference your healths from your html, use:
<div ng-repeat="health in healthState.healths"></div>

Resources