I have a simple to-do app I'm working on, which is using Angular and then PHP/MySQL for the backend.
I now have a simple app that works, where I can add new todos, and add the "percentage completed" per day to the Database, using a simple $http post.
However now what I'm looking to do is, populate the $scope.historicalDailyPercentages array, with data from the database.
At the start of the script, I init the object like so:
$scope.historicalDailyPercentages = []; //TODO, this should be initialised with data from the database.
I understand I'll need to have some sort of $http get loop in there, to check for the data and fill the object, but I'm a little unclear on how to get started there.
The entire goalzy.js script is below for reference. Thanks in advance!
angular.module('goalzy', [])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.headers.post['Content-Type'] = 'application/json; charset=utf-8';
}])
.controller('TodoController', ['$scope', '$http', function($scope, $http) {
$scope.todos = [];
$scope.historicalDailyPercentages = []; //TODO, this should be initialised with data from the database.
$scope.addTodo = function() {
if ($scope.todoText != "") {
if ($scope.todos.length < 3) {
$scope.todos.push({
text: $scope.todoText,
done: false
});
$scope.todoText = '';
//Save to DB
} else {
alert("You can only have 3 todos per day!");
$scope.todoText = '';
}
} else {
alert("you must write something");
}
};
$scope.remaining = function() {
var count = 0;
angular.forEach($scope.todos, function(todo) {
count += todo.done ? 0 : 1;
});
return count;
};
$scope.percentComplete = function() {
var countCompleted = 0;
angular.forEach($scope.todos, function(todo) {
countCompleted += todo.done ? 1 : 0; //Simply calculates how many tasks have been completed
console.log(countCompleted);
});
var totalCount = $scope.todos.length;
var percentComplete = countCompleted / totalCount * 100;
return percentComplete;
}
$scope.finaliseDay = function(percentComplete) {
alert("You're finalising this day with a percentage of: " + percentComplete);
var today = new Date();
var alreadyPresent = $scope.historicalDailyPercentages.some(function(item) {
return item.date.getFullYear() === today.getFullYear() &&
item.date.getMonth() === today.getMonth() &&
item.date.getDate() === today.getDate();
});
//Confirm that nothing has alreayd been posted for today
if (!alreadyPresent) {
// Simple POST request example (passing data)
$http.post('/postDailyPercentage.php', {
user_id: 1,
percent: percentComplete,
date: today
}).
success(function(data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
if (data) {
$scope.historicalDailyPercentages.push({
user_id: 1,
percent: percentComplete,
date: today
});
} else {
alert("Something went wrong" + data);
}
}).
error(function(data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
console.log("Post failure");
});
} else {
alert("You're all set for today - see you tomorrow!");
}
//console.log($scope.historicalDailyPercentages);
}
}]);
To populate that object with an $http.get you can do it as follows:
function getHistoricalDataSuccess(data) {
$scope.historicalDailyPercentages = data;
}
function getHistoricalDataError(error) {
//handle the error
}
$http.get('path/to/api')
.success(getHistoricalDataSuccess)
.error(getHistoricalDataError);
var TodoController = function($scope, HistoricalDailyPercentageService) {
HistoricalDailyPercentageService.get().then(function(percentages) {
$scope.historicalDailyPercentages = percentages;
}, function(error) {
alert(error);
});
};
var HistoricalDailyPercentageService = function($http) {
this.get = function() {
return $http.get('yourUrl')
.then(function(xhr) {
var data = xhr.data;
// Transform the data as you see fit
return data;
}, function(xhr) {
// xhr contains the error message - modify this as you see fit.
return xhr.code;
});
};
};
angular.module('goalzy')
.controller('TodoController', ['$scope', 'HistoricalDailyPercentages', TodoController])
.service('HistoricalDailyPercentageService', ['$http', HistoricalDailyPercentageService]);
I would recommend doing it this way; this will make it easier to test by taking the logic of getting the data out of your already busy controller. #RVandersteen's example will only work inside of your controller, which is fine, but it really does make your controller very busy; controllers should really only assign things to a scope, everything else should be handled in a directive (for example, binding events to methods) or a service/factory/provider (for business logic).
After you have finished up your code, could you post on CodeReview? There's a few improvements I could suggest but they are merely review-based things and not appropriate for the scope of this question.
It's worth noting by the way that because I am using then in the controller I must use then in the service, too. If I use success in the service then my changes will not be reflected when I call then in the controller.
Related
I'm getting the error TypeError: dataFile.saveDataFile is not a function when trying to call the function saveDataFile from my dataFile service (funnily enough).
The dataFile service code is below, it's a little more odd because the loadDataFile function gets called and works no problem.
angular.module('crmApp').
factory('data', function($http, $filter, $q){
var data = {};
data.data;
// Get the data from the DB
data.getData = function () {
var d = $q.defer();
if (data.data) {
console.log("There's already data");
d.resolve(data.data);
// console.log(data.data);
// return data.data;
}
else {
console.log("there's no data, asking for it");
return $http({
method: 'GET',
url: '/getAllData'})
.then(function(response) {
data.data = response.data;
d.resolve(data.data);
// return data.data;
},
function myError(response) {
console.log(response);
return 1;
});
}
return d.promise;
}
The place I'm calling it from - controller.addTasks is here:
'use strict';
angular.
module('addTask')
.controller("addTask", ["$scope", "dataFile",
function($scope, dataFile) {
$scope.addTask = function(dataFile, group) {
// create the tasks json
var newTaskCreated = (new Date).getTime();
var newTask = { "task" : $scope.newTaskDescription,
"created" : newTaskCreated,
"group" : group,
"today" : false,
"status" : "incomplete"};
$scope.tasks.push(newTask);
console.log($scope.tasks);
// Save the new data in tasksData.json
dataFile.saveDataFile($scope.tasks);
// add the task to the screen
// reset the blank addtask input box
$scope.newTaskDescription = "";
}
}]);
None of the similar posts I've seen have helped so any help you can offer is greatly appreciated.
I created a small example to learn about how to handle exceptions with promises particular in focus of my used MVC structure. This structure uses a Data Access Object (DAO) in addition to the 3 known components Model, View and Controller to be more independent from changes at the backend. Due to the conversion in the DAO you can use different methods to retrieve data from the backend. But I guess that's no new concept to you guys at all.
Well, what I want to know is this: If an exception occurs at the MovieInfoService, the code jumps to the getMovieFailed method and returns $q.reject(e). But what happens next? Does the DAO receive something and how does it gets processed?
I am pretty unfamiliar with the concept of error and exception handling in angular. Therefore I have no clue how to handle such problems with promises. Can you guys help me out and provide me with some useful hints, tips or advice how to handle such problems in order to show these problems on the users view to inform him? Actually even an alert() will be enough - I just need to understand the basic idea/concept of handling exceptions with promises.
Here is some sample code of mine:
app.controller('MainController', ['$scope', 'MovieInfoDAO', function ($scope, MovieInfoDAO)
{
var vm = this;
vm.movie = {};
MovieInfoDAO.getMovie()
.then(function(data){
vm.movie = data;
});
activate();
//////////
function activate() {
return getMovieDetails().then(function(){
console.log('Starting Movie Search');
});
}
function getMovieDetails() {
return MovieInfoDAO.getMovie().then(function(data) {
vm.movie = data;
return vm.movie;
}
}
}]);
app.service("MovieInfoDAO", ['$q', 'MovieInfoService', function($q, MovieInfoService)
{
this.getMovie = function()
{
return MovieInfoService.getMovie().then(function(returnData){
var arrInfoItems = [];
for(var i = 0, length = returnData.length; i < length; i++){
var item = new MovieInfoModel(returnData[i]);
arrInfoItems.push(item);
}
return arrInfoItems;
});
};
}]);
app.service('MovieInfoService', ['$http', '$q', function($http, $q) {
this.getMovie = function()
{
return $http({
method: "GET",
data: { },
url: "http://www.omdbapi.com/?t=Interstellar&y=&plot=short&r=json"
})
.then(getMovieCompleted);
.catch(getMovieFailed);
};
function getMovieCompleted(response) {
return response.data;
}
function getMovieFailed(e) {
var newMessage = 'Failed to retrieve Infos from the Server';
if (e.data $$ e.data.description) {
newMessage = newMessage + '\n' + e.data.description;
}
e.data.description = newMessage;
return $q.reject(e);
}
return this;
}]);
function MovieInfoModel(arrParameter){
Model.call(this);
this.title = arrParameter.Title;
this.actors = arrParameter.Actors;
this.plot = arrParameter.Plot;
this.constructor(arrParameter);
}
HTML
<div>
<h1>{{Main.movie.title}}</h1>
<span>{{Main.movie.plot}}</span>
<br/>
<h3>Actors:</h3>
<span>{{Main.movie.actors}}</span>
</div>
I always create my service for ajax calls which is encapsulating $http service. In such service we can create one error handler for all requests. It enables to create many error types and check http response codes like 404,500. Some short example how it can look like, I added only get method:
var app=angular.module('app', []);
app.service('$myAjax', function($http) {
this.get = function (url,success,error) {
return $http.get(url).then(function(response){
if (response.data.status===1)//status is example success code sended from server in every response
return success(response.data);
else{
//here error handler for all requests
console.log("Something is wrong our data has no success on true.");
//error callback
return error(response.data);
}
},function(response){
//this code will run if response has different code than 200
//here error handler for all requests
console.log("Something is very wrong. We have different then 200 http code");
//error callback
return error(response.data);
});
}
});
//TEST
app.controller("Test",function($myAjax){
$myAjax.get('http://jsfiddle.net/echo/jsonp/',function(response){
//here success
return 1;
},function(response){
//here error
return 0;
}).then(function(data){
console.log("I am next promise chain");
console.log("Data from callbacks:"+data);
});
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="Test">
</div>
</div>
I show error even if http code is 200 when data has no status parameter on 1, this is second and internal error handling proposition. Thanks that parameter We can for example send many status information and create for them callbacks. Status 2 can mean - no rights, status 3 - no data etc..
If your intensions is to return the array arrInfoItems in MovieInfoDAO.getMovie(), instead of:
return arrInfoItems;
use a promise, e.g.:
app.service("MovieInfoDAO", ['$q', 'MovieInfoService', function($q, MovieInfoService)
{
this.getMovie = function()
{
var def = $q.defer();
MovieInfoService.getMovie()
.then(function(returnData){
var arrInfoItems = [];
for(var i = 0, length = returnData.length; i < length; i++){
var item = new MovieInfoModel(returnData[i]);
arrInfoItems.push(item);
}
def.resolve(arrInfoItems);
}, def.reject);
return def.promise;
};
}]);
Usage
app.controller('MainController', ['$scope', 'MovieInfoDAO', function ($scope, MovieInfoDAO)
{
var vm = this;
vm.movie = {};
MovieInfoDAO.getMovie()
.then(function(data){
vm.movie = data;
}, function(error){
// Handle error here, can be placed in a vm.error variable
});
}]);
I understand that the appropriate method to share data between controllers in Angular.js is by using Factories or Services.
app.controller('Controller1', function($scope, DataService) {
DataService.getValues().then(
function(results) {
// on success
console.log('getValues onsuccess');
});
});
app.controller('Controller2', function($scope, DataService) {
DataService.getValues().then(
function(results) {
// on success
console.log('getValues onsuccess');
});
});
app.factory('DataService', function($http) {
var getValues = function() {
console.log('making http request');
return $http.get("/api/getValues");
};
return {
getValues: getValues
}
});
I have two controllers calling the same method in a factory twice
and this is perfectly fine and everything is working as it should. My only concer is that it seems a bit unecessary to make the same request twice? Would the use of $broadcast be a better approach?
Or could i structure my code differenty so that the service is called only once?
You could store the results of the request in the factory and retrieve those instead.
app.factory('DataService', function($http) {
var values;
var requestValues = function() {
console.log('making http request');
$http.get("/api/getValues").then(
function(results){
values = results;
});
};
var getValues = function() {
return values;
};
return {
requestValues : requestValues,
getValues: getValues
}
});
If your data is somekind of static and may not change very often over time you could do something like:
app.factory('DataService', function($http) {
self = this;
this.isLoaded = false;
this.results;
this.getValues = function() {
console.log('making http request');
$http.get("/api/getValues").then(
function(results) {
// on success
console.log('getValues onsuccess');
self.isLoaded = true
this.results = results;
return results;
})
);
};
})
And in the controller:
app.controller('Controller2', function($scope, DataService) {
if(!DataService.isLoaded){
results = DataService.getValues()
}else{
results = DataService.results;
}
});
You should consider caching in your DataService. Add a variable to hold the result from the http service and a time-stamp variable to store the time it was retrieved.
If a second call to the service is within a preset time period (lets say, 5 seconds), then http call is not made and data from the cache is returned.
app.factory('DataService', function($http) {
var cachedValue = null;
var lastGet = null;
var getValues = function() {
var timeNow = new Date();
if (cachedValue == null || ((timeNow - lastGet) < 5000)) {
console.log('making http request');
lastGet = timeNow;
cachedValue = $http.get("/api/getValues");
} else console.log('returning cached value');
return cachedValue;
};
return {
getValues: getValues
}
});
My app had this .run
.run(function ($rootScope, $http) {
$rootScope.server = "http://127.0.0.1:5000/rawDemo";
var call = $rootScope.server + "/frontEnd/GetStructure";
var texts = {};
texts.languages = {};
$http.get(call).then(function (response) {
for (var i = 0; i < response.data.languages.length; i++) {
texts.languages[response.data.languages[i].iso2] = {
'title': response.data.languages[i].title,
'description': response.data.languages[i].description,
'keywords': response.data.languages[i].keywords,
'frontEndTexts': response.data.languages[i].frontEndTexts
};
}
$rootScope.texts = texts;
$rootScope.webshop = response.data;
$rootScope.webshop.language = response.data.culture.language;
$rootScope.webshop.numberFormat = "";
$rootScope.carouselData = response.data.frontEndConfig.customConfiguration.mjCarousel;
console.log('end run');
});
})
.And some of my resolvers perform a call in a service...
angular
.module('app')
.factory('Products',['$http', '$rootScope', function($http, $rootScope){
return {
listByCategories : function (categories){
console.log('begin service');
var call = $rootScope.server + '/products/list/categories/' + categories.fullPath +'?page1&recordsPerPage=2' ;
return $http.get(call).then(function(response) {
return response.data;
});
}
}
}])
My expected result in console.log supposed to be:
- end run
- begin service
but instead..begin service starts before the end run. It's because the .run finish and keep the $http executing asynchronously and go on to the next stages like resolvers, for example.
According to my research on this website, it's impossible to make the $http works synchronous. So, my question is...any hint about how to handle the scenario ? My service depends from data that's must be loaded before anything.
I cannot "merge" the .run and the service because all the others views, services, must have the .run executed before. in my .run I load dozen of global configurations basically.
UPDATE : I'm still stucked...but I'm trying something...I changed the way I'm doing the things..
so, on my .run
$rootScope.loadWebshop = function(callBack) {
if($rootScope.loaded) { return ; }
var call = $rootScope.server + "/frontEnd/GetStructure";
var texts = {};
texts.languages = {};
$http.get(call).then(function (response) {
for (var i = 0; i < response.data.languages.length; i++) {
texts.languages[response.data.languages[i].iso2] = {
'title': response.data.languages[i].title,
'description': response.data.languages[i].description,
'keywords': response.data.languages[i].keywords,
'frontEndTexts': response.data.languages[i].frontEndTexts
};
}
$rootScope.texts = texts;
$rootScope.webshop = response.data;
$rootScope.webshop.language = response.data.culture.language;
$rootScope.webshop.numberFormat = "";
$rootScope.carouselData = response.data.frontEndConfig.customConfiguration.mjCarousel;
$rootScope.loaded = true;
console.log('end .run');
callBack();
});
}
..And on my service :
listByCategories : function (categories){
return $rootScope.loadWebshop (function () {
console.log('begin service');
var call = $rootScope.server + '/products/list/categories/' + categories.fullPath +'?page1&recordsPerPage=2' ;
return $http.get(call).then(function(response) {
return response.data;
});
});
}
Now I'm facing a different issue...it's returns undefined because it's asyncronous, even using premise. Any clue ??
I have an angular service that has one function visible to the controllers using the service. ie. getData
I want my service to be able to use a helper function called modifyData in the service that manipulates the data before returning it to the service which then sends the data to the controller. I do not want to use the helper function outside of the service or have it accessible outside of the service.
app.service("dataService", function() {
/* HELPER FUNCTION */
var modifyData = function(data) {
data.modified = true;
return data;
}
this.getData = function() {
//Do a http request to get oldData variable
var newData = modifyData(oldData);
return newData;
}});
My error from Angular is Error: modifyData is undefined.
What am I doing wrong here?
EDIT: Here is my actual code since my example code should work then I might have simplified it too much.
rabApp.service("reviewService",
["$http", "$q", "beerService", "userService",
function($http, $q, beerService, userService) {
/* HELPER FUNCTIONS */
/* Builds a complete review object from a user object, beer object, and partial review object */
var buildReviewObj = function(reviewObj, switchClass) {
var deferred = $q.defer();
if(switchClass) {
reviewObj.dirClass = "left";
} else {
reviewObj.dirClass = "right";
}
//Make sure the review picture is valid and if not use default one
if(reviewObj.image.length <= 0) {
reviewObj.image = "images/default-beer-pic.jpg";
}
//Make sure the review style is present if not put a default in
if(reviewObj.style.length <= 0) {
reviewObj.style = "Unknown";
}
//Add user data
userService.getUser(reviewObj.author_id).success(function(data, status) {
//Check if we have a matched user
if(data.status === "success") {
var userObj = data.data;
//Make sure the profile picture is valid and if not use default one
if(userObj.profile_pic.length <= 0) {
userObj.profile_pic = "images/default-profile-pic.jpg";
}
reviewObj.author = userObj;
} else {
deferred.reject("Bad User object returned");
}
});
//Add beer data
beerService.getBeer(reviewObj.beer_id).success(function(data, status) {
//Check if we have a matched beer
if(data.status === "success") {
var beerObj = data.data;
reviewObj.beer = beerObj;
} else {
deferred.reject("Bad Beer object returned");
}
});
deferred.resolve(reviewObj);
}
/* Gets an array of review objects from the backend
* #return Array of Review objects
*/
this.getReviews = function() {
var deferred = $q.defer();
$http({
method: "GET",
url: "/includes/services/reviews.php",
params: { a : "getReviews",
limit : "10" }
}).success(function(data, status) {
//switchClass variable alternates the review to display left or right
var switchClass = true;
//Notify review controller that we are loading reviews
deferred.notify("loading");
//Check if we have reviews
if(data.status === "success") {
var reviews = [];
//Add beer and user data to each review
data.data.forEach(function(reviewObj, index) {
buildReviewObj(reviewObj, switchClass).then(function(data) {
switchClass = !switchClass;
reviews.push(reviewObj);
});
});
//Notify review controller that we are done
deferred.notify("finished");
//Review array built successfully return reviews
deferred.resolve(reviews);
} else {
//Couldn't get reviews return false
deferred.reject("Couldn't access reviews.php back end service");
}
}).error(function() {
//Couldn't get reviews return false
deferred.reject("Couldn't access reviews.php back end service");
});
return deferred.promise;
}}]);
I found my error I was missing
return deferred.promise;
In the helper function.... facepalm this took me 10 minutes to solve after posting here but only after I struggled for an hour an half before posting here.