Why is ngModel.$setViewValue(...) not working from

52,800

Solution 1

The reason is that since you're creating an isolated scope for your contenteditable directive, the ng-model directive on the same element gets that isolated scope as well. Which means that you have two different scopes that aren't connected to each other, which both have a form.userContent property that changes separately. I guess you could exemplify it by this code:

<!doctype html>
<html ng-app="myApp">
<head>
    <script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
    <script src="http://code.angularjs.org/1.0.5/angular.min.js"></script>
    <script>
    angular.module('myApp', []).controller('Ctrl', function($scope) {

    })
    .directive('contenteditable', function() {
        return {
            restrict : 'A', // only activate on element attribute
            require : '?ngModel', // get a hold of NgModelController
            scope: {},
            link : function(scope, element, attrs, ngModel) {
                if (!ngModel)
                    return; // do nothing if no ng-model

                setInterval(function() {
                    if (angular.element('#contenteditable').scope().form)
                        console.log(angular.element('#contenteditable').scope().form.userContent);

                    if (angular.element('#textarea').scope().form)
                        console.log(angular.element('#textarea').scope().form.userContent);
                }, 1000);

                // Specify how UI should be updated
                ngModel.$render = function() {
                    element.html(ngModel.$viewValue || '');
                };

                // Listen for change events to enable binding
                element.bind('blur keyup change', function() {
                            scope.$apply(read);
                        });
                read(); // initialize

                // Write data to the model
                function read() {
                    ngModel.$setViewValue(element.html());
                }
            }
        };
    });
    </script>
</head>
<body ng-controller="Ctrl">
    <form name="myForm">
        <div ng-init="form.userContent"></div>
        <div contenteditable name="myWidget" ng-model="form.userContent" id="contenteditable" required>Change me!</div>
        <span ng-show="myForm.myWidget.$error.required">Required!</span>
        <hr />
        <textarea ng-model="form.userContent" id="textarea"></textarea>
    </form>
</body>
</html>

As you'll see in your console, there are two different scopes and form.userContent on them change separately if you change the text in the textarea or if you change the text in your contenteditable div.

So I bet you're thinking "enough with the explaining and show me a solution!". Well, there aren't (to my knowledge) a pretty solution for this, but there is one that works. What you want to do is bring a reference of the model into your isolated scope, and make sure that it has the same name in your isolated scope as in the parent scope.

Here's what you do, instead of creating an empty scope like this:

...
scope: {}
...

You bind the model like this:

...
scope: {
    model: '=ngModel'
}
....

Now you have a model property on your isolated scope that is a reference to form.userContent on your parent scope. But ng-model isn't looking for a model property, it's looking for a form.userProperty which still doesn't exist in our isolated scope. So to fix this, we add this inside our linking function:

scope.$watch('model', function() {
    scope.$eval(attrs.ngModel + ' = model');
});

scope.$watch(attrs.ngModel, function(val) {
    scope.model = val;
});

The first watch syncs changes on form.userContent that comes from outside of our directive to our isolated form.userContent, and the second watch makes sure that we propagate any changes on our isolated form.userContent up to the parent scope.

I realize that this is a lengthy answer, and perhaps not very easy to follow. So I'd be happy to clearify anything that you feel is blurry.

Solution 2

the first answer explains the problem well, I believe i have a simpler solution that avoids extra watches.

to summarize answer 1. ngModel can not work inside the isolate scope because the elements you intended it to bind to are not in its scope. they are in the parent scope.

solution 1, bind to the parent's property

<div contenteditable name="myWidget" ng-model="form.userContent" required>Change me!</div>

becomes

<div contenteditable name="myWidget" ng-model="$parent.form.userContent" required>Change me!</div>

solution 2, move ngModel outside the isolate scope

require : '?ngModel', becomes require : '?^ngModel', the ^ tells your directive to look in parent elements for ngModel

<div contenteditable name="myWidget" ng-model="form.userContent" required>Change me!</div>

becomes

<div ng-model="form.userContent">
    <div contenteditable name="myWidget" required>Change me!</div>
</div>
Share:
52,800
Arun P Johny
Author by

Arun P Johny

LinkedIn

Updated on July 03, 2020

Comments

  • Arun P Johny
    Arun P Johny about 4 years

    I'm writing an directive which needs an isolated scope, but I want to bind it to the parent scope via ngModel.

    Here the problem is that the parent's scope value is not getting changed.

    Markup

    <form name="myForm" ng-app="customControl">
        <div ng-init="form.userContent"></div>
        <div contenteditable name="myWidget" ng-model="form.userContent" required>Change me!</div>
        <span ng-show="myForm.myWidget.$error.required">Required!</span>
        <hr />
        <textarea ng-model="form.userContent"></textarea>
    </form>
    

    JS

    angular.module('customControl', []).directive('contenteditable', function() {
        return {
            restrict : 'A', // only activate on element attribute
            require : '?ngModel', // get a hold of NgModelController
            scope: {},
            link : function(scope, element, attrs, ngModel) {
                if (!ngModel)
                    return; // do nothing if no ng-model
    
                // Specify how UI should be updated
                ngModel.$render = function() {
                    element.html(ngModel.$viewValue || '');
                };
    
                // Listen for change events to enable binding
                element.bind('blur keyup change', function() {
                            scope.$apply(read);
                        });
                read(); // initialize
    
                // Write data to the model
                function read() {
                    ngModel.$setViewValue(element.html());
                }
            }
        };
    });
    

    Demo: Fiddle.

    This works fine if I don't use a isolated scope for the directive

    Demo: Fiddle.

  • Behrang
    Behrang over 10 years
    When you say "[...] the ng-model directive on the same element gets that isolated scope as well. Which means that you have two different scopes that aren't connected to each other", if the same isolated scope is shared by both ng-model and contenteditable, how come then you say they have two different scopes? It looks like a contradiction. Or am I missing something?
  • geckob
    geckob over 9 years
    @anders I am not clear how the isolate scope break the code. Can you please clarify?
  • Mukund Kumar
    Mukund Kumar over 9 years
    @ Anders Ekdahl If multiple directives on an element provide an isolate scope, only one new scope is applied.(reference Ng-Book ,page 111 ,Scope Option Heading)
  • Benny Bottema
    Benny Bottema almost 8 years
    I found this answer to be to the point and most helpful. You might want to complete the answer with the dual-$watch approach as it is a legitimate (and widely applied) solution.