Angular interpret {{}} inside a link function of a custom directive - angularjs

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.

Related

Altering innerText in an Angular directive

This seems to me like it should be straight-forward, but I think I'm misunderstanding the order of operations here. The documentation is a bit tough for me to digest and the answers I've found here have led me closer to an answer but not quite far enough.
I'm trying to place a scope variable (containing a string) onto the DOM using a directive and I want to manipulate that text and eventually create a "Read More" text truncation function.
HTML snippet:
<read-more>{{ commentary }}</read-more>
Angular/Coffeescript:
app.directive('readMore', [ ->
restrict: 'E'
scope: false
link: (scope, element, attrs) ->
console.log(element[0])
element[0].innerText = element[0].innerText.substring(0, 125) + "..."
])
The text from that variable gets read into the DOM, and console logs the element as <read-more ng-class="ng-binding"> and the {{ commentary }} string is printed in the console between the tags, however, my function doesn't manipulate it.
I know it's the correct element (and index), but for some reason innerText and innerHTML don't affect what is on the DOM.
If I change the return line in the link function to:
element[0].innerText = "Foo"
I get nothing between the <read-more> tags in the console and, naturally, the DOM now has no content in there.
What am I missing about how link deals with this element on the DOM?
My understanding is that element you are dealing here is not the JS element you are dealing for example in a standard jQuery function. This is angularjs element, Instead of DOM manypulation, I would rather create a directive that sets the model of the "Read More" element, and also hides the entire text that needs to be displayed after click on it. But do that only via angularjs ng-model directive, not by DOM manipulation.
You doesn't need DOM for angular.
Here show you a few ways to bind content:
http://plnkr.co/edit/OSWIy0?p=preview
Use this
link: function($scope) {
$scope.otherText = 'Here more text. For example, this maybe come from http-stream.';
}
instead this:
element[0].innerText = element[0].innerText.substring(0, 125) + "..."
Good luck. :-)
Angular provides a wrapper for the element using jQuery (if available) or jQLite, so use the html or text function:
link: (scope, element, attrs) ->
commentString = element.html() ## alternatively use element.text()
Note, however, that both the text and html functions will return the unevaluated string {{ commentary }} rather than whatever string value the commentary variable holds.
To get around this, simply address commentary using the scope argument passed to the directive's link function.
link: (scope, element, attrs) ->
commentString = scope.commentary
... ## perform string manipulation here
element.text(newCommentString)
This will set the element text to whatever string you pass to it. As for updating the text: if something like "read more" is clicked, have an event handler ready (still inside of link:, like so:
element.on('click', ->
element.text(commentString) ## the full commentary string from above
)

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.

AngularJS: Compiling an element with another directive changes behavior

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

AngularJS : Why are ng-href/ng-src implemented by using attr.$observe instead of scope.$watch?

Having read AngularJS : Difference between the $observe and $watch methods, and the implementation of ng-href/ng-src:
link: function(scope, element, attr) {
attr.$observe(normalized, function(value) {
if (!value)
return;
attr.$set(attrName, value);
// ...
}
}
I wonder why ng-href/ng-src are implemented by using attr.$observe instead of scope.$watch. By using scope.$watch, it looks like
link: function(scope, element, attr) {
scope.$watch(attr[normalized], function(newValue) {
// ...
})
}
then in view we could write <img ng-href="expressionFoo"> instead of <img ng-href="{{ expressionFoo }}">.
The possible reasons I could figure out are
attr.$observe makes a directive to work more like a normal DOM attribute. After link, I could affect the directive by attr.$set('ngHref', ...) in another directive.
interpolate is higher level than expression. It's easier to write plain strings and multiple expressions by using interpolate.
ng-href and ng-src both result in string attributes, so it's safe and easier to use interpolate hence attr.$observe here.
Any idea?
I think the scope isn't what you think.
Option scope in directives is false by default, which means we get parent scope, that could be anything, really.
If we want to watch for element's attribute change, we have to watch the element's attribute "ng-blah". The scope is simply not related.
If we were to use scope.watch, we probably wouldn't even have a property named after that attribute, and nothing would work.

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