Adding a function to a factory (singleton) inside a controller - angularjs

I have a factory called search.
I have many controllers called SearchCampaignController, SearchQuotaController and so on.
The factory search is a singleton object to which these controllers add function implementations to.
The problem is, a parent controller called SearchController must call a function inside the factory search which is not yet implemented because the child controllers execute after the parent controller executes.
It seems like I'm doing something wrong.
angular
.module('app.search')
.factory('search', search);
search.$inject = ['$http', '$window', '$state', 'CONF', '$mdToast', 'USER_ROLES', 'USER_MODULES'];
function search($http, $window, $state, CONF, $mdToast, USER_ROLES, USER_MODULES) {
var searchInfo;
function updateQueryString(params) {
$state.go(
'search',
{
searchType: params.searchType,
quotaId: params.quotaId,
campaignName: params.campaignName,
templateName: params.templateName,
passId: params.passId,
certId: params.certId
},
{notify: false}
)
}
function getQuotaById(params) {
var reqPath = CONF.apiPath + 'core/quotas/' + params.quotaId;
return $http.get(reqPath);
}
function getCampaignById(params) {
var reqPath = CONF.apiPath + 'core/quotas/' + params.quotaId + '/campaigns/' + params.campaignName;
return $http.get(reqPath);
}
function queryCampaigns(params) {
var reqPath = CONF.apiPath + 'core/quotas/' + params.quotaId + '/campaigns';
return $http.get(reqPath);
}
function getTemplateById(params) {
var reqPath = CONF.apiPath + 'core/quotas/' + params.quotaId + '/campaigns/' + params.templateName;
return $http.get(reqPath);
}
function queryTemplates(params) {
var reqPath = CONF.apiPath + 'core/campaigns/' + params.campaignName + '/templates';
return $http.get(reqPath);
}
function getPassById(params) {
var reqPath = CONF.apiPath + 'core/passes/' + params.passId;
return $http.get(reqPath);
}
function getPassbookById(params) {
var reqPath = CONF.apiPath + 'core/passbookCert/' + params.certId;
return $http.get(reqPath);
}
function queryPassbookCerts(params) {
var reqPath = CONF.apiPath + 'core/quotas/' + params.quotaId + '/passbookCerts';
return $http.get(reqPath);
}
//Global search logic
//NEED TO RE-FACTOR
function searchMasterFunction(params, obj) {
if(params.searchType){
search.changeSearchType(params.searchType);
}
updateQueryString(params);
if(params.quotaId){
search.getQuotaAndDisplayResult(params);
// search.getPassbookCertsAndDisplayResult(params);
search.updateQuotaIdInTemplateTab(params); //special - needs re-visit
}
if(params.quotaId && !params.campaignName){
search.getCampaignsAndDisplayResult(params);
}
if(params.quotaId && params.campaignName && params.templateName){
search.getCampaignAndDisplayResult(params);
search.getTemplateAndDisplayResult(params);
}else if(params.quotaId && params.campaignName){
search.getCampaignAndDisplayResult(params);
search.getTemplatesAndDisplayResult(params);
}else if(params.quotaId && params.templateName){
search.getTemplateAndDisplayResult(params);
}
if(params.campaignName){
search.getTemplatesAndDisplayResult(params);
}
if(params.passId){
search.getPassAndDisplayResult(params);
}
//getPassbookById
}
var search = {
searchInfo: searchInfo,
searchMasterFunction: searchMasterFunction,
getQuotaById: getQuotaById,
getCampaignById: getCampaignById,
queryCampaigns: queryCampaigns,
getTemplateById: getTemplateById,
queryTemplates: queryTemplates,
getPassById: getPassById,
getPassbookById: getPassbookById,
queryPassbookCerts: queryPassbookCerts
};
return search;
}
And this is my parent controller which should call the searchMasterFunction inside the factory search so that when there are values in the query string, it automatically populates any search results according to the logic inside the search factory.
angular
.module('app.search')
.controller('SearchController', SearchController);
SearchController.$inject = ['$state', 'search', '$scope', '$log'];
function SearchController($state, search, $scope, $log){
$log.warn("Executing SearchController");
var vm = this;
vm.searchType = $state.params.searchType; //re-visit
vm.tabs = ['quota', 'campaign', 'template', 'pass', 'cert'];
vm.changeSearchTypeOnTabClick = changeSearchTypeOnTabClick;
search.changeSearchType = changeSearchType;
function changeSearchTypeOnTabClick(searchType) {
$state.go('search', {searchType: searchType}, {notify: false});
}
function changeSearchType(searchType) {
vm.searchType = searchType;
}
// this function call is what is causing the problem
// search.searchMasterFunction($state.params);
}
The following is one of my child controllers which implement functions such as search.getQuotaAndDisplayResult.
angular
.module('app.search')
.controller('SearchQuotaController', SearchQuotaController);
SearchQuotaController.$inject = ['search', '$scope', '$log'];
function SearchQuotaController(search, $scope, $log){
$log.info("Executing SearchQuotaController");
var vm = this;
vm.searchInfo;
vm.quota;
vm.searchBtnClick = searchBtnClick;
search.getQuotaAndDisplayResult = getQuotaAndDisplayResult; //THIS LINE IS WHAT NEEDS ATTENTION. I'm adding a function into the `search` factory.
function searchBtnClick(params){
search.searchMasterFunction(params);
};
function getQuotaAndDisplayResult(params) {
vm.searchInfo = params; //update fields in the Quota view
search.getQuotaById(params).then(function(quota){
vm.quota = quota.data; //update the quota object in the view
});
};
}
So the problem is that SearchQuotaController runs AFTER SearchController and therefore if I try to call search.searchMasterFunction in SearchController, it will not be able to execute properly since search.searchMasterFunction relies on the child controller to execute in order for the function implementation to be complete.
Any help would be greatly appreciated. I've already considered the $broadcast method but it seems like a hack and not a real solution.
PS. The reason why I'm adding functions from child controllers into the search factory is because child controllers have access to their local $scope.

Related

how can I use a function which is located inside a service?

What I am trying to do is to use function globally throughout controllers.
The problem is when I want to use the function I defined inside the service in the first function. It shows an error that it cannot find a function. I tried without this keyword but it's not working. I can go to all function when I tried in other controllers, which is a good sign that I can use this service globally.
In short, I want to use all function inside first function.
app.factory("UserService", function() {
var users = ["Peter", "Daniel", "Nina"];
return {
all: function() {
return users;
},
first: function() {
var users = this.all();
return users[0];
}
};
});
The code above was an example that I made and real code appears like this.
controller
angular.module("app").requires.push("app.region");
I put the region to app so I can use the service.
After that I made a controller like this
.controller("regionCreateController", ["$scope", "phoneMaskService", function ($scope, phoneMaskService) {
$scope.createClicked = function (data) {
data = phoneMaskService.putMaskOnRegion(data);
console.log(data);
};
}
When I put phoneMaskService which is the service I made in the app.js and it fails.
This is the error I am getting
angular.js:14110 ReferenceError: removeAllLetters is not defined
This is the actual code making errors.
.factory("phoneMaskService", [function () {
var returnMethod = {
removeAllLetters: removeAllLetters,
putMaskOn: putMaskOn,
putMaskOnRegion: putMaskOnRegion
};
return returnMethod;
function removeAllLetters(value) {
var val = value.replace(/\D+/g, '').replace('\-', '');
return val;
}
function putMaskOn(value) {
console.log(value);
value = this.removeAllLetters(value);
console.log(value);
var isMobile = parseInt(value.charAt(1)) == 2;
if (isMobile) {
var x = value.replace(/\D/g, '').substring(0, 14).match(/(\d{3})(\d{3})(\d{3,})/);
x = ' ( ' + x[1] + ' ) ' + x[2] + ' - ' + x[3];
return x;
} else {
var x = value.replace(/\D/g, '').substring(0, 14).match(/(\d{2})(\d{3})(\d{3,})/);
x = ' ( ' + x[1] + ' ) ' + x[2] + ' - ' + x[3];
return x;
}
}
function putMaskOnRegion(object) {
angular.forEach(object, function (value, key) {
if (key == "contactNumberPhone") {
var testvalue = this.removeAllLetters(value);
console.log(this);
console.log("test value" + testvalue);
object[key] = this.removeAllLetters(value);
}
});
return object;
}
}])
The error happens the line here and says removeallletters are undefined
var testvalue = this.removeAllLetters(value);
One approach to avoid binding problems is to declare the functions inside the factory:
app.factory("UserService", function() {
var users = ["Peter", "Daniel", "Nina"];
return { all: all, first: first };
function all() {
return users;
}
function first() {
var users = all();
return users[0];
}
});
I use the follwoing when declaring factories, which
creates an object within the factory declaration, binds methods to it and returns is as the the factory object.This might work in your case.
app.factory("UserService", function() {
var services = {};
services.users = ["Peter", "Daniel", "Nina"];
services.all = function() {
return services.users;
}
services.first = function() {
return services.all()[0];
}
return services;
});

Redirection error in angularjs

After adding data in location table, clicking on the save button should redirect it to list of data in location table.But ,it stays in the same page after adding.The same path is given to modify location,it works fine. whereas the same path does not redirect when add location.
function locationController($scope, $state, $rootScope, locationServices,$location, locations, location, primaryLocation, $stateParams,locationTypes, countries) {
var vm = this;
$scope.locations = locations.data;
$scope.location = location.data;
if (primaryLocation.data && primaryLocation.data[0])
$scope.primaryLocation = primaryLocation.data[0];
if (!$scope.location) {
var location = {};
if ($stateParams.accountId) {
$scope.location = {accountId: $stateParams.accountId };
} else {
$scope.location = location;
}
}
$rootScope.title = "Locations";
$scope.locationslist = "views/locations.html";
$scope.addOrModifyLocation = function (location) {
if (location._id) {
locationServices.modifyLocation(location).then(function (response) {
$location.path('/account/locations/contacts/' + location.accountId + '/' +location.accountId);
// $state.reload();
})
} else {
location.status = 'ACTIVE';
locationServices.addLocation(location).then(function (response) {
$location.path('/account/locations/contacts/' + location.accountId + '/' +location.accountId);
})
}
};
If you want angular to know about your $location update, you have to do it like this :
$rootScope.$apply(function() {
$location.path("/my-path"); // path must start with leading /
});
If you're using ui-router, a cleaner approach would be to use
$state.go('stateName', {'accountId' : location.accountId, });
edit :
If you have errors that happen during a state change, you can see it by adding the following code in your app after declaring your module :
angular.module("appName").run([
"$rootScope",
function ($rootScope) {
$rootScope.$on("$stateChangeError", function(error) {
console.log(error);
});
}
]);

Restangular and UI-Bootstrap first page appears blank

I am currently getting to grips with Restangular and seem to be making some headway. I have opted to use the UI-Bootstrap for the ease of use, as I am use to working with bootstrap before.
My current issue is that I have pagination working for my controller, however the results do not appear when you first visit the page. If I visit the second page and then go back to the first page the results are there as expected. If I then choose to reload the page in anyway the results on the first page do not appear.
My code is as follows:
app.controller('BillsListCtrl', function ($scope, BillRepository) {
$scope.filteredBills = [],
$scope.currentPage = 1,
$scope.itemsPerPage = 10;
$scope.bills = BillRepository.getList();
$scope.$watch('currentPage', function() {
var begin = (($scope.currentPage - 1) * $scope.itemsPerPage),
end = begin + $scope.itemsPerPage;
$scope.filteredBills = $scope.bills.slice(begin, end);
});
console.log($scope.filteredBills);
});
The Repository:
app.factory('AbstractRepository', [
function () {
function AbstractRepository(restangular, route) {
this.restangular = restangular;
this.route = route;
}
AbstractRepository.prototype = {
getList: function (params) {
return this.restangular.all(this.route).getList(params).$object;
},
get: function (id) {
return this.restangular.one(this.route, id).get();
},
getView: function (id) {
return this.restangular.one(this.route, id).one(this.route + 'view').get();
},
update: function (updatedResource) {
return updatedResource.put().$object;
},
create: function (newResource) {
return this.restangular.all(this.route).post(newResource);
},
remove: function (object) {
return this.restangular.one(this.route, object.id).remove();
}
};
AbstractRepository.extend = function (repository) {
repository.prototype = Object.create(AbstractRepository.prototype);
repository.prototype.constructor = repository;
};
return AbstractRepository;
}
]);
Setting up the BillRepository:
app.factory('BillRepository', ['Restangular', 'AbstractRepository',
function (restangular, AbstractRepository) {
function BillRepository() {
AbstractRepository.call(this, restangular, 'bills');
}
AbstractRepository.extend(BillRepository);
return new BillRepository();
}
]);
Any light you can shed on this issue would greatly be appreciated!
If $scope.filteredBills is what's being displayed on the page, you're only populating that variable when the currentPage variable is changed. When your code runs for the first time, you set the variable and then set the watch on it, so it doesn't change and filteredBills does not get set.
Thanks goes to #ErikAGriffin for his help with this.
I made a change in my repository and removed the $object as shown below:
getList: function (params) {
return this.restangular.all(this.route).getList(params);
},
Then my controller changed to the following:
app.controller('BillsListCtrl', function ($scope, BillRepository) {
$scope.filteredBills = [],
$scope.currentPage = 1,
$scope.itemsPerPage = 10;
$scope.bills = BillRepository.getList().$object;
BillRepository.getList().then(function(data){
$scope.filteredBills = data.slice(0, $scope.itemsPerPage);
});
$scope.$watch('currentPage + itemsPerPage', function() {
var begin = (($scope.currentPage - 1) * $scope.itemsPerPage),
end = begin + $scope.itemsPerPage;
$scope.filteredBills = $scope.bills.slice(begin, end);
});
});

Testing controller with resolve dependencies

I'm trying to unit test a controller which relies on resolve keys using Jasmine. I am also using the controllerAs syntax. The routing code is as follows:
$routeProvider.when('/questions', {
templateUrl: 'questions/partial/main_question_viewer/main_question_viewer.html',
controller:'MainQuestionViewerCtrl',
controllerAs:'questionCtrl',
resolve: {
default_page_size: ['QuestionService', function (QuestionService) {
//TODO Work out page size for users screen
return 50;
}],
starting_questions: ['QuestionService', function (QuestionService) {
var questions = [];
QuestionService.getQuestions(1).then(
function(response){
questions = response;
}
);
return questions;
}],
},
});
The controller (so far):
angular.module('questions').controller('MainQuestionViewerCtrl',
[
'QuestionService',
'starting_questions',
'default_page_size',
function (QuestionService, starting_questions, default_page_size) {
var self = this;
//Model Definition/Instantiation
self.questions = starting_questions;
self.page_size = default_page_size;
self.filters = [];
//Pagination Getters (state stored by QuestionService)
self.current_page = function(){
return QuestionService.get_pagination_info().current_page_number;
}
self.page_size = function(page_size){
if(page_size != null){
QuestionService.set_page_size(page_size);
}
return QuestionService.get_page_size();
}
}
]
);
And the test code:
describe('MainQuestionViewerCtrl', function () {
//===============================TEST DATA=====================================
var allQuestionsResponsePage1 = {
count: 4,
next: "https://dentest.com/questions/?format=json&page=2&page_size=1",
previous: null,
results: [
{
id: 1,
subtopic: {
topic: "Math",
name: "Algebra"
},
question: "if a=3 and b=4 what is a+b?",
answer: "7",
restricted: false
}
]
};
beforeEach(module('questions'));
beforeEach(module('globalConstants')); //Need REST URL for mocking responses
var ctrl, qService;
var backend,baseURL;
//inject dependencies
beforeEach(inject(function ($controller, $httpBackend,REST_BASE_URL) {
ctrl = $controller('MainQuestionViewerCtrl');
backend = $httpBackend;
baseURL = REST_BASE_URL;
}));
//inject QuestionService and set up spies
beforeEach(inject(function (QuestionService) {
qService = QuestionService;
}));
//Convenience for adding query params to mocked requests
var buildParams = function (page, page_size) {
var params = {
format: 'json',
page: page,
page_size: page_size,
};
var keys = Object.keys(params).sort(); //how angular orders query params
var returnString = '?' + keys[0] + '=' + params[keys[0]] +
'&' + keys[1] + '=' + params[keys[1]] + '&' + keys[2] + '=' + params[keys[2]];
return returnString;
};
describe('Instantiation',inject(function ($controller) {
beforeEach(module($provide){
beforeEach(inject(function ($controller) {
//Make a mock call to the server to set up state for the QuestionService
backend.expectGET(baseURL + '/questions/' + buildParams(1, 1)).respond(200, allQuestionsResponsePage1);
qService.getQuestions(1);
backend.flush();
//Now mock the result of resolve on route
ctrl = $controller('MainQuestionViewerCtrl', {
default_page_size: 1,
starting_questions: allQuestionsResponsePage1,
});
}));
it('should start with the first page of all the questions pulled down', function () {
expect(qService.questions).toEqual(allQuestionsResponsePage1);
});
it('should start on page 1', function () {
expect(qService.current_page).toEqual(1);
});
it('should start with the page size set to the default passed in',function(){
expect(qService.page_size).toEqual(1);
})
}));
When trying to run the tests, Angular is complaining that it cant resolve starting_questions or default_page_size because the providers for them aren't known.
It worth pointing out that the reason for mocking the HTTP request for the QuestionService is that it builds pagination info based on the response, which the controller will then access to determine the paginator size/numbers in the UI.
Solved. I was instantiating the controller in the outer describe without passing in mock values for the resolve key dependecies. That was causing the error: the method of instantiating the controller with the mock dependecies works fine.

AngularJS Chart Directive - Data loaded in async service not updating chart

I am having one chart directive created, and I am bootstrpping the app after loading google api. In following code, a simple data table is working fine. But when I load data from server in async manner, chart is not being displayed.
Controller
'use strict';
myNetaInfoApp.controller('allCandidatesController', [
'$scope','allCandidates2009Svc', '$timeout',
function ($scope, allCandidates2009Svc, $timeout) {
$scope.data1 = {};
$scope.data1.dataTable = new google.visualization.DataTable();
$scope.data1.dataTable.addColumn("string", "Party");
$scope.data1.dataTable.addColumn("number", "qty");
$scope.data1.dataTable.title = "ASDF";
$timeout( function (oldval, newval) {
allCandidates2009Svc.GetPartyCriminalCount().then(function(netasParty) {
var i = 0;
for (var key in netasParty) {
$scope.data1.dataTable.addRow([key.toString(), netasParty[key]]);
i++;
if (i > 20) break;
}
});
});
$scope.dataAll = $scope.data1;
//sample data
$scope.data2 = {};
$scope.data2.dataTable = new google.visualization.DataTable();
$scope.data2.dataTable.addColumn("string", "Name");
$scope.data2.dataTable.addColumn("number", "Qty");
$scope.data2.dataTable.addRow(["Test", 1]);
$scope.data2.dataTable.addRow(["Test2", 2]);
$scope.data2.dataTable.addRow(["Test3", 3]);
}
]);
Service
'use strict';
myNetaInfoApp.factory('allCandidates2009Svc', ['$http', '$q',
function ($http, $q) {
var netas;
return {
GetPartyCriminalCount: function () {
var deferred = $q.defer();
$http.get('../../data/AllCandidates2009.json')
.then(function (res) {
netas = res;
if (netas) {
var finalObj = {};
_.each(netas.data, function(neta) {
finalObj[neta.pty] = finalObj[neta.pty] ? finalObj[neta.pty] + 1 : 1;
});
deferred.resolve(finalObj);
}
});
return deferred.promise;
}
};
}]);
Directive
"use strict";
var googleChart = googleChart || angular.module("googleChart", []);
googleChart.directive("googleChart", function () {
return {
restrict: "A",
link: function ($scope, $elem, $attr) {
var dt = $scope[$attr.ngModel].dataTable;
var options = {};
if ($scope[$attr.ngModel].title)
options.title = $scope[$attr.ngModel].title;
var googleChart = new google.visualization[$attr.googleChart]($elem[0]);
$scope.$watch($attr.ngModel, function (oldval, newval) {
googleChart.draw(dt, options);
});
}
};
});
HTML
<div ng-controller="allCandidatesController">
<div class="col-lg-6">
<h2>Parties and Candidates with Criminal Charges</h2>
<div google-chart="PieChart" ng-model="dataAll" class="bigGraph"></div>
<!--<p><a class="btn btn-primary" href="#" role="button">View details ยป</a></p>-->
</div>
<div class="col-lg-6">
<h2>Heading</h2>
<div google-chart="BarChart" ng-model="data2" class="bigGraph"></div>
</div>
</div>
I think you need to wrap your function body in allCandidates2009Svc factory with scope.$apply(). But the return deferred.resolve() will be outside scope.$apply().
function asyncGreet(name) {
var deferred = $q.defer();
setTimeout(function() {
// since this fn executes async in a future turn of the event loop, we need to wrap
// our code into an $apply call so that the model changes are properly observed.
scope.$apply(function() {
deferred.notify('About to greet ' + name + '.');
if (okToGreet(name)) {
deferred.resolve('Hello, ' + name + '!');
} else {
deferred.reject('Greeting ' + name + ' is not allowed.');
}
});
}, 1000);
return deferred.promise;
}
Read the docs here
http://docs.angularjs.org/api/ng.$q

Resources