0

I'm trying to understand how to properly manipulate properties via a controller. The following code executes six updates over four seconds. Updates two and three are not reflected in the view. Why is this, and what do I need to do to have updates of those types affect the view?

Html

<div ng-controller="Controller"> 
    myValue: <span ng-bind="myValue"></span>
</div>

Javascript

var app = angular.module('myApp', []);

app.controller('Controller',  function ($scope, $interval) {
    $scope.myValue = "first";
    console.log($scope.myValue);

    setTimeout(function() { 
        $scope.myValue = "second";  // never updates
        console.log($scope.myValue);
        $scope.$emit("my-event", "third"); // never updates
        console.log($scope.myValue);
        $interval(function() {
            $scope.$emit('my-event', "fourth");
        }, 1000, 1);
    }, 1000);

    $interval(function() {
        $scope.myValue = "fifth";
        console.log($scope.myValue);
        $interval(function() {        
            $scope.$emit("my-event", "sixth");
        }, 1000, 1);
    }, 3000, 1);

    $scope.$on('my-event', function (event, arg) {
        $scope.myValue = arg;
        console.log(arg);
    });
});

JSFiddle

Stafford Williams
  • 9,696
  • 8
  • 50
  • 101

3 Answers3

4

Use $timeout instead of setTimeout to opt-in to the digest cycle. second won't show since the turn of the digest cycle overrides the value of myValue.

Updated fiddle: https://jsfiddle.net/d9gbpddy/4/

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
  • I'm using setTimeout as an example only, how can it be fixed without switching the function? – Stafford Williams Aug 07 '15 at 15:29
  • @StaffordWilliams i'm not sure what you mean - if this isn't the code that has a problem, how am i supposed to solve it? – Daniel A. White Aug 07 '15 at 15:30
  • I was just using a setTimeout as an example of a function being executed at a later stage outside the initialising of the constructor. Had I known $timeout was a function, I would have used that instead of the $interval function in the example to make it clearer that I understand that the updates occur properly when angular functions are calling them, however I'm trying to find out how I can update the view when things are triggered outside angular... if that makes sense? – Stafford Williams Aug 07 '15 at 15:41
  • You can use `$scope.$apply` or wrap the external stuff - its hard to know exactly what is out side of angular unless you are explicit. – Daniel A. White Aug 07 '15 at 16:19
0

You can try {{myValue}} instead of a <span> element

cbender
  • 2,231
  • 1
  • 14
  • 18
0

So I obviously wasn't clear enough in the original question, as the upvoted answer (correctly) suggests using $timeout rather than setTimeout, however the original intent was to understand why the updates were not being reflected in the view, and what could be done to have these types of updates (that originate outside angular) affect the view as was intended.

Read the Scope guide

So whilst I chose to skip the Scopes section of the developer guide because it looked to be the most boring, it was probably the most important, and it clearly points out some items imperative to understanding how angular binds data, notably the Scope Life Cycle which notes;

When the browser calls into JavaScript the code executes outside the Angular execution context, which means that Angular is unaware of model modifications. To properly process model modifications the execution has to enter the Angular execution context using the $apply method. Only model modifications which execute inside the $apply method will be properly accounted for by Angular.

There's an excellent answer here that further explains this concept. The first setence aptly reiterates the importance of understanding scope:

You need to be aware about how Angular works in order to understand it.

Don't just call $scope.$apply

So you start adding calls to $scope.$apply around the place to cater for these things that originate outside angular, but then eventually you start getting:

Error: $digest already in progress

Which means you can't call $scope.$apply whilst $digest is executing. After which you may think, well how can I conditionally call $scope.$apply based on whether the $digest is currently running. But, you don't need to do that...

Just use $timeout

Hah, like the upvoted answer, I know, but based on a different thought process I think. See this answer. $timeout is not just being used in place of setTimeout, but rather is being used (without a delay) to wrap any model updates that are called from outside the Scope Life Cycle, and doing so ensures no conflict with any currently processing $digest.

Wrapping up

So, in the original code snippet, the second and third updates are not reflected in the view because they are performed outside the Angular execution context. The fact that third update doesn't affect the model also means that calling events outside the execution context doesn't get you into the execution context either.

The fourth update is already wrapped inside $interval, which itself causes the updates that code to be run on the next digest. Therefore, updating the code to show an example of an event outside the angular execution context that causes its updates to be shown in the view is as follows:

setTimeout(function() { 
    $timeout(function({ // start wrap
        $scope.myValue = "second";  // now this updates!
        console.log($scope.myValue);
        $scope.$emit("my-event", "third"); // now this updates!
    })); // end wrap
    console.log($scope.myValue);
    $interval(function() {
        $scope.$emit('my-event', "fourth");
    }, 1000, 1);
}, 1000);
Community
  • 1
  • 1
Stafford Williams
  • 9,696
  • 8
  • 50
  • 101