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.
Related
Trying to write a angular model that allows for a two way binding, I.E. where it has a ng-model variable, and if the controller updates it, it updates for the directive, if the directive updates it, it updates for the controller.
I have called the controller scope variable ng-model, and the directive model bindModel. In this case it sends an array of data, but that should not make a difference to how the binding works (I think at least).
So first the mapping, this is how the initial directive looks (link comes later).
return {
link: link,
scope: { cvSkills: '=',
bindModel:'=ngModel'},
restrict: 'E'
}
My understanding is (and I am uncertain about the exact scope at this moment) the directive will be aware of cvSkills (called cvSkills internally, and used to provide intial data), and bindModel which should pick up whats in ng-model).
The call in html is:
<skilltree cv-skills="cvskills" ng-model="CV._skillSet"></skilltree>
So the variables aren't actually (quite) in the directive yet. But there is an awareness of them, so I created two watchers (ignoring the cvskills one for now)
function link(scope,element, attr){
//var selected = ["55c8a069cca746f65c9836a3"];
var selected = [];
scope.$watch('bindModel', function(bindModel){
if (bindModel.length >> 0) {
console.log("Setting " + bindModel[0].skill)
selected = bindModel;
}
})
scope.$watch('cvSkills', function(cvSkills) {
So once the scope.$watch sees an update to bindModel, it picks it up and stores it to selected (same happens separately with cvSkills). In cvskills I then do updates to selected. So it will add additional data to selected if the user clicks buttons. All works and nothing special. My question is now. How do I then update bindModel (or ngModel) when there are updates to selected so that the controllers scope picks it up. Basically, how do I get the updates to propagate to ng-model="CV._skillSet"
And is this the right way to do it. I.E. make the scope aware, than pick up changes with scope.$watch and manually update the variable, or is the a more "direct" way of doing it?
=================== Fix using ngModel ===================
As per the article, if you add require: "ngModel" you get a fourth option to the function (and skip having a binding between ngModel and bindModel).
return {
link: link,
require: 'ngModel',
scope: { cvSkills: '='},
restrict: 'E'
}
Once you have done that, ngModel.viewValue will contain the data from ngModel= , in my case I am updating this in the code (which may be a bad idea). then call ngModel.setViewValue. It is probably safeish if you set the variable and then store it straight away (as follows)
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$viewValue );
} ;
....
ngModel.$viewValue.push(newValue);
ngModel.$setViewValue(ngModel.$viewValue)
================= If you don't care about viewValue ==================
You can use modelValue instead of viewValue, and any updates to modelValue is propagated straight through.
function link(scope,element, attr, ngModel){
ngModel.$render = function() {
console.log("ngRender got called " + ngModel.$modelValue );
} ;
....
ngModel.$modelValue.push(newValue);
Using the require attribute of the directive you can have the controller API from ngModel directive. So you can update values.
Take a look at this very simple demo here : https://blog.hadrien.eu/2015/01/16/transformation-avec-ngmodel/
For more information here is the documentation : https://docs.angularjs.org/#!/api/ng/type/ngModel.NgModelController
I am seeing inconsistent behavior with a directive when I $compile the element that contains the directive. In my case I have a directive that validates whether a password matches another password field. That directive looks like this:
app.directive('passwordMatches', function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
otherPasswordFieldValue: '=passwordMatches'
},
link: function (scope, elem, attrs, ngModelController) {
function validate(value) {
return value === scope.otherPasswordFieldValue;
}
//For DOM -> model validation
ngModelController.$parsers.unshift(function (value) {
var valid = validate(value);
ngModelController.$setValidity('password-matches', valid);
return valid ? value : undefined;
});
//For model -> DOM validation
ngModelController.$formatters.unshift(function (value) {
ngModelController.$setValidity('password-matches', validate(value));
return value;
});
scope.$watch(function() { return scope.otherPasswordFieldValue }, function () {
var valid = validate(ngModelController.$viewValue);
ngModelController.$setValidity('password-matches', valid);
});
}
};
});
This works fine alone. But I have another directive that is often used on the same element. The details of that directive aren't important because I've shown that the root cause of the issue is that that second directive compiles the element. As soon as I add this directive, the behavior changes. Without compiling the element, my passwordMatches directive works fine (the field becomes invalid if what I type doesn't match the other field and I can type whatever I want).
But as soon as I compile the element, I can type what I want until I make the fields match and it behaves normally up until that point. But once the values in the two fields match, if I type anything to make them not match, the field is completely blanked out. The easiest way to see this is in this jsbin: http://jsbin.com/IkuMECEf/12/edit. To reproduce, type "foo" in the first field and then try to type "fooo" (three o's) in the second field. As soon as you type the third "o" the field is blanked out. If you comment out the $compile, it works fine.
Thanks!
The second directive is compiling dom elements that have already been compiled by Angular. This second compile adds a second $watch, parser, etc because all the directive's linking functions are called again (here's a good detailed look at $compile) To confirm this you can put a console.log inside the $watch and you'll see that (with the second directive) it fires twice for every change- because of the duplicate $watch (remove the second directive and it fires only once- as expected). This second compilation step is not only causing the issue you're seeing but could cause other problems down the line.
If you have to recompile an Angular element then you first need to remove the existing one.
Here's one approach to this (explanation in the comments):
compile: function(compileElement) {
compileElement.removeAttr('another-directive');
return function(scope, element) {
// Create an "uncompiled" element using a copy of the current element's html
newe = angular.element(element.html());
// Remember where we were
parent= element.parent();
// Deleting the current "compiled" element
element.remove();
// Add the uncompiled copy
parent.append(news);
// Compile the copy
$compile(newe)(scope);
};
updated punker
Is there a way to interpret the content of a div having my directive as an attribute :
example :
<div my-directive>{{1+1}}</div>
and my link function looks like this :
link = (scope, element, attrs) =>
interpretedString = element.text()
in this example interpretedString is equal to {{1+1}} instead of 2
any help? the scope.$eval evaluates attributes, but what if I want to evaluate the text of a directive?
thanks
In order to interpret the string, you need to interpolate it thanks to the angular's $interpolate service.
Example:
link = (scope, element, attrs) =>
interpretedString = $interpolate(element.text())(scope)
Just to back up a bit, the contents of an element could be anything -- an array of element trees, text nodes, comments, and (unless you declare your directive "terminal") it all gets evaluated recursively and could have directives inside directive. I think you'd be much better off passing an interpolated string as an attribute. I mean you could do it this way, but whoever's using your widget wouldn't really expect that.
Something like:
<div my-directive my-attr="{{1+1}}"></div>
Or even (if that attribute is "primary"):
<div my-directive="{{1+1}}"></div>
From there, instead of $interpolate(element.text())(scope) you'd have $interpolate(attrs.myDirective)(scope)
Of course, the nature of Angular is everything is dynamic and can update all the time. What if the expression didn't just have constants, as a real expression likely wouldn't. If it were:
{{1+foo}}
Then the question is do you care about it changing? If not, $interpolate is fine. It captures the initial value. If you do care about it updating, you should use $observe.
attrs.$observe('myDirective', function(val) {
// if say "foo" is 5, val would be 6 here
});
Then later if you're like scope.foo = 8 the callback runs and passes 9.
Alternative is to do
<my-directive ng-bind="1+1" />
var two = $scope.$eval('ngBind');
:-p
.directive('myDirective', function () {
return {
restrict: 'E',
scope: {
ngBind : '='
},
link: function (scope, element, attrs) {
console.log('expecting string "1+1": '+attrs.ngBind);
console.log('expecting evaluated string "2": '+scope.$eval('ngBind'));
}
};
});
side note: <div ng-bind="val"></div> is almost the same as <div>{{val}}</div>. Except that u save 4 brackets and never see them flashing into view when things might go slower whatsoever reason.
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/
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){ ... }).