I'm totally new to ionic/angular, this is my code:
.controller('PostCtrl', function($scope, Posts, $cordovaSQLite, $http) {
$scope.getPosts = function() {
$http.get('http://localhost/postIds').then(function(resp) {
_.each(resp.data, function(id) {
var query = "SELECT id FROM posts WHERE id = ?";
$cordovaSQLite.execute(db, query, [id]).then(function(res) {
if(res.rows.length = 0) {
$http.get('http://localhost/post/' + id).then(function(resp) {
var post = resp.data;
var query = "INSERT INTO posts (postId, title, user, content) VALUES (?,?,?,?)";
$cordovaSQLite.execute(db, query, [post.id, post.title, post.user, post.content]).then(function(res) {
// success
}, function(err) {
console.log(err);
});
}, function(err) {
console.log(err);
});
}
}, function (err) {
console.error(err);
});
});
}, function(err) {
console.log(err);
});
}
})
what am I doing is
get all ids from server
if id doesnt exist in db(sqlite)
get post by id from server
insert post into db
It ends up deeply nested, ugly.
what is the ionic, angular way to do this?
As the others suggested the best option is to use promises so you don't have to nest statements like you're doing.
AngularJs uses $q promises:
A service that helps you run functions asynchronously, and use their
return values (or exceptions) when they are done processing.
On the internet there are tons of articles about promises and how to chain them.
Recently I found this article which explains the common mistakes with promises.
It's worth reading cause it goes deep into the topic.
In AngularJs you would create a promise using the $q service:
function doSomething() {
var deferred = $q.defer();
deferred.resolve({value: true});
return deferred.promise;
}
This bit of code returns a promise which is resolved - since there's no async operation - when it's called. It would return an object with a property value = true.
The cool thing about promises is the fact that you can chain them:
doSomething()
.then(function(result){
// result.value should be true.
return doSomething();
})
.then(function(result){
// result.value should be true.
// this is the result of the second call.
});
passing the result of the previous - resolved - promise.
If promises are rejected because of some exceptions:
deferred.reject({value: false});
you can trap the error and stop the execution in the chain:
doSomething()
.then(function(result){
// result.value should be true.
return doSomething();
})
.then(function(result){
// result.value should be true.
// this is the result of the second call.
})
.catch(function(reason){
// reason for failure.
});
Finally you can use the finally to do some cleanup or other things:
doSomething()
.then(function(result){
// result.value should be true.
return doSomething();
})
.then(function(result){
// result.value should be true.
// this is the result of the second call.
})
.catch(function(reason){
// reason for failure.
})
.finally(function(){
// it's going to be executed at the end of the chain, even in case of error trapped by the catch.
});
Things are not so simple, though. At the beginning you might find yourself spending a few hours debugging the code.
How would I fix your code ?
First of all I would create a function which fetch the ids calling the web api:
function fetchIds() {
console.log('Fetching Ids ...');
var deferred = $q.defer();
$http({
method: 'GET',
url: 'http://localhost/postIds',
params: {}
})
.success(function(data) {
deferred.resolve(data);
})
.error(function(data, status) {
deferred.reject(data);
});
return deferred.promise;
}
As you can see I've implemented the system described above.
$http already returns a promise but I wrapped it creating a new promise, anyway.
Then I would have to query the database to find the non existing ids (I didn't put my code in a loop as it is easier to get all the records in one call):
function queryForIds(ids) {
console.log('Querying for Ids ' + ids.toString() + ' ...');
var deferred = $q.defer();
var params = [];
for (var i = 0; i < ids.length; i++) {
params.push('?');
}
window.myDatabase.transaction(function(tx) {
tx.executeSql("SELECT * FROM posts WHERE postId IN (" + params.join(',') + ")", ids,
function(tx, results) {
deferred.resolve(results.rows);
},
function(tx, reason) {
deferred.reject(reason);
});
});
return deferred.promise;
}
My code is going to be slightly different from your as I've used WebSql cause I wanted to test it in the browser.
Now we need to find the ids which do not exist in the db:
function getNonExistingIds(ids, dbData) {
console.log('Checking if Ids ' + ids.toString() + ' exist in the db ...');
if (!ids || ids.length === 0) {
console.log('No ids');
return [];
}
if (!dbData || dbData.length === 0) {
console.log('database is empty');
return ids;
}
var dbIds = [];
angular.forEach(dbData, function(data, key) {
dbIds.push(data.postId);
});
var nonExisting = [];
angular.forEach(ids, function(id, key) {
var found = $filter('filter')(dbIds, id, true);
if (found.length === 0) {
nonExisting.push(id);
}
});
return nonExisting;
}
This function does not return a promise but you still can pipe it like you would do with a real promise (You'll find out how later).
Now we need to call the web api to fetch the posts for the ids which couldn't be found in the database:
function fetchNonExisting(ids) {
if (!ids || ids.length === 0) {
console.log('No posts to fetch!');
return;
}
console.log('Fetching non existing posts by id: ' + ids.toString() + ' ...');
var promises = [];
angular.forEach(ids, function(id, key) {
var promise = $http({
method: 'GET',
url: 'http://localhost/post/' + id,
params: {}
});
promises.push(promise);
});
return $q.all(promises);
}
Things here get interesting.
Since I want this function to return one and only result with an array of posts I've created an array of promises.
The $http service already returns a promise. I push it in an array.
At the end I try to resolve the array of promises with $q.all. Really cool!
Now we need to write the posts fetched in the database.
function writePosts(posts) {
if (!posts || posts.length === 0)
{
console.log('No posts to write to database!');
return false;
}
console.log('Writing posts ...');
var promises = [];
angular.forEach(posts, function(post, key) {
promises.push(writePost(post.data));
});
return $q.all(promises);
}
Again, we are chaining an array of promises so that we can resolve them all in one go.
This function up here calls writePost:
function writePost(post) {
return $q(function(resolve, reject) {
window.myDatabase.transaction(function(tx) {
tx.executeSql("INSERT INTO posts (postId, title, user, content) VALUES (?,?,?,?)", [post.id, post.title, post.user, post.content],
function(tx, result) {
console.log('INSERT result: ' + result);
resolve(result);
},
function(tx, reason) {
console.log('INSERT failure: ' + reason);
reject(reason);
});
});
});
}
this bit here is quite complicated cause WebSql doesn't work with promises and I want them to be resolve in one go and get the result back.
Now what can you do with all these functions? Well, you can chain them as I explained earlier:
var ids = [];
fetchIds()
.then(function(data) {
console.log(data);
ids = data;
return queryForIds(data);
})
.then(function(dbData) {
return getNonExistingIds(ids, dbData);
})
.then(function(nonExistingIds) {
console.log('Non existing ids: ' + nonExistingIds);
return fetchNonExisting(nonExistingIds);
})
.then(function(response) {
return writePosts(response);
})
.then(function(result) {
console.log('final result: ' + result);
})
.catch(function(reason) {
console.log('pipe error: ' + reason);
})
.finally(function() {
// Always Executed.
});
The final result can find found in this gist.
If you prefer to download the whole application and test it on your PC, this is the link (myApp).
Related
I have a application where users can add movies to their watchlist. I want to prevent users from adding a movie that already exists in their watchlist.
This my addMovie function in my Angular controller:
$scope.addMovie = function (movie) {
movieFactory.selectMovie(movie).then(function(response){
movieFactory.addMovie(response);
Notification.success(movie.title + ' has been added to your watchlist');
$scope.movies = [];
$scope.overlay = false;
$scope.searchquery = '';
$rootScope.$broadcast('onAddMovieEvent', response);
});
};
I pass the movie object through from my initial ng-click. Then I request a selectMvoie function in the movieFactory that gets the correct movie data from the TMDB api.
Then I call the addMovie function in the movieFactory factory with the response from selectMovie function.
factory.addMovie = function (movie) {
var deferred = $q.defer();
$http({
method: 'POST',
url: '/movies',
data: movie
})
.success(function (data) {
deferred.resolve(data);
})
.catch(function () {
deferred.reject();
});
return deferred.promise;
};
This does a /post request which goes into my Express route:
router.post('/', function(req,res){
pool.getConnection(function(err, connection) {
connection.query('SELECT * FROM user_movieid WHERE movie_id= ? AND userid= ?' ,[req.body.id, req.user.id] , function(err, result) {
for (var i = result.length - 1; i >= 0; i--) {
if (movie.id === result[i].movie_id) {
console.log('exists');
}
}
});
});
})
There's also a connection.query that posts the movie into the database but that's not relevant now.
My situation now is that when I post a movie that already exists my node console shows the message exists but still posts the movie (obviously).
How would I return an "error" status back to my addMovie function in my Angular controller so I can do a if statement to show a different notification?
If you want to keep your API 'restful' then I would return a response with a non 2xx status code. I would think 409 Conflict is appropriate for this case.
router.post('/', function(req,res){
pool.getConnection(function(err, connection) {
connection.query('SELECT * FROM user_movieid WHERE movie_id= ? AND userid= ?' ,[req.body.id, req.user.id] , function(err, result) {
for (var i = result.length - 1; i >= 0; i--) {
if (movie.id === result[i].movie_id) {
console.log('exists');
// choose an appropriate status code, probably a conflict 409 in this case
return res.status(409).send('Movie already exists in your watchlist')
}
}
});
});
})
Then in your Angular code you can notify the user that the movie is already in their list.
$scope.addMovie = function (movie) {
movieFactory.selectMovie(movie).then(function(response){
movieFactory.addMovie(response);
Notification.success(movie.title + ' has been added to your watchlist');
$scope.movies = [];
$scope.overlay = false;
$scope.searchquery = '';
$rootScope.$broadcast('onAddMovieEvent', response);
})
// catch any errors
.catch(function() {
// ideally you should check the status code here and probably handle other non 409 status codes differently
// since this .catch will execute for any other 4xx and 5xx errors
Notification.fail(movie.title + ' is already in your watchlist');
});
};
I created following controller with 2 service calling with services. Second response comes before then first. i want to do like i need first response first and second response second. but i just stuck with async and sync please help me for solving.
Second call is depends on first call. For example if first call returns 10 record then i have to call second web service 10 time taking id from first response. so i use for loop but it is not proper.
Controller
var mycompaigndata = [];
asyncService.loadDataFromUrls($http.get(WSURL + 'api/first/',
{
headers:
{
"Authorization":'Bearer <my-token>'
}
}))
.then(function(data)
{
console.log(data);
});
asyncService.loadDataFromUrls($http.get(WSURL + 'api/second',
{
headers:
{
"Authorization":'Bearer <my-token>'
}
}))
.then(function(data)
{
console.log(data);
});
Service
app.service('asyncService', function($http, $q)
{
return {
loadDataFromUrls: function(url)
{
var deferred = $q.defer();
var urlCalls = [];
urlCalls.push(url);
$q.all(urlCalls)
.then(
function(results)
{
deferred.resolve(results)
},
function(errors)
{
deferred.reject(errors);
},
function(updates)
{
deferred.update(updates);
});
return deferred.promise;
}
};
});
To make sure the second calls are executed after the first one is finished, put the second call within then of the first call. To make multiple 'second' calls depending on the number of results of the first call, use $q.all.
asyncService.loadDataFromUrls('api/first/')
.then(function(firstData) {
//assuming firstData is an array of 'x' items, do a call for each of these items:
console.log('results of first call holds ' + firstData.length + ' items');
var promises = [];
for(var i = 0; i<firstData.length; i++){
var id = firstData[i].id;//you can use this to pass to the second call
promises.push(asyncService.loadDataFromUrls('api/second'));
}
return $q.all(promises);
})
.then(function(results) {
//'results' is an array of results, the nth item holds the result of the 'nth' call to loadDataFromUrls
for(var i = 0; i<results.length; i++){
console.log('result nr. ' + i + ' :' + results[i])
}
});
By using return $q.all(promises), you're avoiding the promise pyramid of doom, and keep a flat structure.
Your service code doesn't need to loop anymore. As a sidenote, you can shorten the code of the service and avoid using the 'explicit promise construction antipattern' (see here) like this:
app.service('asyncService', function($http, $q)
{
return {
loadDataFromUrls: function(url)
{
return $http.get(WSURL + url, {
headers: {
"Authorization": 'Bearer <my-token>'
}
}).then(function(response){ return response.data; });
}
};
});
Your asyncService seems completely unnecessary and unuseful.
It sounds like you just need to learn how to chain promises and use $q.all correctly:
function queryApi(subUrl) {
return $http.get(WSURL + subUrl, {
headers: {
"Authorization":'Bearer <my-token>'
}
}).then(function (result) { return result.data; });
}
queryApi('api/first/')
.then(function (data) {
return $q.all(data.map(function (entry) {
return queryApi('api/second/' + entry.id);
}));
})
.then(function (results) {
console.log(results);
});
put second request inside first request promise:
var mycompaigndata = [];
asyncService.loadDataFromUrls($http.get(WSURL + 'api/first/',
{
headers:
{
"Authorization":'Bearer <my-token>'
}
}))
.then(function(data)
{
asyncService.loadDataFromUrls($http.get(WSURL + 'api/second',
{
headers:
{
"Authorization":'Bearer <my-token>'
}
}))
.then(function(data)
{
console.log(data);
});
});
I think the best answer is to use loop since you need to iterate the response to get the id.
asyncService.loadDataFromUrls(WSURL + 'api/first/')
.then(function(data) {
//iterate to get the id
//call service again
asyncService.loadDataFromUrls(WSURL + 'api/first/')
.then(function(data) {
//code here
});
});
Service
app.service('asyncService', function($http, $q) {
return {
loadDataFromUrls: function(url) {
return $http.get(url, {
"Authorization":'Bearer <my-token>'
});
}
};
});
I'm trying to create an AngularJS service, which returns data based on several HTTP requests. But i seem to just not get it to work.
The REST call works as follow:
get /index which returns an array of urls
call each of the url's, and add the result to an array
I expect that at the end of the call of the service function, that i receive a data structure containing all the data from the url's.
My current, somewhat working code uses callbacks, but even though it works in one controller, it does not in another. I want to correctly use promises, but i'm already confused with success vs then.
My service:
// Get a image
obj.getByUrl = function (imageUrl, callback) {
$http.get('https://localhost:9000' + imageUrl).success(function (data) {
callback(data);
});
}
// Get all images
obj.getAll = function(callback) {
$http.get('https://localhost:9000/1.0/images').success(function (data) {
if (data.status != "Success") {
console.log("Err");
}
var images = [];
for(var n=0; n < data.metadata.length; n++) {
var c = data.metadata[n];
obj.getByUrl(c, function(data2) {
images.push(data2.metadata);
});
}
callback(images);
});
}
i'd like to use the service in a controller resolve like this:
resolve : {
images: function(ImagesServices, $route) {
return ImagesServices.getState($route.current.params.containerName)
},
I came as far as this, but it does only return the data of the index call, not the aggregated data:
obj.getAll3 = function() {
var images = [];
var promises = [];
//var httpPromise = $http.get('https://localhost:9000/1.0/images');
var httpPromise = $http({
url: 'https://localhost:9000/1.0/images',
method: 'GET',
});
return httpPromise.success(function(data) {
var data2 = data.metadata[0];
// angular.forEach(data.metadata, function(data2) {
console.log("D11: " + JSON.stringify(data2));
//var inPromise = $http.get('https://localhost:9000' + data2)
var inPromise = $http({
url: 'https://localhost:9000' + data2,
method: 'GET',
})
.success(function (data2) {
console.log("D2: " + JSON.stringify(data2));
images.push(data2);
});
promises.push(inPromise);
// });
return $q.all(promises).then(function() {
return images;
});
});
}
Maybe someone can point me into the right direction?
This is the typical case where chaining promises, and using $q.all(), is adequate:
/**
* returns a promise of array of images
*/
obj.getAll = function() {
// start by executing the first request
return $http.get('https://localhost:9000/1.0/images').then(function(response) {
// transform the response into a promise of images
// if that's not possible, return a rejected promise
if (data.status != "Success") {
return $q.reject("Error");
}
// otherwise, transform the metadata array into
// an array of promises of image
var promises = data.metadata.map(function(imageUrl) {
return $http.get('https://localhost:9000' + imageUrl).then(function(resp) {
return resp.data;
});
});
// and transform this array of promises into a promise
// of array of images
return $q.all(promises);
});
}
This avoid the callback antipattern, and uses chaining. It's a bit long to explain here, but I wrote a blog post that should, hopefully, make the above code clear: http://blog.ninja-squad.com/2015/05/28/angularjs-promises/
The past view days I read a lot of best practices in handling with promises. One central point of the most postings where something like this:
So if you are writing that word [deferred] in your code
[...], you are doing something wrong.1
During experimenting with the error handling I saw an for me unexpected behavior. When I chain the promises and It run into the first catch block the second promise gets resolved and not rejected.
Questions
Is this a normal behavior in other libs / standards (e.g. q, es6), too and a caught error counts as solved like in try / catch?
How to reject the promise in the catch block so that the second gets, called with the same error / response object?
Example
In this example you see 'I am here but It was an error'
Full Plunker
function BaseService($http, $q) {
this.$http = $http;
this.$q = $q;
}
BaseService.prototype.doRequest = function doRequest() {
return this.$http({
method: 'GET',
url: 'not/exisint/url'
})
.then(function (response) {
// do some basic stuff
})
.catch(function(response) {
// do some baisc stuff e.g. hide spinner
});
}
function ChildService($http, $q) {
this.$http = $http;
this.$q = $q;
}
ChildService.prototype = Object.create(BaseService.prototype);
ChildService.prototype.specialRequest = function specialRequest() {
return this.doRequest()
.then(function (response) {
alert('I am here but It was an error');
})
.catch(function (response) {
// do some more specific stuff here and
// provide e.g. error message
alert('I am here but It was an error');
return response;
});
}
Workaround:
With this workaround you can solve this problem, but you have to create a new defer.
BaseService.prototype.doRequest = function doRequest() {
var dfd = this.$q.defer();
return this.$http({
method: 'GET',
url: 'not/exisint/url'
})
.then(function (response) {
// do some basic stuff
dfd.resolve(response);
})
.catch(function(response) {
// do some basic stuff e.g. hide spinner
dfd.reject(error);
});
}
Your workaround is almost correct, you can simplify it to the following:
BaseService.prototype.doRequest = function doRequest() {
return this.$http({
method: 'GET',
url: 'not/exisint/url'
})
.then(function (response) {
// do some basic stuff
return response;
}, function (error) {
return this.$q.reject(error);
});
}
$q.reject is a shortcut to create a deferred that immediately get's rejected.
Yes, this is default behaviour in other libraries as well. .then or .catch simply wraps the return value into a new promise. You can return a rejected promise to make the .catch chain work.
You can also do the opposite, for instance when you want to reject the promise in the success callback for whatever reason:
function getData() {
return this.$http.get(endpoint).then(result => {
// when result is invalid for whatever reason
if (result === invalid) {
return this.$q.reject(result);
}
return result;
}, err => this.$q.reject(err));
}
getData().then(result => {
// skipped
}, error => {
// called
});
See example above
Just to add to Dieterg's answer and to your workaround, you can also wrap the code into $q constructor:
BaseService.prototype.doRequest = function doRequest() {
return $q(function (resolve, reject) {
$http.get('not/exisint/url').then(function (response) { // success
/* do stuff */
resolve(response);
}, function (error) { // failure
/* do stuff */
reject(error);
});
});
};
So I have a simple example of using $q.all to batch $resource calls, what I want to know is why is my update handler never called?
I would have thought it would be called after each promise is successfully completed?
Only the result handler is called. What am I doing wrong?
Here is the code snippet:
var promises = [];
angular.forEach($scope.mappedData, function(item) {
var resource = new Resource(item);
promises.push(resource.$save());
});
$q.all(promises).then(
function(result) {
console.log('result', result);
},
function(error) {
console.log('error', error);
},
function(notify) {
console.log('notify', notify);
}
);
$q.all creates a new singular promise that once all the previous promises are complete will then continue on. If you want to do each one individually you'll have to reference them individually.
I had the same problem, and I came with this solution. I've tried to arrange the code for your case.
var results = [], errors = [], d = $q.defer()
angular.forEach($scope.mappedData, function(item) {
var resource = new Resource(item);
resource.$save().promise
.then(function (result) {
results.push(result)
if(results.length == $scope.mappedData.length)
d.resolve(results)
}, function (err) {
errors.push(err)
results.push(null) // Very important =P
}, function (notf) {
d.notify(notf)
})
})
d.promise.then(function (results) {
console.log(results)
}, function (err) {
console.error(err)
}, function (notf) {
console.info(notf)
})
Let me know if it helps ;)