57

I am trying to watch my model value from inside my linking function.

scope.$watch(attrs.ngModel, function() {
       console.log("Changed"); 
    });

When I change the model value inside my controller, the $watch function is not triggered.

$scope.myModel = "ACT";

$timeout(function() {
   $scope.myModel = "TOTALS"; 
}, 2000);

Fiddle: http://jsfiddle.net/dkrotts/BtrZH/4/

What am I missing here?

Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
Dustin
  • 8,217
  • 11
  • 33
  • 44
  • 3
    FYI, the reason that watching attrs.ngModel worked [over here](http://stackoverflow.com/questions/14567908/watch-controller-model-value-from-inside-directive) is because the directive was not creating a new scope. In this question, your directive is creating an isolate scope, so you need to watch something in that isolate scope. – Mark Rajcok Feb 04 '13 at 20:18
  • ... or you could just watch the model value and not worry about who's scope is what. :P haha. – Ben Lesh Feb 04 '13 at 20:31
  • 1
    @blesh, I'm actively reading around trying to determine if I like your solution better than what dnc253 provided. I have to say that I'm not keen on having to `require: ngModel` and write a function in the $watch, vs just `$watch('model', ...`. +1 for both of you though. – Mark Rajcok Feb 04 '13 at 20:55
  • @MarkRajcok Haha... it really doesn't matter. The most important thing is that it *works and is maintainable*. dnc's solution was perfectly fine, and I might use his in a lot of cases. However, if someone comes here actively looking for a solution for how to watch the modelValue specifically on ngModel (which is what the title of the question reads like), I wanted to provide that answer for them. – Ben Lesh Feb 04 '13 at 21:38

6 Answers6

153

You'll need to watch a function that returns the $modelValue you're watching.

The following code shows a basic example:

app.directive('myDirective', function (){
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
           scope.$watch(function () {
              return ngModel.$modelValue;
           }, function(newValue) {
               console.log(newValue);
           });
        }
     };
});

Here's a plunker of the same idea in action.

Ashley Davis
  • 9,896
  • 7
  • 69
  • 87
Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • 5
    +1 for your answer as well. Both answers really are correct. Mine provided a solution to the specific problem linked in the question, whereas yours provides a general solution for those looking for a way to `$watch` what is specified in ngModel. – dnc253 Feb 04 '13 at 21:45
  • If I try using an isolate scope with this solution, it only works if I use `scope: { name: '=ngModel' }`. If I try to use a different isolate scope property name -- e.g., `scope: { model: '=ngModel' }` -- it doesn't work!? Maybe when the ngModelController first runs it somehow knows about the name in the HTML/view?? – Mark Rajcok Feb 09 '13 at 23:36
  • @MarkRajcok: That's pretty weird, I don't get such an error. Is it possible you have a parent scope interfering with that name? – Ben Lesh Feb 11 '13 at 02:35
  • 1
    Here's a [fiddle](http://jsfiddle.net/mrajcok/AXBut/1/) that shows what happens if you use a different name in the object hash than what ng-model uses in the HTML. See also http://stackoverflow.com/a/14792601 – Mark Rajcok Feb 11 '13 at 18:22
  • That has to be a bug. It's probably not a common circumstance. – Ben Lesh Feb 11 '13 at 18:27
  • Okay, the more I think about this, the more I think it's because the ngModel controller is set up against the parent scope. And when it updates the parent scope, it's not doing so in a way that triggers the watches associated with the bidirectional binding in your directive. It's still weird though, and I'm only speculating. – Ben Lesh Feb 11 '13 at 19:02
  • This solution didn't work for me with isolated scope. I changed this line: `return ngModel.$modelValue;` to `return scope.ngModel;` and it's working fine. Maybe it helps someone. – Łukasz Wiatrak Nov 08 '13 at 07:33
  • @Lucasus what version of Angular? – Ben Lesh Nov 08 '13 at 15:14
  • @blesh version 1.2.0-rc.2 – Łukasz Wiatrak Nov 08 '13 at 15:21
  • Yeah, things have changed a bit, this answer was for 1.0.X ... 2 minor versions different means a lot in this case. – Ben Lesh Nov 08 '13 at 18:16
  • Note that the original solution did not work because there was a missing return statement. I have correct the problem. – Ashley Davis Aug 17 '14 at 22:14
  • Can you explain why `$modelValue` instead of `$viewValue`? (it works with both) – Francesco Belladonna Dec 22 '15 at 16:04
31

The problem is that you $watching attrs.ngModel which is equal to "myModel". You do not have "myModel" bound in your scope. You want to $watch "model". That is what is bound in the scope of your directive. See http://jsfiddle.net/BtrZH/5/

dnc253
  • 39,967
  • 41
  • 141
  • 157
23

The proper way to do this is:

app.directive('myDirective', function () {
  return {
    require: 'ngModel',
    link: function (scope, element, attrs, ngModel) {

        ngModel.$render = function () {
            var newValue = ngModel.$viewValue;
            console.log(newValue)
        };

    }
  };
});
Emmanuel
  • 4,933
  • 5
  • 46
  • 71
  • This binding work one time! I tried to bind ngModel to [nouiSlider](http://refreshless.com/nouislider/) component. And this solved my circular binding problem. Thanks! – bhdrk Aug 31 '15 at 09:55
  • 7
    It works because every time angular detects a change in this elements model it will call [$render](https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$render) to update it, so here we replace the original $render function to do whatever we want it to do. – Timo Huovinen Sep 23 '15 at 11:26
  • @TimoHuovinen What does the original $render function do? – Emmanuel Jun 22 '16 at 01:31
  • 2
    @HugoBaés - this is not always the case. For instance when using this on an input element, many [input types](https://github.com/angular/angular.js/blob/65f800e19ec669ab7d5abbd2f6b82bf60110651a/src/ng/directive/input.js) implement $render to correctly set the value shown to the user – Clark Feb 07 '19 at 06:33
8

Here is another way to do this:

app.directive('myDirective', function (){
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
           attrs.$observe('ngModel', function(value){ // Got ng-model bind path here
              scope.$watch(value,function(newValue){ // Watch given path for changes
                  console.log(newValue);  
              });
           });
        }
    };
});

Doing it that way you will be able to listen value changes with binds like that

Ashley Davis
  • 9,896
  • 7
  • 69
  • 87
user2978730
  • 81
  • 1
  • 1
4

This is an extension of @ Emmanuel's answer above to answer @Martin Velez, although I know it's pretty late! (Also I can't make comments yet, so if this isn't the right place for this, sorry!)

I'm not sure which version of Angular OP was using, but in Angular#1.2+ at least on the official docs https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#$render, $render is listed like this:

Called when the view needs to be updated. It is expected that the user of the ng-model directive will implement this method.

The $render() method is invoked in the following situations:

$rollbackViewValue() is called. If we are rolling back the view value to the last committed value then $render() is called to update the input control. The value referenced by ng-model is changed programmatically and both the $modelValue and the $viewValue are different from last time. Since ng-model does not do a deep watch, $render() is only invoked if the values of $modelValue and $viewValue are actually different from their previous value.

I interpret this to mean that the correct way to $watch an ngModel from a directive is to require ngModel and implement a link function that injects ngModelController. Then use the ngModel API that's built in to $render-on-change ($watch), or whatever else.

Timo Huovinen
  • 53,325
  • 33
  • 152
  • 143
RoboBear
  • 5,434
  • 2
  • 33
  • 40
1

There are 2 ways to do it.

1) You can use $attrs.[any_attribute] and set on it any listener

2) You can have isolated scope with 2 ways binding variable and set a listener on it.If you want more,here is a cool article

http://www.w3docs.com/snippets/angularjs/bind-variable-inside-angularjs-directive-isolated-scope.html

Hazarapet Tunanyan
  • 2,809
  • 26
  • 30