gerrod
gerrod

Reputation: 6627

Update ngModel on a directive, from a directive

I'm having trouble understanding why changes to an ngModel doesn't propagate from one directive to another. Here's a plunker that shows a simplified version of what we're trying to do.

Basically, I've declared a directive which uses ngModel, and an isolate scope:

.directive('echo', function() {
    var link = function(scope, element, attrs, ngModel) {
        // --- 8< ---
        scope.$watch(attrs['ngModel'], function() {
            scope.model = ngModel.$modelValue;
            console.log("***** Echo model updated: ", scope.model);
        });
    };

    return {
        restrict: 'E',
        require: 'ngModel',
        link: link,
        scope: {
            id: "="
        }
    }
})

That directive is then wrapped by another directive, also with a dependency on ngModel and with an isolate scope:

.directive('wrapper', function() {
    var link = function(scope, element, attrs, ngModel) {
        scope.$watch(attrs['ngModel'], function() {
            var model = ngModel.$modelValue;
            console.log("----- Wrapper model updated", model);

            scope.model = model;
        })
    };

    return {
        restrict: 'E',
        require: 'ngModel',
        link: link,
        scope: {
        },

        template: "<div><h2>Echo:</h2> <echo id='myEcho' ng-model='model'></echo></div><div><h2>Model text:</h2>{{ model.text }}</div>"
    }
})

You can see that the "wrapper" directive requires an ngModel, as does the echo directive.

When I use the "wrapper" directive in my HTML, and then I push a value into the ngModel, the "wrapper" directive correctly identifies that the model has changed (the console.log shows this). At that point, the wrapper directive then updates the model on its scope, which I would have expected would propagate that model update into the "echo" directive.

However, watching the console, the "echo" directive never sees the model get updated.

Q: Why doesn't the "echo" directive see the updated model from the "wrapper" directive?

Note: This may be made slightly more complex by the fact that the "echo" is not only consumed from the "wrapper" directive - sometimes it is consumed directly.

Upvotes: 0

Views: 348

Answers (1)

Sly_cardinal
Sly_cardinal

Reputation: 12993

Updated answer:

No, the issue does not relate to timing - watches will still fire whether they have been added before or after the watched value has been set.

I would recommend putting some breakpoints in your echo directive and stepping through to see how the watchers are being set.

Here is an updated plunker that is working: http://plnkr.co/edit/bbv2vpZ7KaDiblVcoaNX?p=preview

.directive('echo', function() {
    var link = function(scope, element, attrs, ngModel) {
        console.log("***** Linking echo");

        var render = function (val) {
            var htmlText = val || 'n/t';
            element.html(htmlText);
        };
        scope.$watch("model.text", render);
    };

    return {
        restrict: 'E',
        link: link,
        scope: {
            id: "=",
            model: '=echoModel'
        }
    }
})

.directive('wrapper', function() {
    var link = function(scope, element, attrs, ngModel) {
        console.log("---- Linking Wrapper");
    };

    return {
        restrict: 'E',
        require: 'ngModel',
        link: link,
        scope: {
          wrapperModel: '=ngModel'
        },

        template: "<div><h2>Echo:</h2> <echo id='myEcho' echo-model='wrapperModel'></echo></div><div><h2>Model text:</h2>{{ wrapperModel.text }}</div>"
    }
})

The reason it's not working is because of the way the attrs and watchers work which might be a little unexpected.

Basically you're trying to watch the scope.model property on your scope, not the evaluated value of the ngModel attribute as you might expect:

.directive('echo', function() {
    var link = function(scope, element, attrs, ngModel) {
        // Your HTML is `<echo ng-model='model'></echo>`
        // which means this `scopePropertyToWatch` will have the value 'model'.
        var scopePropertyToWatch = attrs['ngModel'];

        // This means it will try to watch the value
        // of `scope.model`, which isn't right because
        // it hasn't been set.
        scope.$watch(scopePropertyToWatch, function() {
            scope.model = ngModel.$modelValue;
            console.log("***** Echo model updated: ", scope.model);
        });
    };

    // ...
})

There are two simple solutions.

1. Setup two-way binding on the ngModel attribute:

.directive('echo', function() {
    var link = function(scope, element, attrs, ngModelCtrl) {
        // Watch the `scope.ngModel` property on the scope.
        // NOT the attr['ngModel'] value which will still
        // be 'model'.
        scope.$watch('ngModel', function() {
            scope.model = ngModelCtrl.$modelValue;
            console.log("***** Echo model updated: ", scope.model);
        });
    };

    return {
        restrict: 'E',
        require: 'ngModel',
        link: link,
        scope: {
            id: "=",
            ngModel: "=" // This will make the `ngModel` property available on the scope.
        }
    }
});

It gets a bit complex using ngModel the way you are - I'd recommend having a look at this video on how to use ngModel in custom components: Jason Aden - Using ngModelController to Make Sexy Custom Components

2. Watch the property on the $parent scope:

.directive('echo', function() {
    var link = function(scope, element, attrs, ngModelCtrl) {
        // Add a watch on the **parent** scope for the attribute value.
        // NOTE that we use the attrs['ngModel'] value because the property
        // on the parent scope **is**: `scope.$parent.model`
        scope.$parent.$watch(attrs['ngModel'], function() {
            scope.model = ngModelCtrl.$modelValue;
            console.log("***** Echo model updated: ", scope.model);
        });
    };

    return {
        restrict: 'E',
        require: 'ngModel',
        link: link,
        scope: {
            id: "="
        }
    }
});

Again, it can get a little complex using ngModelCtrl.$modelValue but at least that will get your watchers firing.

Upvotes: 2

Related Questions