0

I'm learning/experimenting with angular, and stumbled upon some weird behavior (in my opinion). I recreated my issue with the smallest example possible - the original code was a lot longer.

What I want this to do:
The user can enter a name. Whenever the name equals "error", its color will become red and an alert is displayed. When the alert is dismissed by the user (by clicking 'ok'), the name is reset to "initial name" and becomes green again.

What it doesn't do:
When name becomes "error", its color isn't red.

Name is still green

Code:

<html   lang="en-US" ng-app="myApp" ng-controller="myCtrl">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<body>
    <div>
        <input ng-model="name" ng-style="{color: myColor}"/>
    </div>
</body>

<script>
    var app = angular.module('myApp', []);
    app.controller('myCtrl', function($scope, $document) {
        $scope.name = "initial name";
        $scope.myColor = "green";
        $scope.$watch('name', function() {
            if($scope.name === "error") {
                $scope.myColor = "red"; // This line has no effect?
                alert("Not allowed! Resetting...");
                $scope.name = "initial name";
            } else {
                $scope.myColor = "green";
            }
        });
    });
</script>

I know that changing $scope.myColor should change the color (if I remove the else-clause, it remains red forever), but it seems the alert is blocking the desired behavior, for some reason. I also changed the order of the lines inside the if-clause, but that didn't help.

Question:
Any geniuses out here who can explain why "error" doesn't become red?
(I don't really need a solution, I just want to know why this happens.)

4
  • It becomes red and again changes the color as model value changes! Commented Oct 8, 2015 at 7:19
  • @RayonDabre Sure, after dismissing the alert. But before the alert pops up, "error" should become red (at least, that's what I expected). Commented Oct 8, 2015 at 7:20
  • If you use an alert you should use $window.alert and inject $window into your controller. Also suggest logging the output rather that using alert if it is for debugging purposes only, using the $log service. Commented Oct 8, 2015 at 7:24
  • just not use alert it block main js loop, so while alert show nothing happens Commented Oct 8, 2015 at 7:24

8 Answers 8

5

alert() is a blocking call which blocks everything else until it is dismissed. Therefore the $scope.myColor is changed to red but before the angular digest cycle completes, alert pops up and blocks everything else. Once you dismiss alert, the name is reset, and the color is set back to normal.

Since everything happens in milliseconds, you'e not able to see it. So, if you put your alert in a 1-2 seconds timeout, you should be able to see the red color of error.

Sign up to request clarification or add additional context in comments.

3 Comments

even though this will work, still better to just not use alert
@charlietfl I totally agree with you but since mentioned in the OP that this is only for learning purpose and no solution is needed so this was just to help him understand why it happens :-)
wasn't trying to rain on your answer. Put that there mostly for OP. Can definitely have other unexpected issues crop up that may not be apparent each use
2

As others have mentioned, alert blocks all processing until the alert is closed. You could either use another system such as a $modal from ui-bootstrap (which you probably want to do), but if you absolutely want to use alert, then you can use setTimeout/$timeout to ensure that the alert occurs at the beginning of the next $digest cycle.

$timeout(function() {
  alert('hello, world')
}, 0)

However this isn't guaranteed to execute in the order you want and as a result I'd really just recommend using ui-bootstrap or similar like others have suggested.

1 Comment

Thanks for the catch, haven't used Angular in a little while
0

alert blocks all script execution and is firing before a digest can complete.

It's not a good thing to use in an angular app which does a lot of things under the hood differently than in a regular page.

I would suggest you use a javascript driven alert system. There are numerous available angular modules for this

Comments

0

Since you update the value to red inside the watch but also change the name directly after that the callback to the watch on name will be triggered again changing it back to green right away. You'd may expect the text to be red during the alert being visible but since you are still inside the watch and the digest cycle is 'on hold' (because of the alert) you won't see this.

Comments

0

Angular does not follow all the fields of $scope all the time. What it does - it runs $digest from time to time. In this case, after the body of $watch function finished.

So, in this case, you changed the field, showed alert and after you closed the alert, it changed the name, started $digest, which started this function once more.

Comments

0

Angular may be working in a way that looks real time but actually works by collecting all the changes to "watched" variables and then applies them to the view. Inside a $watch you can't apply the changes until the code ends but the alert pauses the execution. See: https://www.ng-book.com/p/The-Digest-Loop-and-apply/

You can solve by doing this:

<html   lang="en-US" ng-app="myApp" ng-controller="myCtrl">
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
<body>
    <div>
        <input ng-model="name" ng-style="{color: myColor}"/>
    </div>
</body>

<script>
    var app = angular.module('myApp', []);
    app.controller('myCtrl', function($timeout, $scope, $document) {
        $scope.name = "initial name";
        $scope.myColor = "green";
        $scope.$watch('name', function() {
            if($scope.name === "error") {
              $timeout(function(){
                $scope.myColor = "red"; // This line has no effect?
                alert("Not allowed! Resetting...");
                $scope.name = "initial name";
              });
            } else {
                $scope.myColor = "green";
            }
        });
    });
</script>

Comments

0

From the description of the scope lifecycle, this happens because of dirty checking.

Your assignment $scope.myColor = "green"; in your $watch changes the value of the model initiates further a $digest. The alert()halts processing before that can be completed.

https://docs.angularjs.org/guide/scope

After evaluating the expression, the $apply method performs a $digest. In the $digest phase the scope examines all of the $watch expressions and compares them with the previous value. This dirty checking is done asynchronously. This means that assignment such as $scope.username="angular" will not immediately cause a $watch to be notified, instead the $watch notification is delayed until the $digest phase. This delay is desirable, since it coalesces multiple model updates into one $watch notification as well as guarantees that during the $watch notification no other $watches are running. If a $watch changes the value of the model, it will force additional $digest cycle.

Comments

0

Well, Sometimes Angular does not digest as you expect it should and you have to force a digest cycle to enforce him.

You can do this by using

$timeout(function(){
    //Your content here
});

Or by using

$scope.apply(function(){
    //Your content here
});

This last method not recommended as it could trigger exceptions "digest already in progress"

I made you a plunker with your code working : plunker

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.