Angular JS Directive - complete background - angularjs

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.

Related

Angular Directive not executing on UI Bootstrap Modal open

I have a one-page site that I am building out and this is my first time using Angular on a site. Building it on top of Laravel too for the backend but that is beyond the scope of this question.
I need to be able to open a modal on a main page view which will add a new resource (e.g. a new client) or edit a resource. I want to somehow get the form's html inside the modal body when the $uibModal.open()'s controller is called and set the $scope.modalBody equal to the injected items.modalBody (the only way this works is if I use:
$scope.modalBody = $sce.trustAsHtml(items.modalBody);
The only problem now is that anything inside the HTML body, Angular will not use it's magic and do any data-binding. It is still in the raw form of
{{ object.property }} or since I'm using Laravel and avoiding conflict with the Blade template engine:
<% object.property %>
See screenshot:
screenshot
I have been banging my head against the wall on this one...I have tried putting $scope.$apply() in my directive and my controller, neither of which worked. I have a feeling that is the source of my problem though. I have also tried making the html just a <new-client></new-client> directive and using templateUrl: 'views/clients/add.php' which would be ideal, but the template is not being included inside the <new-client></new-client>.
I'm using ui-bootstrap 0.14.3 and Angular 1.4.8.
Could this be a bug? Or am I doing something wrong? Anyone have a better way of getting a form into my modal? Let me know what code you want to see so I don't clutter this post with unnecessary code blocks.
I have come across a similar issue with using jQuery's AJAX to receive template strings and append it to a server.
So when HTML is added via jQuery, bound html string, etc., angular doesn't know it needs to automagically compile this data.
What you need to do is use the $compile service, to $compile your html and then attach the correct $scope to it:
`$compile('jQuerySelectorReturningHtmlOrAnHTMLStringThatNeedsToBeCompiled')($scope);`
There are multiple examples in Angulars Documentation for $compile that can give you an idea of what is happening. I think by what you have described the same thing is happening here in your situation.
The key is to call this $compile service function after the html has been bound to the page.
EDIT:
There are a few other options based on some comments, that will serve as a viable solution to rendering this content on your view. For example a directive that takes a string attribute representing the HTML string of your desired view.
1. Modify your directive template in the compile step:
You have the ability to modify your template before the directive compiles and binds any attributes to it, to that directives scope:
app.directive('myAwesomeCompileStepDirective', [myAwesomeCompileStepDirectivef]);
function myAwesomeCompileStepDirectiveFn() {
return {
restrict: 'EA',
compile: function compileFn(tAttrs, tElement) {
//Here you can access the attrs that are passed into your directive (aka html string)
tElement.html(tAttrs['stringThatYouWantToReplaceElementWith']);
return function linkFn(scope, element, attrs, controller, transcludeFn) {
//if all you want to do is update the template you really don't have to do anything
//here but I leave it defined anyways.
}
}
}
}
You can view a file I wrote for a npm component which uses this method to modify my directive template before it is compiled on the page & you can also view the codepen for the complete component to see it in action.
2. Use $compile service to call $compile in link function using directive attrs.
In the same way as the aforementioned method, you can instead inject the $compile service, and call the function mentioned above. This provides a bit more work, for you but more flexibility to listen to events and perform scope based functions which is not available in the compile function in option 1.

Transform text between element AngularJS

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())
}
}
};
});

Angular translation not updating placeholder of Chosen plugin within ng-include $scope

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);
}
}
}
});

AngularJS Multiple Directive Resource Contention

I am trying to build a directive with angular.
Here is the plunker
I wanted to split it into 3 directives:
Top, grand-parent directive. - many DAYS
Middle, created with ng-repeat - one DAY
Bottom, created with ng-repeat - many TIME BLOCKS
angular
.directive('dateTimeBlocks', [function dateTimeBlocksDirective () {}]) .directive('dayBlock', [function dayDirective () {}])
.directive('timeBlock', [function timeBlockDirective () {}])
I wanted to create middle and bottom directives with
isolated scopes and only pass the data that I want to modify inside.
But it seems to unable to compile
"Multiple directives [dateBlock, dateBlock] asking for template on: ..."
Any input would be greatly appreciated.
This line causes that error:
<date-block data-date-block="datePeriod"></date-block>
The reason is a combination of factors. First, AngularJS always normalizes directive declarations, so data-date-block (or x-date-block, data:date:block etc.) is actually treated as date-block. Therefore, the above line is equivalent to:
<date-block date-block="datePeriod"></date-block>
Now, the dateBlock directive is declared with restrict: 'AE', so it can be applied as either an element or attribute. Therefore, the above line resulting in AngularJS applying the dateBlock directive to the element twice.
That per se doesn't cause the error, but dateBlock declares a template and AngularJS doesn't allow an element to have 2 templates (it doesn't make sense anyway, which template should AngularJS choose?), so it throws an error.
There are several ways to fix it.
Restrict the directive to E so that AngularJS doesn't treat data-date-block as a directive.
Rename the isolated scope property dateBlock to something else.
Use the attribute form of the directive and use <div> for the element form. Like this: <div data-date-block="datePeriod"></div>
Just in case anyone else comes here, you can also get this error if you have a template and templateUrl in the same directive.
i.e:
...
template: '<div>Hello world</div>',
templateUrl: "MyTemplate.html",
...
Hope that helps someone, the error message doesn't immediately point you to this.

Creating an AngularJS Directive for jQuery UI Button

Update: Fiddle w/ full solution: http://jsfiddle.net/langdonx/VXBHG/
In efforts to compare and contrast KnockoutJS and AngularJS, I ran through the KnockoutJS interactive tutorial, and after each section, I'd rewrite it in AngularJS using what little I already knew + the AngularJS reference.
When I got to step 3 of the Creating custom bindings tutorial, I figured it would be a good time to get spun up on Angular Directives and write a custom tag. Then I failed miserably.
I'm up against two issues that I haven't been able to figure out. I created a new Fiddle to try and wrap my head around what was going on...
1 (fiddle): I figured out my scoping issue, but, is it possible to just passthrough ng-click? The only way I could get it to work is to rename it to jqb-click which is a little annoying.
2 (fiddle): As soon as I applied .button() to my element, things went weird. My guess is because both Angular and jQuery UI are manipulating the HTML. I wouldn't expect this, but Angular seems to be providing its own span for my button (see line 21 of the JavaScript), and of course so is jQuery UI, which I would expect. I hacked up the HTML to get it looking right, but even before that, none of the functionality works. I still have the scope issue, and there's no template binding. What am I missing?
I understand that there's an AngularUI project I should be taking a look at and I can probably pull off what I'm trying to do with just CSS, but at this point it's more about learning how to use Directives rather than thinking this is a good idea.
You can create an isolated scope in a directive by setting the scope parameter, or let it use the parent scope by not setting it.
Since you want the ng-click from parent scope it is likely easiest for this instance to use the parent scope within directive:
One trick is to use $timeout within a directive before maniplulatig the DOM within a templated directive to give the DOM time to repaint before the manipulation, otherwise it seems that the elements don't exist in time.
I used an attribute to pass the text in, rather than worrying about transclusion compiling. In this manner the expression will already have been compiled when the template is added and the link callback provides easy access to the attributes.
<jqbutton ng-click="test(3)" text="{{title}} 3"></jqbutton>
angular.module('Components', [])
.directive('jqbutton', function ($timeout) {
return {
restrict: 'E', // says that this directive is only for html elements
replace: true,
template: '<button></button>',
link: function (scope, element, attrs) {
// turn the button into a jQuery button
$timeout(function () {
/* set text from attribute of custom tag*/
element.text(attrs.text).button();
}, 10);/* very slight delay, even using "0" works*/
}
};
});
Demo: http://jsfiddle.net/gWjXc/8/
Directives are very powerful, but also have a bit of a learning curve. Also in comparison of angular to knockout, angular is more of a meta framework that in the long run has far more flexibilty than knockout
Very helpful reading for understanding scope in directives:
https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance

Resources