Justin Justin - 1 month ago 5
AngularJS Question

Change Attribute From within Directive

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?

Answer

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)