Add directive from inside another directive in angularjs - angularjs

Adding directive from inside another directive, makes the browser to hang.
What im trying to do is
1) Alter an custom element directive (like <h7></h7>) inside the compile function. By doing this the browser hangs.
code:
<h7>TEST</h7>
animateAppModule.directive('h7', function($compile){
return {
restrict:"E",
compile:function(tElement, tAttrs, transclude){
tElement[0].setAttribute("ng-class", "{selected:istrue}");
return function(scope, iElement, iAttrs){
//$compile(iElement)(scope);
}
}
}
})
If i uncomment this line //$compile(iElement)(scope);, the browser hangs.
You can uncomment the above said line in this fiddle http://jsfiddle.net/NzgZz/3/ to see the browser hanging.
However the browser hanging is not happening if i have template property in the h7 directive, as shown in this fiddle. http://jsfiddle.net/KaGRt/1/.
In overall what im trying to achieve is
I want to agument the template, with new functionalities with help of induvidual directives. Somthing like decorator pattern.
I'm doing this inside the compile function of an directive which is in the directive chain so that it affects all that instances of that template.
Pseudo example of what I'm trying to achieve.
<xmastree addBaloon addSanta></xmastree>
1) Say xmastree has a template - <div class="xmastree" ng-class={blinks:isBlinking}></div>
2) Say addBaloon has a template <div class="ballon" ng-class={inflated:isinflated}></div>
Then, addBaloon compile function should augment the template from step1 to something like this
<div class="xmastree" ng-class={blinks:isBlinking}>
<div ng-repeat = "ballon in ballons">
<div class="ballon" ng-class={inflated:isinflated}></div>
</div>
</div>
3) Say addSanta has a template <div class="santa" ng-class={fat:isFat}></div>
Then, addSanta compile function should augment the template from step2 to something like this
<div class="xmastree" ng-class={blinks:isBlinking}>
<div ng-repeat = "ballon in ballons">
<div class="ballon" ng-class={inflated:isinflated}></div>
</div>
<div ng-repeat = "santa in santas">
<div class="santa" ng-class={fat:isFat}></div>
</div>
</div>
After all the compilation, if i run the template derived from step3 against a scope with suitable properties, i should be able to get the HTML.

Calling $compile on the element of the directive itself will cause the same directive to run again, which then repeats that process - forever. In the angular source code of many directives, you can see that they handle this situation like this: $compile(element.contents())(scope); using element.contents() rather than just element(). That means that everything inside of the element is compiled and the directives/data-bindings are registered and no loop is created.
If you do need to $compile the element itself, either replace the original element entirely or remove the original directive from it (remove the attribute, change node type, etc) before compiling.

Related

Is it possible to get the innerHTML of a directive containing an ng-repeat before angular removes it from the DOM during bootstrapping?

I'm working on a style guide and want to add syntax highlighting and code samples dynamically without having to copy/paste and escape the code for each item. The idea is that for each bb-prism directive, I include a bb-sample element that contains my code sample. The bb-prism directive clones the innerHTML of the code sample, creates pre and code elements, and then copies the clone into the code element. It's pretty slick except for when using ng-repeat. Angular seems to remove the ng-repeat before my prism directive can copy it.
<bb-prism>
<h2>Multiple Addresses</h2>
<bb-sample>
<!-- Doesn't work with ng-repeat -->
<bb-address address="address" ng-repeat="address in addresses"> </bb-address>
</bb-sample>
</bb-prism>
The expected result is that the sample code should render:
<bb-address address="address" ng-repeat="address in addresses"> </bb-address>
Instead of:
<!-- ngRepeat: address in addresses -->
Here's a working example:
http://codepen.io/rustydev/pen/77fc7dc3e5365f10ebfabd54440f07c7/
I played with numerous approaches, including using pre-compile in a stripped down version and couldn't log the html to console.
I propose an alternate approach that will show only the original clean markup and without any angular internal classes added
It uses script tag templates, $templateCache and ng-include.
Here's a rough ( but working) proof of concept setting markup in a textarea for now
View
<div test template ="repeat">
<h3>repeating items example</h3>
<!-- Our snippet template -->
<script type="text/ng-template" id="repeat">
<div ng-repeat="item in items">{{item}}</div>
</script>
<!-- include same template -->
<ng-include src="'repeat'"></ng-include>
<!-- source display -->
<h3>source</h3>
<textarea style="width:100%"></textarea>
</div>
Directive
.directive('test', function($templateCache) {
return {
scope: {template: '#'},
link: function(scope, elem) {
var code =$templateCache.get(scope.template);
elem.find('textarea').val(code.trim())
}
}
});
DEMO
Note that there are gulp and grunt scripts that can compile whole directory of html files into $templateCache also and put them in a run() block
Another thought is use ng-include or templateUrl(function) for each of your html prism components and use $templateCache to get whole block of html and use find() on that to get the uncompiled innerHTML of <bb-sample>
EDIT: More comprehensive demo

Variable value as directive/controller name inside template (with $compile/$interpolate)?

I am creating a directive in which template I need to use the a scope's variable value as the name of the directive (or alternatively controller) to load.
Say I have a directive widget that has a template called widget.html which looks like:
<div class="widget widget.type" {{widget.type}} ng-controller="widget.type">
<div class="navBar">
<div ng-include="widget.type + '-t.html'"></div>
<i class="fa fa-close"></i>
<hr>
</div>
<div ng-include="widget.type + '-f.html'"></div>
</div>
Now widget.type is not getting evaluated in the first line. It works fine for ng-include. Say widget.type's value is weather. The first line should then be interpolated first to look like (doesn't matter if class attribute, widget.type-attr or ng-controller is interpolated)
<div class="widget" weather>
and then compiled to include the weather directive.
How can I get widget.type interpolated in the template?
Not an option is to use ng-include to load the directive. I need to use one common template for the widget and want to add/override/extend the base directive with additonal functionality/Variables.
If this is not the way to achieve that, is there a way to extend a directive the OOP-way?
See the plunkr
You can only place interpolation expressions in text nodes and attribute values. AngularJS evaluates your template by first turning it into DOM and then invoking directive compilation, etc. If you try to place {{...}} instead of attribute name, you'll just end up with messed-up DOM.
If you really need to replace a whole directive based on $scope variable value, you'll need to create a directive for application of other directives and do some heavy lifting with $compile (you'll have to completely re-compile the template each time the value changes). I'd recommend trying to find other designs solving your situation before attempting this.
For adjusting your template based on element attributes, see this answer.

Is there a different way to hide a scope variable from showing while AngularJS is loading?

I am using this way:
<div ng-cloak>{{ message.userName || message.text }}</div>
Is this the only / best way to ensure the user does not see the {{ }} when AngularJS is still loading ?
There are several ways to hide content before Angular has a chance to run
Put the content you want to hide in another template, and use ngInclude
<div ng-include="'myPartialTemplate.html'"></div>
If you don't actually want another request made to the server to fetch another file, there are a couple of ways, as explained in the $templateCache docs. There are tools to "compile" external HTML templates into JS to avoid having to do this manually, such as grunt-angular-templates.
Similar to ngInclude, if you put everything in custom directives, with its own template, then the template content won't be shown until Angular has had a chance to run.
<my-directive></my-directive>
With a definition of:
app.directive('myDirective', function() {
return {
restrict: 'E',
template: '<div>Content hidden until Angular loaded</div>'
}
});
ngBind as an alternative to {{}} blocks
<div>Hello <span ng-bind="name"></span></div>
ngCloak as you have mentioned (in this list for completeness).
<div ng-cloak>Content hidden until Angular compiled the template</div>
But you must have certain styles loaded before the page is rendered by the browser, as explained in the ngCloak docs.
You can use ng-bind too as the documentation explains.
A typical advantage about ng-bind is the ability to provide a default value while Angular is loading (indeed, ng-cloak can only hide the content):
<p>Hello, <span ng-bind="user.name">MyDefaultValueWhileAngularIsLoading<span/></p>
Then as soon Angular is loaded, the value will be replaced by user.name.
Besides, ng-cloak is useful when dealing with blocks (many HTML lines) and ng-bind on a particular element.

Access parent element(body) in directive link function

Given a HTML structure similar to this:
<body>
<div id="one" my-directive></div>
<div>
<div id="two" my-directive></div>
</div>
</body>
When I try to access the parent element of two It works and the log returns the parent div, but when the parent is the body, as in one case, it doesn't work and returns an empty set.
app.directive 'myDirective', ->
(scope,iElement,iAttrs) ->
console.log iElement.parent()
EDIT: My guess for this problem is that my app's body is rendered on client side and appended to the body element on module's run method. The html is inserted with $('body').html($compile(body.render())($rootScope)); and I suppose the directive is called within the $compile function before the contents are inserted in the body. Can I work around this problem?
Indeed you understood your problem correctly: $compile will trigger the template compilation and linking phases on your element, so it has no parent while doing so.
The easy way to fix that is to first append your HTML to the body, and then compile it.
var html = body.render();
$('body').html(html);
$compile(angular.element(body))($rootScope);
Or if you don't want to compile the whole body but just the new element:
var elem = $( body.render() ).appendTo($('body'));
$compile(elem)($rootScope);

Using {{$index}} in compiled directive within ng-repeat (jQuery UI buttonset)

In an ng-repeat list, I'm having a terrible time putting an ON/OFF button (using JQ UI wrapping radio buttons) for each item in the list.
When using radio buttons, it seems JQ UI buttonset needs both the "input" and "label" tags plus also the 'for' of the label must match the 'id' of the input.
I can use {{$index}} to make them unique, like this:
<label for='algoOn{{$index}}'>ON</label>
<input type='radio' [... blah blah ..] id='algoOn{{$index}}'>
The problem is calling $().buttonset() once the DOM is ready. I've tried various things (dom.ready, link function etc), but had to resort to calling it after a delay [ $('.buttonme').buttonset() ] to trigger all buttons on the page. Hacky.
However, I'd like to wrap the on/off button in a directive. Still have the same problems with needing unique IDs. (If you don't have unique IDs the buttons get bigger and bigger on each successful call in the directive's link function)
BUT... using {{$index}} in the template gives me a mysterious syntax error:
Syntax error, unrecognized expression: [for=on{{$index}}] <onoffbtn prop="win.runstate" class="ng-isolate-scope ng-scope">
(even though code doesn't have 'for=on{{$index}}' in it!)
The directive is the preferred approach but can't figure out how to get around this one.
Secondly, in the directive, all radio buttons are in sync after the first click, but when the page first loads the buttons in the directive are both blank. It doesn't set itself to the model right away. I thought to do that in the link function (eg. element -> find the input -> set the value) but angular has re-written all of the 'names' and 'ids'.
Plunker showing both issues is here: http://plnkr.co/edit/DTy8dGsRDVVDnWZBYlqQ
Thanks!
Like you said this is doable from a directive. Using your html, I just added buttonset to the wrapping div:
<div id='A{{$index}}' buttonset>
<label for='algoOn{{$index}}'>ON</label>
<input class="buttonme" type='radio' name='onoff{{$index}}' ng-model='win.runstate' ng-name='onoff' value='running' id='algoOn{{$index}}'>
<label for='algoOff{{$index}}'>OFF</label>
<input class="buttonme" type='radio' name='onoff{{$index}}' ng-model='win.runstate' ng-name='onoff' value='stopped' id='algoOff{{$index}}'>
</div>
Here is how the buttonset directive looks
angular.module('button', [])
.directive('buttonset', function() {
return function(scope, elm, attrs) {
$(function(){
$(elm).buttonset();
});
};
});
Here is the plunker, no more hacks :)
Update:
The errors you are getting have to do with the fact that the dwbuttonset directive is executing before the code is compiled by angular. Therefore, what you need to do is to wait until this has been done. You can use $timeout with a 0 value (see this question) in order to queue your method until everything has been loaded.
Example:
.directive('dwbuttonset', function($timeout){
return function(scope, elm, attrs) {
$timeout(function(){
$(elm).buttonset();
});
}})

Resources