AngularJS: Assign to scope value although attribute not set on element - angularjs

In my directive I create an isolate scope and assign to the ngModel in code. Here's my isolate scope:
scope: {
ngModel: '=',
value: "=",
placeholder: "#"
}
Inside the link function I assign to scope.ngModel. That works fine if the ng-model attribute is set on the element, but when it's not it raises an error.
Error: Non-assignable model expression: undefined
What's the preferred way to check if the attribute exists? Do I have to do this explicitly with element.hasAttribute or am I doing it completely wrong?

If the ngModel-attribute is optional in your directive you have to check if it exist before you assign any value to the scope variable, or Angular will raise an error. I'd use if( attrs.ngModel ){...} or element.attrs('ngModel') to check if it's present.
With ngModel you also have the option of using the ngModelController in your directive link function. You do this by require:'^ngModel' (^ if it's optional) and the ngModelController will be available as the fourth argument in your link controller (link:function(scope,element,attrs,ngModelController){ ... }).

Related

ng-attr not evaluating on directive element

I am attempting to pass data from the controller to an isolated scope using element attributes. Here is my tag in the view:
<comment ng-attr-cid="{{question.id}}" ctype="questions"></div>
And here is the directive:
'use strict'
angular.module('arlo.directives').directive "comment", ['Comment', (Comment) ->
directive =
templateUrl: "angular/partials/comment.html"
restrict: "E"
scope:
cid: "="
ctype: "="
link: (scope, element, attrs) ->
scope.toggled = false
scope.comment = null
scope.comments
scope.toggle = ->
if scope.toggled is true then scope.toggled = false else scope.toggled = true
scope.comment = null
scope.addComment = ->
Comment.addComment(scope.ctype, scope.cid, scope.comment).then ->
scope.comments = Comments.commentsList
scope.toggled = false
scope.comment = null
scope.loadComments = ->
Comment.loadComments(scope.ctype, scope.cid).then ->
scope.comments = Comments.commentsList
scope.loadComments()
]
The problem is cid is getting assigned "{{question.id}}" instead of the value of value of question.id. I attempted to use ng-attr-cid="question.id" but that is not working either. ON top of that, ctype is evaluating as undefined.
If I add ng-attr-cid on any other element, it evaluates and added cid="" to the element.
Can someone please explain what I am missing?
In an isolated scope (what you get when you specify a scope object on a directive) you can import variables into the scope based on attributes of the original element.
In this case, there is no need to use ng-attr since our directive will handle grabbing the values.
"=" is for when you want to copy a variable, so you just provide the variable name, e.g. cid="question.id"
"#" is for when you want to interpolate a variable before passing it to your directive, e.g. cid="{{question.id}}". Also very handy for passing raw strings.
In short
drop the ng-attr
change the directive scope.cid to "#" OR use cid="question.id" in your HTML
check the value of questions (not sure if this was deliberately pluralised or not, since ctype is undefined in your directive, it means that questions is undefined as well.
Here is a plnkr showing the fix.
It's not entirely clear why you need the ng-attr prefix on the cid attribute, but if you do in fact need to do that then unfortunately your 'cid' isolate scope value is interfering with some implementation detail of how angular deals with ng-attr-*.
You can awkwardly work around that by using a link function in your directive which observes the 'cid' attribute that will be created by ng-attr-cid, and removing your existing cid: '=' property on your isolate scope definition.
... your existing directive definition ...
link: function link(scope, elem, attrs) {
attrs.$observe('cid', function(val) {
scope['cid'] = val;
});
}
... etc, etc ...
This sets up an observer on the cid attribute and updates the scope whenever it changes.
See plnkr.

scope.$watch not invoked by AngularJS directive

I have the following link function in an AngularJS directive:
link: function(scope, iElement, iAttrs) {
scope.$watch('name', function(newVal){
if(newVal){
console.log(newVal);
}
}, true);
}
The full fiddle is located here: http://jsfiddle.net/balteo/K4t7P/55/
I am trying to figure out why the $watch function is not invoked when a user changes the name variable in the textarea.
You are creating a new scope when you write this on your directive
scope: {
name: '='
}
Just remove it and all will work well
Fiddle
Explanation
About the scope attribute, in the docs, we read:
If set to {} (object hash), then a new "isolate" scope is created. The 'isolate' scope differs from normal scope in that it does not prototypically inherit from the parent scope. This is useful when creating reusable components, which should not accidentally read or modify data in the parent scope.
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value of the attr attribute. If no attr name is specified then the attribute name is assumed to be the same as the local name.
Note that for two-way data-binding, is expected that you pass your model as attribute.
When you do this (write your model as an attribute) it works like a charm (check this fiddle).
But you are passing your attribute via ng-model. It's already available on the scope of the directive. When you create a new scope, you are actually creating a child scope at your controllers scope and setting it to your scope parameter in the link function. In fact, if you watch the $scope.$parent.name it will work as well (check this fiddle).

AngularJs accessing attribute difference, via linking and scope

Can anyone explain whats the difference between accessing directive attribute from the scope as in :
scope: { someVar: '=' }
vs
link: function (scope, elem, attr, ctrl) {
attr.someVar
}
It seems somehow both accomplish the same thing.
From the AngularJS ng.$compile docs:
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value of the attr attribute... Any changes to parentModel will be reflected in localModel and any changes in localModel will reflect in parentModel.
In other words, this creates a watch on the attribute, causing changes to the model in either the parent scope or the directive scope to be reflected in the other.
Referencing the attribute merely returns a string. Thus if you had a directive foo and your HTML looked like
<foo some-var="bar"></foo>
In your link function, attr.someVar would be the string literal "bar". You can evaluate bar in the scope by calling scope.$eval(attr.someVar) (or scope.$parent.$eval(attr.someVar) if the directive has an isolate scope) at any time, but it is not done for you.

using ng-show in directive template

I am trying to define a directive to show a checkmark when a question has been answered.
questModule.directive('showCheckmarkOnDone', function () {
return {
transclude: true,
template: "<div ng-transclude></div><img src='img/checkmark.svg' " +
"ng-show='scope.questions[type][number-1].done' " +
"class='checkmark' style='height: {{height}}px; width: {{height}}px;'>",
link: function (scope, element, attrs) {
scope.height = $(".app").height()/12;
scope.number = attrs.number;
scope.type = attrs.type;
}
};
});
The scope.questions[type][number-1].done exists in my controller for the page, and I can see that it is being updated correctly when I press the done button. However, the directive does not register the change. I tried putting a $watch in the link function - that didn't help either. I think I'm a bit confused about how to get my directive scope to play nicely with my controller scope - any thoughts on how I can give this directive access to an object that exists in an outside controller? (scope.questions)
This is not a valid way to define directive scope:
scope: '#'
You can either (a) not define it, (b) set it to true, or (c) set it to {} (an object). See the docs for more info: http://docs.angularjs.org/guide/directive (find the header: "Directive Definition Object")
In this case, I imagine if you remove it, you may be OK, because it will allow scope.questions to be visible from your directive. If you reproduce the issue in jsfiddle.net or plnkr.co, it would be much easier to assist you.
Edit:
Your directive generally should have 1 parent element
You should not use scope in your directive's HTML, it's implied
I think you said as much in your comment, but you should strive to make your directive more generic by passing in scope.questions[0][0].done instead of looking it up in your directive's HTML using attributes.
Here's a working example: http://jsfiddle.net/EZy2F/1/

Directive scope attributes break depending on attribute name

I have a very strange phenomenon with a directive and an isolated scope, where the attributes in the scope work or do not work depending on the naming of the attribute. If I use
{check:'#check'}
it works just fine and as expected. However,if I use:
{checkN:'#checkN'}
the defined function never gets assigned. An example would look like:
HTML:
<item ng-repeat="list_item in model.list" model="list_item" checkN="checkName()" check="checkName()" position="$index"></item>'
Javascript
app.directive('item', function(){
return {
restrict: 'E',
replace : false,
scope:{
$index: '=position',
check: '&check',
checkN: '&checkN',
model:'='
},
template: '',
link: function(scope, element, attrs){
console.log(scope.check())
console.log(scope.checkN())
}
}
});
The console will then give me the following:
The checkName function has been called [which is the return string of the function]
undefined
It is really possible that it depends on the usage of capital letters? This would be very "unexpected" behaviour.
Thanks for your help
schacki
Html is case insensitive, therefore myAttribute and myattribute would be indistinguishable from each other depending on the browser. Angularjs' authors made a design decision about passing from html to javascript and vice-versa in terms of directives.
ngRepeat directive would be used as ng-repeat in the view(html).
Likewise, your directive checkN should be used as check-n for angular to recognise that as directive.

Resources