Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJs - field in UI not reflecting updated model value

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);
  };

});
like image 476
JM. Avatar asked Mar 19 '23 18:03

JM.


2 Answers

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.

like image 75
PSL Avatar answered Apr 01 '23 08:04

PSL


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);
};
like image 38
Mark Avatar answered Apr 01 '23 08:04

Mark