Here's the Plunkr
A common scenario, I have a collection of items displayed in an ng-repeat. For each row displayed I have a button that initiates a process (file upload), and a status field. I would like my UI to reflect whenever the status of the process changes. This should be easy in Angular w/ the 2-way binding, right?
I created a 2nd (child) controller on the ng-repeat so that I could simply update the status of a item in it's own scope rather than deal with a collection of items, especially since this process is asynchronous and the user will likely upload many files concurrently.
The problem: My understanding of $scope in Ang/JS is lacking - lol. Seriously though, the bound {{xxx}} value in the UI is not updating when the scoped model value is update. Click any one of the buttons and watch the alerts. How can I get the UI to update correctly?
FYI - in actuality that button calls an API on an external library to upload a file and returns to me a url to check the status of my upload. I then poll the url in a setInterval() loop to ping for the status until completion or error. I have simplified that portion in the Plunkr because this complexity itself is not the problem. Plunkr
<!DOCTYPE html>
<html ng-app="myapp">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="style.css" />
<script data-require="[email protected]" src="http://code.angularjs.org/1.2.7/angular.js" data-semver="1.2.7"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<table>
<th></th>
<th>Id</th>
<th>Name</th>
<th>Status</th>
<tr ng-repeat="item in items" ng-controller="ChildCtrl">
<td><button ng-click="updateStatus(item)">click</button></td>
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.status}}</td>
</tr>
</table>
</body>
</html>
JS
var app = angular.module('myapp', []);
app.controller('MainCtrl', function($scope) {
$scope.items = [ {id: 1, name: "Moe", status: "init"}
, {id: 3, name: "Larry", status: "init"}
, {id: 2, name: "Curly", status: "init"}
];
});
app.controller('ChildCtrl', function($scope) {
$scope.updateStatus = function(item){
$scope.myItem = item;
alert('child: ' + item.id + ' status: ' + item.status);
item.status = 'clicked';
alert('status just update in UI to: ' + item.status);
callResult = fakeAjaxCall($scope);
alert('callResult: ' + callResult);
};
var fakeAjaxCall = function(scope){
setTimeout(function (item) {
if (-1 == -1) { //success
result = "Wow, it worked!";
alert('current status: ' + scope.myItem.status);
alert('ajax result: ' + result);
scope.myItem.status = result;
alert('new status: ' + scope.myItem.status);
alert("but the status in the UI didn't update");
}
}, 2000);
};
});
You would need to use $timeout instead of setTimeout
to have the digest cycle invoked from within angular which updates the modal or you must invoke digest cycle yourself ( by wrapping the code scope.$apply()
, scope.evalAsync
etc...) , Reason is that angular has no idea when setTimeout
is done since it does not happen within angular. You should try not to manually invoke digest cycle as much as possible when you have an angular way to do things and you are free to use it. In this case you can replace setTimeout
with $timeout
and the model changes will be get reflected in the view automatically since angular invoke a digest cycle when the $timeout
is done. One more advantage is that when using $timeout
it returns a promise as well which you can chain through and still have promise pattern implemented when opposed to setTimeout
.
app.controller('ChildCtrl', function($scope, $timeout) {
$scope.updateStatus = function(item){
$scope.myItem = item;
console.log('child: ' + item.id + ' status: ' + item.status);
item.status = 'clicked';
console.log('status now updates in UI to: ' + item.status);
callResult = fakeAjaxCall($scope);
console.log('callResult: ' + callResult);
};
var fakeAjaxCall = function(scope){
$timeout(function (item) {
if (-1 == -1) { //success
result = "Wow, it worked!";
console.log('current status: ' + scope.myItem.status);
console.log('ajax result: ' + result);
scope.myItem.status = result;
console.log('new status: ' + scope.myItem.status);
console.log("but the status in the UI doesn't update");
}
}, 2000);
};
Plnkr
While debugging async operations, i would recommend usage of console.log (or $log
) instead of alert for debugging.
When you change scope variables outside of angular like you are in the timeout callback you can tell angular to update the digest cycle using $scope.$apply.
var fakeAjaxCall = function(scope){
setTimeout(function (item) {
if (-1 == -1) { //success
result = "Wow, it worked!";
alert('current status: ' + scope.myItem.status);
alert('ajax result: ' + result);
scope.$apply(scope.myItem.status = result); // <---- Changed
alert('new status: ' + scope.myItem.status);
alert("but the status in the UI doesn't update");
}
}, 2000);
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With