Reputation: 144
I have a view where I render tweets. Inside the tweets, there is the user_mentions array. From this array, I want to get one user, make another request to the API, get the avatar for the user and then render it. The view looks like:
<md-item ng-repeat="item in content">
<div class="md-tile-left" ng-if="item.show">
<img ng-src="{{item.thumb}}" class="face" alt="{{item.name}}">
</div>
<div class="md-tile-content">
{{item.text}}
</div>
</md-item>
content variable will contain all the tweets. Initially, content[i].show is false for all i. The controller code looks like:
for(var i = 0; i < data.length; i++) {
if(data[i].user_mentions[0]) {
UserAvatar.get(data[i].user_mentions[0].screen_name).then(function(data){
$scope.content[i].thumb = data.avatar;
$scope.content[i].show = true;
});
}
}
Now, the problem I am facing is that after the UserAvatar service is called, variable i will continue to increment. So, by the time the callback is reached, i will have the value of data.length (hence, an error of undefined will be thrown). I have found a work-around by passing i to the API and then getting it back:
UserAvatar.get(data[i].user_mentions[0].screen_name, i).then(function(data){
$scope.content[i].thumb = data.avatar;
$scope.content[i].show = true;
});
I realise that this is just a hack to make it work, but is there a more clever way of doing it?
Upvotes: 4
Views: 1058
Reputation: 10084
The problem is that your attempting to use closures inside a for loop which doesn't work as you've discovered. Your hack is one option to fix it. Another is to wrap your loop in a function either using forEach()
/map()
or an IIFE ((function(i) { ... })(i);
).
In your example I'd do the following:
var promises = data.map(function(item, index) {
if (item.user_mentions[0]) {
return UserAvatar.get(item.user_mentions[0].screen_name)
.then(function(itemData) {
$scope.content[index].thumb = itemData.avatar;
$scope.content[index].show = true;
});
} else {
return $q();
}
});
$q.all(promises).then(function() {
// All complete
});
Upvotes: 1
Reputation: 28339
I realise that this is just a hack to make it work, but is there a more clever way of doing it?
As @MukeshAgarwal said in his comment, this is not really a hack since UserAvatar.get
is an asynchronous process.
Therefore, you have to capture the value of i
at the moment of the asynchronous invocation.
The method you used is not that bad, but another method is to use an immediately invoked function :
This does not work :
for (var i = 0; i < 10; i++) {
// do something async
setTimeout(function() {
console.log(i); // <- This will log 10 each time (the value of i at execution)
}, 1000)
}
But this works :
for (var i = 0; i < 10; i++) {
(function() {
var j = i; // <- The value of i is captured in j. j is in a closure
// do something async
setTimeout(function() {
console.log(j); // <- This will log 0, then 1, then 2 ... then 9
}, 1000)
})()
}
Upvotes: 1
Reputation: 4876
Whenever a function is called you have to remember that when it's executed all the variables in the scope (not only $scope
but also outer vars like i) created by an outer function will be visible to the function, I think you've already realized this, something important to realize is that you don't actually need i
in the handler of .then
you need to get the content at index i
but how do you get rid of this dependency? I present two possible solutions:
1)
function getAvatar(name, contentToUpdate) {
return UserAvatar.get(name)
.then(function (data) {
contentToUpdate.thumb = data.avatar;
contentToUpdate.show = true;
});
}
for(var i = 0; i < data.length; i++) {
if(data[i].user_mentions[0]) {
getAvatar(data[i].user_mentions[0].screen_name, $scope.content[i]);
}
}
When the handler of the .then
function is called then the i
dependency doesn't exist anymore
2)
for(var i = 0; i < data.length; i++) {
if(data[i].user_mentions[0]) {
UserAvatar.get(data[i].user_mentions[0].screen_name).then((function(content) {
return function (data) {
content.thumb = data.avatar;
content.show = true;
}
})($scope.content[i]));
}
}
In this solution a IIFE returns a function which will be the handler, but the IIFE will actually catch the value of the content being analyzed, therefore when the inner function returned (which is the onFulfilled
handler of .then
) is called there's no i
dependency
Bonus: One rule that has helped me a lot to deal with functions has been the following:
A function will be executed in the place it was defined
I don't remember where I read it, I think it was in Nicholas Zakas' Professional JavaScript for Web Developers, highly recommended :)
Upvotes: 3