How to avoid nested call in ionic / angular?
I am completely 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,
-
get all ids from server
-
If id doesn't exist in db (sqlite)
-
get a message by id from the server
-
Insert message into db
It ends up being deeply nested, ugly.
what is the ionic, angular way of doing this?
source to share
Like others have suggested, the best option is to use promises, so you don't need to insert assertions as you do.
AngularJs uses $ q promises :
A service that helps you run functions asynchronously and use them to return values (or exceptions) when they are executed.
There are many articles on the internet about promises and how to bind them.
I recently found this article which explains common mistakes using promises.
It's worth reading because it goes deeper into this topic.
In AngularJs, you will create a promise using a service $q
:
function doSomething() {
var deferred = $q.defer();
deferred.resolve({value: true});
return deferred.promise;
}
This bit of code returns a promise which is allowed - since there is no async operation when it is called. It will return an object with property value
= true.
The cool thing about promises is 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.
});
transferring the result according to the previous - resolved promise.
If promises are rejected due to some exception:
deferred.reject({value: false});
you can catch the error and stop execution on 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 finally
for cleaning 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 going to be executed at the end of the chain, even in case of error trapped by the catch.
});
The whole thing is not so simple. At first, you can spend a few hours debugging your code.
How can I fix your code?
First of all, I would create a function that retrieves the ids that call 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 have implemented the system described above.
$http
already returns a promise, but I wrapped it in a new promise.
Then I would have to query the database to find non-existing IDs (I did not 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 will be slightly different from yours as I was using WebSql because I wanted to test it in a browser.
Now we need to find identifiers that don't 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 can still broadcast it as if you were doing a real promise (you will learn how later).
Now we need to call the web api to fetch messages for ids that cannot 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);
}
Here are interesting.
Since I want this function to return one and only result with an array of messages, I created an array of promises. The $ http service is already returning a promise. I am pushing it into an array.
At the end, I am trying to resolve the promises array with $q.all
. Really cool!
Now we need to write the messages loaded into 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 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 is pretty tricky here because WebSql doesn't work with promises and I want them to be resolved in one go and return a result.
Now, what can you do with all these features? Well, you can link 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 be found in this gist .
If you want to download the entire application and test it on your PC, this is the link ( myApp ).
source to share