6

Using AngularJS. I have a directive that I want to have two way data binding. The directive will have an attribute called "activate". Initially, the value of "activate" will be "1".

The directive's link function will check if "activate" is equal to "1". If so, it will change "activate" to 0, but do some other stuff.

Later, if I want the directive to do some stuff again, in the controller, I will change "activate" to "1" again. Since the directive has the watch, it will repeat the cycle.

Unfortunately, every time I do this, I get "Expression '0' used with directive 'testDirective' is non-assignable!" or "Non-assignable model expression: 1 (directive: testDirective)".

Here is HTML:

<body ng-app="app">
    <test-directive activate="1"></test-directive>    
</body>

Here is JS :

var app = angular.module('app', []);


app.directive('testDirective', function() {
    return {
        restrict: 'E',
        scope: {
            activate : '='
        },


        link: function( scope, elem, attrs, controller ) {
            var el = elem[0];

            var updateContent = function() {
                el.innerText = 'Activate=' + scope.activate;
            };

            updateContent();
            attrs.$observe( 'activate', function() {
                console.log('Activate=' + scope.activate);
                if( scope.activate == '1') {
                    scope.activate = '0'
                    updateContent();
                }
            });
        }
    }
});      

Here it is on jsFiddle : http://jsfiddle.net/justbn/mgSpY/3/

Why can't I change the value stored in the directive's attribute? I'm using 2 way binding.

The docs say " If the parent scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception."

NOTE: The update content properly shows the value of "activate". However, the value of "activate" in "" does not update.

However that makes no sense to me as the parent scope property DOES exist.

Any ideas?

Justin Noel
  • 5,945
  • 10
  • 44
  • 59

2 Answers2

12

Although I agree with the use of $watch instead of attrs.$observe that's not the main reason for the error message you are getting.

The problem is that you are trying to assign a value to a non-assignable expression - as the error message tells you : Non-assignable model expression: 1 (directive: testDirective)

The non-assignable expression in this case is the number "1"

<test-directive activate="1">

You manage to pass the initial value (1) to the directive, but when the directive tries to update that value the attribute activate can't be changed - because it's a number.

So what you need to do is to change that to a variable, so you can update the value later.

See the code below where I initialise a variable in the $scope called activate.initialValue via a controller.

And I have also used $watch instead of attrs.$observe.

I've added the $timeout just to simulate an event to change the activate value after 2 seconds.

HTML

<body ng-app="app" ng-controller="appCtrl">
    <test-directive activate="activate.initialValue"></test-directive>    
</body>

JS

var app = angular.module('app', []);

app.controller('appCtrl', function($scope){
    $scope.activate = {
        initialValue : 1
    }
});

var app = angular.module('app', []);

app.controller('appCtrl', function($scope){
    $scope.activate = {
        initialValue : 2
    }
});

app.directive('testDirective', function($timeout) {
    return {
        restrict: 'E',
        scope: {
            activate : '='
        },      
        link: function(scope, elem, attrs) {
            scope.$watch('activate', function(newValue, oldValue){
               console.log('activate has changed', newValue);
            });

             $timeout(function(){ 
                    scope.activate = 0;     
                }, 2000);
        },
        template: "{{activate}}"
    }
});   

You can also see working here (http://jsfiddle.net/mgSpY/63/).

And here it is AngularJS official documentation about it (http://docs.angularjs.org/error/ngModel:nonassign)

starcorn
  • 8,261
  • 23
  • 83
  • 124
Denison Luz
  • 3,575
  • 23
  • 25
3

To monitor a property of the scope, you should use scope.$watch instead of attrs.$observe:

link: function(scope, elem, attrs, controller) {
  var el = elem[0];

  var updateContent = function() {
    el.innerText = 'Activate=' + scope.activate;
  };

  scope.$watch('activate', function(value) {                               
    console.log('Activate=', value);
    if(value === 1) {
      scope.activate = '0'                    
    }
    updateContent();
  });
}

jsFiddle here.

Notice that I've removed the updateContent call from the link function, because you can only safely access a property of the scope inside a $watch callback, unless you can ensure that the value bound to that property is available before the directive is processed by Angular.

$observeshould only be used to observe/watch the value changes of a DOM attribute that contains interpolation (for instance, value="{{ value }}"). Check out this SO question to understand it better.

Community
  • 1
  • 1
Michael Benford
  • 14,044
  • 3
  • 60
  • 60
  • Michael : Thanks so much for the answer. It definitely works properly now. I don't exactly understand how using 'scope.activate=0' works now but did not before. I'm still changing the value of the same attribute. – Justin Noel Sep 06 '13 at 23:15
  • I'll update the answer with more information. Hopefully it'll clear things up. – Michael Benford Sep 06 '13 at 23:35