This has been bothering me for about an hour now due to my lack of AngularJS proficiency. I am working through trying to understand directives. This example regarding capitalizing text from a directive seems clear, but how to you capitalize text in the following example through a directive without referring to "myMessage.description" within the directive?
<td my-directive>{{myMessage.description}}</td>
I know its staring me right in the face, but I cannot seem to find a direct answer to this probably because I am not phrasing my question correctly in my mind. My thought is that I would have to refer to something hanging off of attrs.value or attrs.text within the link function. If so, how do I update the view with the new value?
The typical way to transform text in a view is to just use a filter in the interpolation, there's a built in uppercase filter so you can just add |uppercase to the interpolated expression.
http://plnkr.co/edit/FbqLdnu3AAW83uy2w31u?p=preview
<p>Hello {{foo.bar|uppercase}}!</p>
Here's a way you could do it with a directive:
http://plnkr.co/edit/xADMMdyuklMgJ9IdmocT?p=preview
.directive('upperCase', function($interpolate){
return {
// Compile function runs before children are compiled/processed
// so we get the initial contents in this function
compile:function(iElem, iAttrs){
var curContents = iElem.html();
//After we grab the contents we empty out the HTML so
//it won't be processed again.
iElem.html('');
// The compile function can return a link function
// this gets run to get the scope for the instance
// and uses the $interpolate service to do the same
// thing angular would do when it sees the {{}}
// with the added step of running toUpperCase
return function(scope, iElem, iAttrs){
iElem.html($interpolate(curContents)(scope).toUpperCase())
}
}
};
});
Related
I am a rookie in angularJS learning about directives (and struggling a lot :)).
I am trying to understand a piece of angularJS code in the plunker
by user tasseKATT for the stack overflow question regarding angular-ui-bootstrap.
I was hoping if anyone can explain this code fragment in more detail.
Specifically
How parsing and compilation happens in directives
How angular knows when to recompile directives ($watch - perhaps, if so how).
I checked the documentation for $parse but dont see any explanation on the service taking a function. What piece of information am I missing.
Also what is the
(value || '').toString();
used for.
What are the properties compileHTML. Where can I see the documentation for the compile function explained in more detail than the one provided by AJS.
What is $$addBindingClass(tElement) and $$addBindingInfo.
Explain the function ngBindHtmlWatchAction
what is $sce.
The fragment from the directive is below
app.directive('compileHtml', ['$sce', '$parse', '$compile',
function($sce, $parse, $compile) {
return {
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.compileHtml);
var ngBindHtmlWatch = $parse(tAttrs.compileHtml, function getStringValue(value) {
return (value || '').toString();
});
$compile.$$addBindingClass(tElement);
return function ngBindHtmlLink(scope, element, attr) {
$compile.$$addBindingInfo(element, attr.compileHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
element.html($sce.trustAsHtml(ngBindHtmlGetter(scope)) || '');
$compile(element.contents())(scope);
});
};
}
};
}
]);
Disclaimer : First thing buddy, you are a rookie in angular and you are trying to understand directives using a very complicated directive. Maybe its better if you start with something a little less complicated :)
I will try to answer your many questions:
How parsing and compilation happens in directives:
Angular knows which all directives are present, so when you write a name which matches one of the directive it compiles the html and link the directive.(I am trying to find a good blog for you to read on these cycles of directive, maybe this post will help)
How angular knows when to recompile directives ($watch - perhaps, if so how): angular doesnot recompile directives, a directive is only compiled once when angular encounters it for the first time.
$watch is basically put on a variable, so whenever angularjs is applying the changes of the scope, it looks for that variable if it is changed.
$parse
It is a way of adding angular to html. So if you directly append html to an element, it does not have angular like features. So we get the html, compile/parse it and then append to the element. Parsing is basically getting all the variables in the html and applying two way binding on those, plus replacing them with their values in controller.
Read this post.
Remember that compile/parse are always done against a scope variable, because variables defined inside the html are properties of the scope variables.
What is this (value || '').toString();
This is a common coding practce of js developers.
This basically amounts to :
(value ? value : '').toString();
Why dont we use value directly like this
value.toString();
Because if the value is undefined or null, it does not have a toString function and it will throw an error.
So the coder is trying to check if the value is there, then convert the value to string otherwise, just put empty string(converting to string results empty string).
what is $sce?
sce is a service, that comes in file angular-sanitize.js which is a part of angular bundle. When somebody tries to modify html to insert his link(malicious activity we call it), angular does not allow and treats his html as simple text.
But what if you want to add html, you pass your html to $sce service(injected in controller/directive etc) and the output is the html which you can insert into the view(this will come as html).
$$addBindingClass
This is just to add "ng-binding" class to the element, so that you can see which element has a model created by angularjs.
$$addBindingInfo
This is to add information to the element for debugging purpose. You can select the element in the inspector and run the following statement in console to get the scope information.
angular.element($0).scope(); //$0 means selected element
Here is a link that explains this thing better.
I have a custom directive that generates an input with its validation errors , eventually after building the input, here is the part I'm wondering about :
var html= template.get();
element.html(html);
$compile(element)(scope);
I also added to the directive object which I think is not making difference since I don't have template in the object or should it?
replace:true
but yet the directive element DOM is still there , and the generated input is being appended as a child , can you help me out in this ?
Yes, replace is used in conjunction with template/templateUrl.
For the templates that are retrieved dynamically in link, use
var html= template.get();
element.replaceWith($compile(html)(scope));
Notice that obvious drawback in comparison with replace is that directive's attributes won't be translated to template element, this has to be done manually.
Its not working because you haven't provided a template parameter and your link function was manually adding the elements. Remove the link function and add a template parameter like so and it'll work. Ive updated your fiddle with it working too
app.directive('test',function($compile){
return {
restrict:'E',
replace:true,
template:'<input type="text" name="test">'
}
});
I have an ng-view with multiple instances of the localytics (angular) Chosen plugin. I also have an ng-include with one instance of the plugin. Both rendered on the same page.
I'm using the data-placeholder attribute to render a value which is filtered through the angular-translate plugin.
Initially I was having issues with all Chosen instances rendering the translated text when the method to update the language was being called.
I got around this by calling $route.reload() at the end of the method (not ideal, but acceptable).
I tried:
binding the values for the translations and the translate filter inline
setting them in controllers
watching the properties on the $scope (which never
triggered)
destroying the template before reloading the route
However, the placeholder within the ng-include refuses to update without the use of a hard refresh. Calling $window.location.reload() at the end of the method allows all instances to show the correct translation, but short of this I've not been able to find a way to fix the issue.
I'm assuming it's a scoping issue. Perhaps the Chosen plugin (which is a directive) creates its own scope, then the ng-include has its own scope, as does the ng-view.
All properties that are being translated, outside of the Chosen plugins, are working as expected.
Currently the angular-translate objects look like this:
var translationEN = {
SEARCH: {
'SEARCH-BTN': 'Search'
}
}
So I'm binding them inline as per the following:
<div ng-bind="'SEARCH.SEARCH-BTN' | translate">
I've also attempted some of the methods on $translate, such as $translate.refresh() to no avail.
If anyone has any ideas, any help and / or comments are very much appreciated.
Thanks in advance.
You can use the chosen attribute to pass in some configurations instead of using the data-placeholder attribute, like this:
<select chosen="{'placeholder_text_single': 'Select the options'}"></select>
Or you can write custom attributes that the chosen directive will also accept as configurations. However, when using attributes, the directive will evaluate the expression instead of using the literal value, which won't work as expected for translation purposes, as '{{ ... }}' is not a valid expression. The attributes would be like this:
<select chosen placeholder-text-single="'Select the options'"></select>
A similar problem occurred for me when the options were loaded by a promise.
With just an empty array, the translation worked fine, but putting the promise back in the code caused this behaviour.
A quick debugging in the chosen directive showed that, the element, from which the chosen angular plugin takes the template for the chosen widget, is not linked (or compiled... I'm really new with angular), it still contains the {{placeholder.string | translate}} value for the data-placeholder attribute, however, the attr.placeholder contains the tranlated value.
So this line sets wrong value as the default text: https://github.com/localytics/angular-chosen/blob/master/chosen.js#L57
I extended the chosen directive with a preLink function, which modified the element's data-placeholder attribute with the right value:
angular.module('myModule').directive('chosen', function() {
return {
priority : 1,
restrict: 'A',
link : {
pre : function(scope, element, attr, ngModel) {
var defaultText = attr.placeholder;
angular.element(element[0]).attr('data-placeholder', defaultText);
}
}
}
});
I'm using angularjs and need to find the watch of the ng-repeat, because I need ng-repeat to stop working from a specific point. this my code:
<ul>
<li ng-repeat="item in items">
<div>{{item.name}}</div>
</li>
</ul>
I need to find the watcher of the ng-repeat. If I go to scope.$parent.$$watchers[x] and perform splice the watch is removed, but how can I find the specific watch?
It's not possible to find the watch (see explanation below), but you can achieve what you wish by use the following directive
app.directive('repeatStatic',
function factory() {
var directiveDefinitionObject = {
restrict : 'A',
scope : true, // this isolates the scope from the parent
priority : 1001, // ng-repeat has 1000
compile : function compile() {
return {
pre : function preLink(tElement, tAttrs) {
},
post : function postLink(scope, iElement, iAttrs, controller,
transcludeFn) {
scope.$apply(); // make the $watcher work at least once
scope.$$watchers = null; // remove the $watchers
},
};
}
};
return directiveDefinitionObject;
}
);
and its usage is
<ul>
<li repeat-static ng-repeat="item in items">
{{ item }}
</li>
</ul>
See http://plnkr.co/k9BTSk as a working example.
The rational behind is that
the angular directive ng-repeat directive uses internal function $watchCollection to add a self created listener that watchs the items object. Since the listener is a function created during the process, and is not keep anywhere as reference, there is no good way to correctly identify which function to remove from the $$watchers list.
However a new scope can be forced into the ng-repeat by using an attribute directive, in this way the $$watchers added by ng-repeat are isolated from the parent scope. Thus, we obtain full control of the scope.$$watchers. Immediate after the ng-repeat uses the function that fills the value, the $$watchers are safe to be removed.
This solution uses hassassin's idea of cleaning the $$watchers list.
I have a fork of Angular that lets you keep the watch in the $$watchers but skip it most of the time. Unlike writing a custom directive that compiles your HTML it lets you use normal Angular templates on the inside, the difference is that once the inside is fully rendered the watches will not get checked any more.
Don't use it unless you really genuinely need the extra performance because it can have surprising behaviour.
Here it is:
https://github.com/r3m0t/angular.js/tree/digest_limit
Here's the directive you can use with it (new-once):
https://gist.github.com/r3m0t/9271790
If you want the page to never update you can use new-once=1, if you want it to sometimes update you can use new-once=updateCount and then in your controller run $scope.updateCount++; to trigger an update.
More information: https://github.com/Pasvaz/bindonce/issues/42#issuecomment-36354087
The way that I have dealt with this in the past is that I created a custom directive that copies the logic of the built in ngRepeat directive but never sets up the watches. You can see an example of this here, which was created from the ngRepeat code from version 1.1.5.
The other way, as you mentioned was to remove it from $$watchers of a scope, which is a little stranger since it accesses a private variable.
How this could be done is that you create a custom directive on the repeat to remove the watch that is the repeat. I created a fiddle that does this. It basically just on the last element of the repeat clears the parent watch (which is the one on data)
if (scope.$last) {
// Parent should only be the ng-repeat parent with the main watch
scope.$parent.$$watchers = null;
}
This can be modified to fit your specific case.
Hope this helps!
It sounds like you want to put the bindonce directive on your ng-repeat.
https://github.com/Pasvaz/bindonce
If you don't need angular dual-binding, have you tried a custom directive with a complie function where you construct HTML yourself by creating DOM from scratch without any angular dual-binded mechanisms ?
I've made an AngularJS directive to add an ellipsis to overflow: hidden text. It doesn't seem to work in Firefox, and I don't believe I've structured it as well as possible. The flow is:
Add directive attribute to HTML element
Directive reads ng-bind attribute into scope
Directive watches for changes to ng-bind in link function
On ng-bind change, directive does some fancy calculations to determine where text should be split and ellipsis added (I've not included this code here, just assume it works)
Directive sets the element's HTML equal to this new string, not touching ng-bind
HTML
<p data-ng-bind="articleText" data-add-ellipsis></p>
DIRECTIVE
app.directive('addEllipsis', function(){
return {
restrict : 'A',
scope : {
ngBind : '=' // Full-length original string
},
link : function(scope, element, attrs){
var newValue;
scope.$watch('ngBind', function () {
/*
* CODE REMOVED - Build shortened string and set to: newText
*/
element.html(newText); // - Does not work in Firefox and is probably not best practice
});
}
};
});
The line in question is that last one in the directive:
element.html(newText)
I'm assuming some template-style approach should be used? I'm unclear how to best approach the answer. Thanks for any help.
You could use a filter instead. Something like this:
FILTER
app.filter('addEllipsis', function () {
return function (input, scope) {
if (input) {
// Replace this with the real implementation
return input.substring(0, 5) + '...';
}
}
});
HTML
<p data-ng-bind="articleText | addEllipsis"></p>
If you replace data-ng-bind="articleText" with ng-model="articleText" it should work in both Chrome and Firefox. I don't know why, maybe it is a bug? But it is a quick fix.
If you are interested in the difference, you can take a look at this post. But the behavior being different in different browser is indeed a bit weird.