How to Watch Directive's Directive ng-model

Neel picture Neel · Apr 2, 2014 · Viewed 12.2k times · Source

I have a directive that uses the parent scope in that view. This directive has a child directive that uses an isolated scope. I am trying to get the parent directive to watch any changes done to the ngModel of the child directive and update its own modal if changes were made. Here is a jsfiddle that probably explains better: http://jsfiddle.net/Alien_time/CnDKN/

Here is the code:

   <div ng-app="app">
        <div ng-controller="MyController">

            <form name="someForm">
                <div this-directive ng-model="theModel"></div>
            </form>

         </div>
    </div>

Javascript:

    var app = angular.module('app', []);
app.controller('MyController', function() {

});

app.directive('thisDirective', function($compile, $timeout) {

    return {
        scope: false,

        link: function(scope, element, attrs) {
            var ngModel = attrs.ngModel;
            var htmlText = '<input type="text" ng-model="'+ ngModel + '" />' +
                           '<div child-directive ng-model="'+ ngModel + '"></div>';

            $compile(htmlText)(scope, function(_element, _scope) {
                element.replaceWith(_element);                
            });

            // Not sure how to watch changes in childDirective's ngModel ???????

        }, // end link
    } // end return

});

app.directive('childDirective', function($compile, $timeout) {
    return {
            scope: {
                ngModel: '='            
        },  

        link: function(scope, element, attrs, ngModel) {
            var htmlText = '<input type="text" ng-model="ngModel" />';

            $compile(htmlText)(scope, function(_element, _scope) {
                element.replaceWith(_element);                
            });   

            // Here the directive text field updates after some server side process
            scope.ngModel = scope.dbInsertId;

            scope.$watch('dbInsertId', function(newValue, oldValue) {
                if (newValue)
                    console.log("I see a data change!");  // Delete this later
                    scope.ngModel = scope.imageId;
            }, true);

        },

    } // end return
});

In the example, you can see that there is a text input inside a parent directive as well as its child directive. If you type inside each of them, the other model gets updated since they are binded by ngmodel. However, the child directive's text input gets updated after a server connection. When that happens, the text input in the parent directive doesnt get updated. So I think I need to watch the ngModel inside the child directive for any changes. How can I do that? Does it make sense?

Answer

JoseM picture JoseM · Apr 3, 2014

As @shaunhusain mentioned you have to use the ngModelController to interact with ngModel. On the ngModelController you can set up a watch on the $modelValue and you can change the value in the model by calling $setViewValue. Remeber that to use the ngModelController you need to add a require: "ngModel" to your directive definition object.

When you get a value from outside of angular (like from your database) and you in turn want to use that value to change the model value, you need to wrap that code in a scope.$apply()

app.directive('thisDirective', function($compile, $timeout, $log) {

    return {
        scope: false,
        require: 'ngModel',

        link: function(scope, element, attrs, ngModel) {
            ...

            scope.$watch(
                function(){
                    return ngModel.$modelValue;
                }, function(newValue, oldValue){
                    $log.info('in *thisDirective* model value changed...', newValue, oldValue);
                }, true);

        }, // end link
    } // end return

});

app.directive('childDirective', function($compile, $timeout, $log) {
    return {
        scope: {
            ngModel: '='
        },
        require: 'ngModel',

        link: function(scope, element, attrs, ngModel) {
            ...

            scope.$watch(
                function(){
                    return ngModel.$modelValue;
                }, function(newValue, oldValue){
                    $log.info('in *childDirective* model value changed...', newValue, oldValue);
                }, true);

            // make believe change by server
            setTimeout(function() {
                scope.$apply(function() {
                    ngModel.$setViewValue('set from the server...');
                };
            },5000);


        },

    } // end return
});

relevant jsfiddle http://jsfiddle.net/CnDKN/2/

BUT I don't think this is not really right usage of $setViewValue. According to the docs, that is supposed to be used to update the value that is shown on the UI, not necessarily the model value.

There is actually another way of making this work which I think is more straightforward and easier to use. Just use =attr to set up bi-directional binding of the property you want to use in both thisDirective and in childDirective. And then you can just set up the the ng-model attribute setting in your directive and when you first use it you don't even need to know that it is using ng-model underneath.

Here is the code that shows you what I mean:

app.directive('thisDirective', function($compile, $timeout, $log) {

    return {
        scope: {
            thisval: '='
        },

        link: function(scope, element, attrs) {

            var htmlText = '<input type="text" ng-model="thisval" />' +
                           '<div child-directive childval="thisval"></div>';

            $compile(htmlText)(scope, function(_element, _scope) {
                element.replaceWith(_element);                
            });

            scope.$watch('thisval',function(newVal,oldVal){
                $log.info('in *thisDirective* thisval changed...',
                          newVal, oldVal);
            });


        }, // end link
    } // end return

});

app.directive('childDirective', function($compile, $timeout, $log) {
    return {
        scope: {
            childval: '='
        },

        link: function(scope, element, attrs) {
            var htmlText = '<input type="text" ng-model="childval" />';

            $compile(htmlText)(scope, function(_element, _scope) {
                element.replaceWith(_element);                
            });

            scope.$watch('childval',function(newVal,oldVal){
                $log.info('in *childDirective* childval changed...',
                          newVal, oldVal);
            });

            // make believe change that gets called outside of angular
            setTimeout(function() {
                // need to wrap the setting of values in the scope 
                // inside of an $apply so that a digest cycle will be 
                // started and have all of the watches on the value called
                scope.$apply(function(){
                    scope.childval = "set outside of angular...";
                });
            },5000);

        },

    } // end return
});

Updated jsfiddle example: http://jsfiddle.net/CnDKN/3/