Obtaining element attributes within a custom directive used within ng-repeat - angularjs

This is driving me a little bit crazy. I need to read the src of an image within a custom attribute:
app.directive('imgTransform', function () {
return {
retrict: 'A',
link: function (scope, elem, attrs) {
console.log(elem.attr('ng-src'));
}
}
});
This works fine when used like so:
<img ng-src='http://lorempixel.com/100/100/technics' alt="" img-transform="" />
However, it does not work inside ng-repeat:
<p ng-repeat='image in images'>
<img ng-src='{{image}}' alt="" img-transform="" />
</p>
The value returned is {{image}}. How do I get the actual value?

Try using the attrs:
console.log(attrs.ngSrc);
Fiddle: http://jsfiddle.net/6SuWD/
The reason for this could be that ng-repeat uses the original DOM as template and recreates it for each iteration. For some (obscure to me) reason, you are reading the attribute of the template. This explanation could be very wrong though...
However, since Angular gives you the API to access the attributes, it would be safer to go with it anyway.

You will have to watch for changes in this attribute using $observe since ng-repeat interpolates the values of ng-src. See this reference.

LukaszBachman is correct. When you pass interpolated values to a directive, the interpolation hasn't fired yet when the directive is in its linking phase.
If you were to do console.log(attrs); - you would clearly see that there is an actual value on ngSrc, when looking in the browser console. However, since interpolation hasn't kicked in yet you dont have access to it.
This would get you the actual value of ngSrc:
myApp.directive('imgTransform', function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
attrs.$observe('ngSrc', function (val) {
console.log(val);
});
}
}
});

Related

Angularjs Interpolation using double curly braces not working under ng-if

UPDATE1: developed the plunker sample that will reproduce the problem. See below.
I have a strange problem in my project, where it appears in one place only. Finally, I was able to reproduce the problem using plunker sample:
http://plnkr.co/edit/JJbq54?p=preview
In the above sample, see the section "With ng-if" and "Without ng-if", enter something in the input text, and see how the double curly braces not working under ng-if, but ng-bind works fine. Also, if you remove check-if-required from the template sites-and-improvements.html also the problem is solved.
More details below:
I have the the following HTML5 code block:
<div ng-if="isFullCUSPAP" id="sites_and_imrpovements_comments">
<div class="form-row">
<div class="inputs-group">
<label>WIND TURBINE:</label>
<div class="input-binary">
<label>
<input type="radio" id="wind_turbine"
name="wind_turbine"
ng-model="$parent.wind_turbine"
value="Yes" force-model-update />
Yes
</label>
</div>
<div class="input-binary">
<label>
<input type="radio" id="wind_turbine"
name="wind_turbine"
ng-model="$parent.wind_turbine"
value="No" force-model-update />
No
</label>
</div>
<span ng-bind="wind_turbine"></span>
<span>wind_turbine = {{wind_turbine}}</span>
</div>
</div>
</div>
I know that ng-if will create a new child scope. See above code, scope variable wind_trubine. Only in this HTML5 file, the curly braces {{}} is not working. However, if I use ng-bind it works fine. In other HTML5 files, I have no problem what so ever. This HTML5 is implemented using directive as follows:
app.directive('sitesAndImprovements', function() {
return {
restrict: 'E',
replace:true,
templateUrl: '<path-to-file>/site-and-improvments.html',
link: function (scope, elem, attrs) {
//Business Logic for Sites and Improvements
}
}
})
And, simply, I put it in the parent as follows:
<sites-and-improvements></sites-and-improvements>
The only difference I could see, is that this implementation has two levels of nested ng-if, which would look like the following:
<div ng-if="some_expression">
...
...
<sites-and-improvements></sites-and-improvements>
...
...
</div>
Based on comments, I used controller As notation and defined MainController accordingly. See snapshots below. It seems there is a problem if ng-if is nested with two levels. The scope variable is completely confused. I don't get the same results using ng-bind and double curly braces.
If you examine the above snapshots, even though I used controller As notation, you will see that ng-bind gives different results when compared with interpolation using {{}}.
I even changed the default value of wind_turbine to be set as follows in the link function:
scope.MainController.wind_turbine = 'Yes';
I noticed that on page load, everything looks fine, but when I change the value of the input element wind_trubine using the mouse, all related reference are updated correctly except the one that uses {{}}.
Maybe this is because there are two nested levels of ng-if?
Appreciate your feedback.
Tarek
Remove the replace: true from the sites-and-improvements directive:
app.directive('sitesAndImprovements', function() {
return {
restrict: 'E',
̶r̶e̶p̶l̶a̶c̶e̶:̶t̶r̶u̶e̶,̶
templateUrl: 'site-and-improvments.html',
link: function (scope, elem, attrs) {
//debugger;
}
}
})
It is fighting the check-if-required directive:
app.directive('checkIfRequired', ['$compile', '$timeout', function ($compile, $timeout) {
return {
priority: 2000,
terminal: true,
link: function (scope, el, attrs) {
el.removeAttr('check-if-required');
$timeout(function(){
//debugger;
$(':input', el).each(function(key, child) {
if (child && child.id === 'test_me') {
angular.element(child).attr('ng-required', 'true');
}
if (child && child.id === 'testInput1') {
//debugger;
//angular.element(child).attr('ng-required', 'true');
}
});
$compile(el, null, 2000)(scope);
})
}
};
}])
The DEMO on PLNKR.
replace:true is Deprecated
From the Docs:
replace ([DEPRECATED!], will be removed in next major release - i.e. v2.0)
specify what the template should replace. Defaults to false.
true - the template will replace the directive's element.
false - the template will replace the contents of the directive's element.
-- AngularJS Comprehensive Directive API - replace deprecated
From GitHub:
Caitp-- It's deprecated because there are known, very silly problems with replace: true, a number of which can't really be fixed in a reasonable fashion. If you're careful and avoid these problems, then more power to you, but for the benefit of new users, it's easier to just tell them "this will give you a headache, don't do it".
-- AngularJS Issue #7636
For more information, see Explain replace=true in Angular Directives (Deprecated)
Another solution posted by AngularJS team here:
https://github.com/angular/angular.js/issues/16140#issuecomment-319332063
Basically, they recommend to convert the link() function to use compile() function instead. Here is the update code:
app.directive('checkIfRequired', ['$compile', '$timeout', function ($compile, $timeout) {
return {
priority: 2000,
terminal: true,
compile: function (el, attrs) {
el.removeAttr('check-if-required');
var children = $(':input', el);
children.each(function(key, child) {
if (child && child.id === 'test_me') {
angular.element(child).attr('ng-required', 'true');
}
});
var compiled = $compile(el, null, 2000);
return function( scope ) {
compiled( scope );
};
}
};
}]).directive('sitesAndImprovements', function() {
return {
restrict: 'E',
replace:true,
templateUrl: 'site-and-improvments.html'
}
});
The main problem I have with this solution is that I am using the scope parameter which is passed to the link() function. For example, in the .each() loop above, I need to get the value of the element ID which is based on interpolation using {{<angular expre>}}.
So I tried to use pre-link and post-link within the compile function where the scope is available. I noticed that the section with ng-if is removed when execution is in pre-link and then it is added shortly after that. So I had to use $watch to monitor changes to the children to run the needed process when required. I developed this plunker sample:
http://plnkr.co/edit/lsJvhr?p=preview
Even after all such effort, the issue is not resolved. So the bottom line for similar cases, is that if you need to use the scope then you have to remove replace: true.
Any feedback would be appreciated.
Tarek

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

Get string value from AngularJS directive tag: my-first-directive="I want this"

I am wanting to pass a string value to my AngularJS directive without using a separate attribute, something like this...
In my HTML
<div my-first-directive="number 1"></div>
<div my-first-directive="number 2"></div>
<div my-first-directive="number 3"></div>
and in my JavaScript
.directive('myFirstDirective', function () {
'use strict';
return {
restrict: 'A',
link: function (scope, element) {
// now I want the string that follows the directive
console.log(element[0].attributes[0].nodeValue);
console.log(element[0].attributes[0].textContent);
console.log(element[0].attributes[0].value);
}
};
});
Now all the three console.log methods output the string I require... however I am unsure this isn't the best way to obtain such a value, don't I need to think about isolate scope and the like? I don't require "2 way binding" or anything. Is there a better or AngularJS way of obtaining the string?
Many thanks in advance
link() takes an attrs argument, you will find what you want there:
link: function (scope, element, attrs) {
// what you want is:
console.log(attrs.myFirstDirective);
}
The attribute names are normalized.

AngularJS: Retrieve Element Name from Directive

I have spent some time looking for this but I haven't found anything.
I have the following
HTML file:
<my-directive name="someName" id="someId" method="somemethod">
sometext
</my-directive>
My directive:
app.directive('myDirective', function() {
return {
restrict: 'EA',
templateUrl: "example.html",
transclude: true,
link: function(scope, element, attrs)
{
alert(element.name); //Used for testing, Not working
}
};
});
I am trying to access the element parameters in the directive (name, method, id) but I am unable to figure out how.
Thanks in advance.
Please have a look at this Plnkr
You have the attrs as parameter inside the link function. Use that instead of the element.
link: function(scope, element, attrs) {
scope.result = attrs['name'];
}
You are also using transclusion, but you haven't defined a "ng-transclude" attribute in the template.
Using an alert for testing is very bad practice. You should be writing an assertion that specifically looks for the attribute you want (name in this case) and verifying that it is what you expect it to be. As commenter doodeec above said, you'll find the value you need under attrs.name. References to the element may also need to be element[0] to ensure that you do not get an undefined or null value. Lastly, you have your directive binding to both element and attribute, which seems to be a less than optimal situation. Were I you, I would bind to one or the other, but not both. It'll make for cleaner code in both places and remove some spaghetti.

AngularJs directives - how to get attributes values from within directive

Any idea how to access attributes values from inside the directive?
angular.module('portal.directives', [])
.directive('languageFlag', ['$routeParams', function(params) {
return function(scope, element, attrs) {
console.log(attrs["data-key"]); // returns undefined
console.log(element.attr('data-key')); // returns {{data-key}}
angular.element(element).css('border', '1px solid red');
};
}]);
Html code is:
<ul>
<li ng-repeat="lng in flags">
<a class="lngFlag {{flag.Key}}" data-key="{{flag.Key}}" data-id="{{lng.Id}}" ng-click="changeLangHash({'lng':lng.Id })" language-flag></a>
</li>
</ul>
Thanks
Use $observe:
Observing interpolated attributes: Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined. -- directives doc
return function(scope, element, attrs) {
attrs.$observe('key', function(value) {
console.log('key=', value);
});
}
As #FMM mentioned in a comment, data is stripped by Angular when it normalizes the attribute name, hence the use of key above, rather than dataKey.
try attrs["dataKey"] - this is the way that html parses attributes with dash (-).
if you want the value from the scope instead of {{something}}, you can do two things:
scope[attrs['dataKey']] - will work but shouldn't do this
or use $parse but then don't use ``{{}}`
app.directive('languageFlag', ['$routeParams','$parse', function(params,$parse) {
return function(scope, element, attrs) {
var value = $parse(attrs.dataKey)(scope);
console.log(value);
angular.element(element).css('border', '1px solid red');
};
}]);
or you can use $interpolate the same way like $parse but with {{}}
angular strips 'data-' off any attributes, so if your attribute is 'data-key', just use 'key', not 'dataKey'
I would suggest using object notation if you are inside the link function of the directive, which gets the attrs parameter:
attrs.yourAttributeName
Another issue I discovered is that $attr will convert attribute names to lower-casing.
<input myTest="test" />
Value can be obtained with this... attr["mytest"]
i.e.
...link: function (scope, element, attr) { console.log(attr["mytest"]); ...}

Resources