Angular Directive: use $watch or pass in binded value - angularjs

I have a controller which calls a service returning a list of courses.
Each course has an property called 'percentageDone'
I want to assign some CSS to my div depending on this value.
there are 2 ways I have found to do this. There are probably more, I am a newbie at Angular. Thing is I am not sure what is the best way. In terms of performance and general best Angular practice. I realize I could use ng-class, but I am using this exercise to learn Angular.
So my Div
<div ng-repeat="e in courses">
<div completion-color="{{e.percentageDone}}"></div>
</div>
with this directive
myApp.directive('completionColor', function () {
return {
restrict: 'A',
link: function (scope, el, attrs) {
var donePerc = attrs['completionColor'];
if (donePerc < 33) {
el.addClass('progress-bar-danger');
}
else if (donePerc >= 33 && newVal < 66) {
el.addClass('progress-bar-warning');
}
else if (donePerc >= 66) {
el.addClass('progress-bar-success');
}
}
};
});
or another way
<div completion-color="e.percentageDone"></div>
and js
myApp.directive('completionColor', function () {
return {
restrict: 'A',
link: function (scope, el, attrs) {
scope.$watch(attrs['completionColor'], function (newVal) {
if (newVal < 33) {
el.addClass('progress-bar-danger');
}
else if (newVal >= 33 && newVal < 66) {
el.addClass('progress-bar-warning');
}
else if (newVal >= 66) {
el.addClass('progress-bar-success');
}
});
}
};
});
I like that I don't have to put the {{}} in my directive, but in general the data will not change much. So not sure I need to use a $.watch.

I'd say $watch is the last thing you should use here.
I myself see no problem with using {{}}, that's what they are for. But you have another option if you want, you could use One Time Binding (https://docs.angularjs.org/guide/expression) if you're using angular 1.3.X and that way you're improving performance, since angular doesn't keep watch for that:
<div ng-repeat="e in courses">
<div completion-color="{{::e.percentageDone}}"></div>
</div>
And work with a scope in your directive (you can use isolated as well):
scope: {
completionColor: "#"
},
Now you can use scope.completionColor in your link function.

Related

Angular 1.x Directive With A Template

I'm trying to create an angular directive for forming sentences. The goal is to take a list and iterate through them as necessary. The result of the directive would be something like:
shoes, pants and socks
or
shoes, pants and +5 more
I have the basic directive setup to work with a array of strings - but I'd like to customize it to allow custom templates for each sentence element (i.e. hyperlinks, styling, etc). That is:
<sentence values="article in articles">
<strong>{{article.title}}</strong> by <span>{{article.author}}</span>
</sentence>
The HTML the user sees in the browser needs to be something like:
$scope.articles = [
{ title: '...', author: '...'},
{ title: '...', author: '...'},
...
]
<span><strong>ABC</strong> by <span>123</span></span>
<span>, </span>
<span><strong>DEF</strong> by <span>456</span></span>
<span>and</span>
<span>+5 more</span>
I'm guessing it has something to do with transclude but cannot figure out the API. I've also experimented with using ng-repeat instead of the directive template but wasn't able to find a solution.
Something like this should work where maxArticles is a number defined on your scope
<sentence values="article in articles | limitTo: maxArticles">
<strong>{{article.title}}</strong> by <span>{{article.author}}</span>
<span ng-if="$index < maxArticles - 2">, </span>
<span ng-if="$index === articles.length - 1 && articles.length <= maxArticles">and</span>
</sentence>
<span ng-if="articles.length > maxArticles">
and +{{articles.length - maxArticles}} more.
</span>
Iterating AND providing dynamic content is a common use for a custom directive with the compile function + the $compile service. Watch out: essentially you are repeating the functionality of ng-repeat, you may want to consider alternatives.
E.g. instead of the articles list, use another one (perhaps named articlesLimited). The new list is constructed dynamically and contains the first elements from articles. A flag (e.g. hasMore) indicates whether the original articles has more elements, simply as: $scope.hasMore = articles.length > 5. You use the hasMore flag to show/hide the "+N more" message.
For what it's worth however, below is an implementation of the sentence directive. See the comment for weak points!
app.directive('sentence', ['$compile', function($compile) {
var RE = /^([a-z_0-9\$]+)\s+in\s([a-z_0-9\$]+)$/i, ONLY_WHITESPACE = /^\s*$/;
function extractTrimmedContent(tElem) {
var result = tElem.contents();
while( result[0].nodeType === 3 && ONLY_WHITESPACE.test(result[0].textContent) ) {
result.splice(0, 1);
}
while( result[result.length-1].nodeType === 3 && ONLY_WHITESPACE.test(result[result.length-1].textContent) ) {
result.length = result.length - 1;
}
return result;
}
function extractIterationMeta(tAttrs) {
var result = RE.exec(tAttrs.values);
if( !result ) {
throw new Error('malformed values expression, use "itervar in list": ', tAttrs.values);
}
var cutoff = parseInt(tAttrs.cutoff || '5');
if( isNaN(cutoff) ) {
throw new Error('malformed cutoff: ' + tAttrs.cutoff);
}
return {
varName: result[1],
list: result[2],
cutoff: cutoff
};
}
return {
scope: true, // investigate isolated scope too...
compile: function(tElem, tAttrs) {
var iterationMeta = extractIterationMeta(tAttrs);
var content = $compile(extractTrimmedContent(tElem));
tElem.empty();
return function link(scope, elem, attrs) {
var scopes = [];
scope.$watchCollection(
function() {
// this is (IMO) the only legit usage of scope.$parent:
// evaluating an expression we know is meant to run in our parent
return scope.$parent.$eval(iterationMeta.list);
},
function(newval, oldval) {
var i, item, childScope;
// this needs OPTIMIZING, the way ng-repeat does it (identities, track by); omitting for brevity
// if however the lists are not going to change, it is OK as it is
scopes.forEach(function(s) {
s.$destroy();
});
scopes.length = 0;
elem.empty();
for( i=0; i < newval.length && i < iterationMeta.cutoff; i++ ) {
childScope = scope.$new(false, scope);
childScope[iterationMeta.varName] = newval[i];
scopes.push(childScope);
content(childScope, function(clonedElement) {
if( i > 0 ) {
elem.append('<span class="sentence-sep">, </span>');
}
elem.append(clonedElement);
});
}
if( newval.length > iterationMeta.cutoff ) {
// this too can be parametric, leaving for another time ;)
elem.append('<span class="sentence-more"> +' + (newval.length - iterationMeta.cutoff) + ' more</span>');
}
}
);
};
}
};
}]);
And the fiddle: https://jsfiddle.net/aza6u64p/
This is a tricky problem. Transclude is used to wrap elements but when using transclude you don't have access to the directive scope, only to the scope of where the directive is being used:
AnglularJS: Creating Custom Directives
What does this transclude option do, exactly? transclude makes the contents of a directive with this option have access to the scope outside of the directive rather than inside.
So a solution is to create another component to inject the template's scope inside the directive, like this:
.directive('myList', function() {
return {
restrict: 'E',
transclude: true,
scope: { items: '=' },
template: '<div ng-repeat="item in items" inject></div>'
};
})
.directive('inject', function() {
return {
link: function($scope, $element, $attrs, controller, $transclude) {
$transclude($scope, function(clone) {
$element.empty();
$element.append(clone);
});
}
};
})
<my-list items="articles">
<strong>{{item.title}}</strong> by <span>{{item.author}}</span>
</my-list>
This was taken from this discussion: #7874
And I made a Plnkr.

Re-evaluate directive attribute expression

I'm having troubles in re-evaluating an expression passed as attribute of a custom angular (1.2.28) directive.
I've tried all the possible combination of $eval, $parse as well as isolated and non-isolated scope. I can't wrap my mind around this.
I have something like this:
<div ng-repeat="item in dataset">
<my-directive
show-tooltip="user.level=='visitor' && item.memberOnly"
content-tooltip="isAdded(item) && 'Remove Me' || 'Add Me'">
<my-directive>
</div>
The problem is that user.level can change because for example the user logged in and the (enclosing) scope function isAdded can returns different values depending if the items was already added to a list or not.
The directive:
angular.module("MyModule", [])
.directive("myDirective", () {
return {
restrict: 'E',
priority: 999,
link: function(scope, elm, attrs) {
showTooltip = scope.$eval(attrs.showTooltip);
contentTooltip = scope.$eval(attrs.contentTooltip);
// This works
scope.$watch(attrs.contentTooltip, function(value) {
if( value and value != contentTooltip)
contentTooltip = value
});
// This never works
scope.$watch(attrs.showTooltip, function(value) {
if( value and value != showTooltip)
showTooltip = value
});
// Do things..
}
}
});
I don't know why but the first watch will work, the second will never work. I've used a similar approach with $parse but couldn't get it to work either.
Maybe I'm doing this totally wrong
Look into using attrs.$observe instead:
attrs.$observe('showTooltip', function(){
})

angularjs directive - get element bound text content

How do you get the value of the binding based on an angular js directive restrict: 'A'?
<span directiverestrict> {{binding}} </span>
I tried using elem[0].innerText but it returns the exact binding '{{binding}}' not the value of the binding
.directive('directiverestrict',function() {
return {
restrict:'A',
link: function(scope, elem, attr) {
// I want to get the value of the binding enclosed in the elements directive without ngModels
console.log(elem[0].textContent) //----> returns '{{binding}}'
}
};
});
You can use the $interpolate service, eg
.directive('logContent', function($log, $interpolate) {
return {
restrict: 'A',
link: function postLink(scope, element) {
$log.debug($interpolate(element.text())(scope));
}
};
});
Plunker
<span directiverestrict bind-value="binding"> {{binding}} </span>
SCRIPT
directive("directiverestrict", function () {
return {
restrict : "A",
scope : {
value : '=bindValue'
},
link : function (scope,ele,attr) {
alert(scope.value);
}
}
});
During the link phase the inner bindings are not evaluated, the easiest hack here would be to use $timeout service to delay evaluation of inner content to next digest cycle, such as
$timeout(function() {
console.log(elem[0].textContent);
},0);
Try ng-transclude. Be sure to set transclude: true on the directive as well. I was under the impression this was only needed to render the text on the page. I was wrong. This was needed for me to be able to get the value into my link function as well.

AngularJS: writing a capatilize directive does not seem to work

I am new to AngularJS so if i'm completely off track i apologize.
Template (which is iterated):
<div class="title">
<input ng-model="menuItem.kind" capitalize-first />
</div>
Directive (to capitalize):
angular.module('phonecat', []).directive('capitalizeFirst', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var capitalize = function(inputValue) {
var capitalized = inputValue.charAt(0).toUpperCase() +
inputValue.substring(1);
if(capitalized !== inputValue) {
modelCtrl.$setViewValue(capitalized);
modelCtrl.$render();
}
return capitalized;
}
modelCtrl.$parsers.push(capitalize);
capitalize(scope[attrs.ngModel]); // capitalize initial value
}
};
});
The result is a blank span. Any ideas?
You can't use scope[attrs.ngModel]. AngularJs will not set scope['menuItem.kind'] = ..., but scope.menuItem.kind = ....
Additionally, the value might had been resolved to a parent scope. So your best choice is to use ngModel.$modelValue to get the current model value of ngModel (capitalize(modelCtrl.$modelValue);).
This leads to another problem. You don't have this value in linking phase, it's lightly async. To workaround it, all you need is to inject $timeout and run this code asynchronously:
$timeout(function() {
capitalize(modelCtrl.$modelValue);
}, 0);
So, summarizing, all you need is to change your directive last line to the one above, and this will solve the problem. All the rest is ok, take a look at this Plnkr.

Angular Directive Different Template

I have a directive myDirective with variable type. If I run <my-directive type="X"> I want the directive to use templateUrl: x-template.html.
If I do <my-directive type="Y"> I want the directive to use templateUrl: y-template.html.
This is my current directive.
app.directive('myDirective', function() {
var myDirective = {
templateUrl: 'X-template.html',
restrict: 'E',
scope: {
type: '='
},
};
return myDirective;
});
I read thru stackoverflow and angular documentation but have not found anything that I need.
I am now trying to do something along the lines of:
if ($scope.type === 'X') {
templateUrl: 'X-template.html',
}
else if ($scope.type === 'Y') {
templateUrl: 'Y-template.html',
}
But do not know where to do it.
Do you guys know if this is possible and how?
Angular will accept a function as the template option, so you could do something like so:
.directive('myDirective', function () {
return {
templateUrl: function (tElement, tAttrs) {
if (tAttrs) {
if (tAttrs.type === 'X') {
return 'X-template.html';
}
if (tAttrs.type === 'Y') {
return 'Y-template.html';
}
}
}
}
});
For more info, see the documentation for the $compile service.
You can work around this issue using ng-include inside compile:
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
fiddle
If you're willing to live on the bleeding edge with a build on the 1.1.x code path (note the warning attached to every 1.1.x build notes entry so I don't dilute this answer by repeating it again here), you're in luck--this very feature was just added in the 1.1.4 release on April 3rd. You can find the release notes for 1.1.4 here and the feature's task log includes a Jasmine test that demonstrates how to use the new functionality.
If you're more conservative and are using a 1.0.x release, then you won't be able to accomplish this as easily, but it can be done. Mark Rajcok's solution looks like it would fit your requirements as-stated, but I would just add a few additional notes:
Aside from its 1.1.4 release, compile-time directives don't support modification at runtime.
As of 1.1.4, you can safely modify the attributes of compile-time directives, but only from another compile-time directive.
You may want to consider replaceWith() instead of append() since <my-directive> is not a standard-defined HTML element type.
If your X and Y templates contain additional directives, I don't think you'll be able to pass attributes on <my-template> through to the root element of your template so easily.
A directive with replace: true will transfer attributes from the source element to its replacement root, but I do not think that ngInclude will do the same from is host to the root of the included template.
I also seem to recall that ngInclude does not require that its template have exactly one root element.
You could perhaps preserve attributes on a replacement parent by using replaceWith() instead of append() and wrapping the <div ng-include=""> tag within a <div></div>. The outer <div> could hold attributes and would still be accessible after the <div ngInclude> element replaced itself with loaded content.
Be aware that ngInclude creates a new scope. This subjects you to a flashing yellow klaxons warning about the dangers of primitive scope models. For more information, see this fine page from Angular's GitHub depot.
I can propose another alternative for those on 1.0.x, but it involves a fair amount of code. It's a more heavy-weight operation, but it has the upside of not only being able of switching between templates, but full-fledged directives as well. Furthermore, its behavior is more readily dynamic.
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/directive/my-directive.html',
link: function(scope, element, attrs, ctrl) {
// You can do this with isolated scope as well of course.
scope.type = attrs.type;
}
}
);
my-directive.js
<div ng-switch on="{{type}}">
<div ng-switch-where="X" ng-include="X-template.html"></div>
<div ng-switch-where="Y" ng-include="Y-template.html"></div>
</div>
my-directive.html
This is my version for optionally overriding a default template
templateUrl: function (elem, attrs) {
if (attrs.customTemplate) {
return '/path/to/components/tmpl/' + attrs.customTemplate + '.html';
} else {
return '/path/to/components/tmpl/directive.html';
}
}
e.g on a directive
<div my-directive custom-template="custom"></div>
I solve this problem so:
app.directive("post", function ($templateCache, $compile) {
function getTemplate(mode) {
switch (mode) {
case "create":
return "createPost.html";
case "view":
return "viewPost.html";
case "delete":
return "deletePost.html";
}
}
var defaultMode = "view";
return {
scope: {},
restrict: "AE",
compile: function (elem, attrs, transclude) {
return function ($scope, $element, $attr) {
function updateTemplate() {
$element.html("");
$compile($templateCache.get(getTemplate($scope.mode)).trim())($scope, function (clonedElement, scope) {
clonedElement.appendTo($element);
});
}
$scope.mode = $attr.mode || defaultMode;
$scope.$watch("mode", updateTemplate);
}
}
}
});
It's probably not the best way to do this, but I have no extra scope.
Ok, this might help someone here :-)
To inject your custom attr into your link or controller function use the following.
I'm at work right now but will post a fiddle later if I get a chance :-)
.directive('yourDirective', function() {
return {
restrict: 'EA',
template: '<div></div>', // or use templateUrl with/without function
scope: {
myAttibute: '#myAttr' // adds myAttribute to the scope
},
link: function(scope) {
console.log(scope.myAttibute);
}
}
}
// HTML ""
// Console will output "foo"

Resources