Angular.js: Onchange is getting triggered before the model update for isolate scope

Created on 21 Oct 2013  ·  11Comments  ·  Source: angular/angular.js

Onchange event for the elements having isolate scope is getting triggered before the model update.

Example:

<!doctype html>
<html ng-app="App">
    <head>

        <link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.js"></script>
        <script src="http://code.angularjs.org/1.0.6/angular.js"></script>
        <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script>

        <script type="text/javascript">
            "use strict"

            var App = angular.module('App', []);
            App.controller('Ctrl', function ($scope,$timeout) {

                $scope.change1 = function () {
                    console.warn("inside change", $scope.y);
                }

                $scope.change2 = function () {
                    console.warn("inside change", $scope.x);    
                }

            });

            App.directive("wminput", function () {
                return {
                    restrict: "E",
                    replace: true,
                    scope: {
                        "onChange":"&",
                        "value": "="
                    },
                    template: '<input type="text" data-ng-model="value" data-ng-change="onChange();">'
                }
            });

        </script>

    </head>
    <body>
        <div data-ng-controller="Ctrl">
            "Onchange will be triggered after the model update"
            <input data-ng-change="change1()" data-ng-model="y">
            <br><br>
            "Onchange will be triggered before the model update"
            <wminput on-change="change2()" value="x"></wminput>
        </div>

    </body>
</html>
$compile forms low works as expected confusing bug

Most helpful comment

Narretz, I strongly disagree. There is a contract made with the developer consuming the code that the two-way binding takes precedence. When a change is made anywhere along the chain of binding, the entire chain should be updated before continuing. So even if another directive inherits from this directive, and something further down binds to a binding of a binding... if that model is updated, the original controller scope model at the bottom of the stack should be updated in the same digest cycle before continuing on to other functions. It's what is expected from the consumer.

All 11 comments

Interesting corner case ... http://plnkr.co/edit/lsrga8qMcbgNWATrVht3?p=preview

This happens only when the onChange function is delegated to the parent scope width the '&' binding.

We have been working on a RAD tool based on angularjs.
This was a major bug as every widget in the application has isolateScope.
We have resolved the problem by creating a proxy method for onChange.

Have a look at : https://studio.wavemaker.com/

Regards, Vinay

Could you post an example of this proxy and how it is used? Might help in debugging this.

Will it be fixed anytime soon? It is sooooo confusing.

After further consideration, I have to say that this is expected behavior.. In the plnkr example, the value of the ngModel is a two bound string between the controller and the directive scope. Now when you are changing the value in the directive, ngModel will update the _isolate_ scope first, and fire the ngChange in the same digest. Since the two-way binding hasn't yet picked up a change to the directive scope, the ngChange in the parent scope gets the "old" value of x. Now the two-way binding watcher needs at least two digest cycles to stabilize the value, so the controller scope is only updated after the ngChange has been called.
I agree that this is confusing, but that's how it works. You can workaround this by binding an object to the directive which holds the value the you bind to ngModel. That way, the value is set by reference in both the isolate and the controller scope at the same time and available when ngChange is executed: http://plnkr.co/edit/fs7S6yX1a5aeo1Ese522?p=preview

Narretz, I strongly disagree. There is a contract made with the developer consuming the code that the two-way binding takes precedence. When a change is made anywhere along the chain of binding, the entire chain should be updated before continuing. So even if another directive inherits from this directive, and something further down binds to a binding of a binding... if that model is updated, the original controller scope model at the bottom of the stack should be updated in the same digest cycle before continuing on to other functions. It's what is expected from the consumer.

Slightly related, and likewise confusing. When using ngChange to trigger validation on another field that shares a ngModel, the change is triggered before the $modelValue is updated. Making validation convoluted.

Using $timeout as a workaround. Still confused why that's needed.

Example:
http://codepen.io/jakobadam/pen/qmBdrJ?editors=1010

regards, Jakob

Are there any plans to revisit this issue? Perhaps reopen?

I ran into this issue and reading through the comments, while the way it currently works due to the isolate scope makes sense; the outcome that comes from expecting an updated model when the on-change call fires and getting the model one iteration previous definitely is not the behavior you expect as a developer.

I adhere to the question, is this gonna be revisited?

I don't think so. The current behavior is expected and although it is not desirable in some cases, there are at least as many cases where it is. Changing it now would (a) be a breaking change and (b) make many usecases unsupported (e.g. where you want ngChange to modify the value _before_ it updates the parent). It's one of those cases where you can't have everyone happy, so we have to stick with the version that:

  • Makes sense.
  • Does not break existing users.
  • Supports more specific/advanced usecases, even if at the cost of little extra code (see below).

The current implementation of ngChange is tightly bound to the concept of ngModel. Directive bindings is a totally independent concept. The two can work together nicey, but should not affect the implementation/inner workings of one another.

#

As already mentioned, if your specific callback needs access to the updated value, there are a few options:

  1. Pass the updated value as argument to the callback.

  2. If you need it to be run asynchronously (i.e. after the digest is over and all bindings have been propagated), you can always use $timeout inside the callback.

  3. If you want an "async" version of ngChange, it is also trivial to implement as a custom directive. (You could also overwrite the built-in directive, but I would definitely not recommend that.) E.g.:

.directive('myAsyncChange', function($timeout) {
  return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, element, attr, ctrl) {
      ctrl.$viewChangeListeners.push(function() {
        $timeout(function() {
          scope.$eval(attr.myAsyncChange);
        });
      });
    }
  };
});
<input ng-model="foo" my-async-change="onChange()" />
Was this page helpful?
0 / 5 - 0 ratings

Related issues

coli picture coli  ·  62Comments

petebacondarwin picture petebacondarwin  ·  59Comments

fredrikbonander picture fredrikbonander  ·  55Comments

guruward picture guruward  ·  145Comments

leeola picture leeola  ·  76Comments