Angular Selective Transclude - angularjs

Does anyone know a way to have a directive that can transclude the body of the directive selectively?
For example, if I have the following div and directive of myWrapper.
<div data-my-wrapper="foo">
<h1>Hello World</h1>
</div>
I'd like to have code in my directive that is like this pseudo code.
if (foo.locked) {
// user a static template saying that the item is locked.
}
else {
// Tranclude the body content
}
Is there a way to direct to the directive what content should be transcluded? Or is there a recommended way to have the equivalent of a if/then/else that could allow me to swap out the content instead of the body of the element that the directive sits in?

You can gain access to transcluded content inside the link function of a directive by using making use of the fifth argument, which happens to be a transclude linking function. Remember also to set the transclude property of the directive.
function link(scope, iElement, iAttrs, controller, transcludeFn) { ... }
Here's a crude demonstration of the transclude linking function at work: JSFiddle.
Glad that helped.
NOTE:
Please be aware that if you're using a version of AngularJS prior to 1.2.x that the transclude linking function is instead available as the third argument to the compile property of a directive. This has since been deprecated as pointed out by the official documentation.

Related

Confusing attributes in angularjs directive's link function

I'm writing an angular.js directive, which would conditionally hide the element.
So it would look like this:
link: function(scope, elem, attrs) {
...
elem.hide()
}
I found a lot of examples that were doing exactly that, but somehow my elem attribute is an array not an element, so it does not have a hide() method.
What am I missing?
Thanks!
Most people are loading jQuery before they load angular, which extends its jqLite to the full jQuery.
The hide method doesn't seem to be part of jqLite API (https://docs.angularjs.org/api/ng/function/angular.element), hence such method is not exposed.
That doesn't mean you need jQuery, but that it is not the correct way to handle your problem. There are already the ng-show and ng-if directives to conditionnaly hide an element based on the controller, couldn't you use them?
In your html, add <div ng-show="isDisplayed">, and in your linking function scope.isDisplayed = false

AngularJS : Directive shell that allows user defined content with directive scope

Here's a plunker example you can see: http://plnkr.co/edit/NQT8oUv9iunz2hD2pf8H
I have a directive that I would like to turn into a web component. I've thought of several ways as to how I can achieve that with AngularJS but am having difficulty with a piece of it. I'm hoping someone can explain my misstep rather than tell me a different way to do it.
Imagine you have a directive component that sets up some shell with css classes maybe some sub components, etc.. but lets the user define the main content of the component. Something like the following:
<my-list items="ctrl.stuff">
<div>List Item: {{ item.name }}</div>
</my-list>
The HTML for the list directive could be something like the following (with OOCSS):
<ul class="mas pam bas border--color-2">
<li ng-repeat="items in item track by item.id" ng-transclude></li>
</ul>
Normally this can be solved in the link function by linking the directives scope to the new content. And it does work for other components. However introducing the ng-repeat seems to break that portion of the control. From what I can tell, the appropriate place might be the compile function but the documentation says the transcludeFn parameter will be deprecated so I'm not sure how to proceed.
I should also note that when using the beta AngularJS, there is either a bug or a new paradigm coming, because this is no longer a problem. It seems like the transcluded content always gets access to the directives scope as well as the outer controllers scope.
I really appreciate any enlightenment on this.
It's by design that content added via ng-transclude will bind with an outer controller scope, not a scope of the current element that ng-transclude is on.
You could solve the problem by copy the ng-transclude's code and modify it a bit to give a correct scope:
.directive('myTransclude', function () {
return {
restrict: 'EAC',
link: function(scope, element, attrs, controllers, transcludeFn) {
transcludeFn(scope, function(nodes) {
element.empty();
element.append(nodes);
});
}
};
});
And replace the ng-transclude with my-transclude in your directive template.
Example Plunker: http://plnkr.co/edit/i7ohGeRiO3m5kfxOehC1?p=preview

AngularJS: is there a difference between $transclude local in directive controller and transclude parameter in link function?

I implemented a directive that transcludes multiple fragments of child content into a template. It works but seems simpler than most of the examples I have seen, and raised a few questions about how transclusion works.
Here is the directive:
module.directive('myTransclude', function() {
return {
restrict: 'A',
transclude: true,
replace: true,
scope: true,
template: '<div style="border: 1px solid {{color}}"><div class="my-transclude-title"></div><div class="my-transclude-body"></div></div>',
link: function(scope, element, attrs, controller, transclude) {
// just to check transcluded scope
scope.color = "red";
transclude(scope, function(clone, scope) {
Array.prototype.forEach.call(clone, function(node) {
if (! node.tagName) {
return;
}
// look for a placeholder node in the template that matches the tag in the multi-transcluded content
var placeholder = element[0].querySelector('.' + node.tagName.toLowerCase());
if (! placeholder) {
return;
}
// insert the transcluded content
placeholder.appendChild(node);
});
});
}
}
});
and here is example usage:
<div ng-controller="AppController">
<div my-transclude>
<my-transclude-title> <strong ng-bind="title"></strong>
</my-transclude-title>
<my-transclude-body>My name is {{name}} and I've been transcluded!</my-transclude-body>
</div>
</div>
You can see it in action in this fiddle.
Please notice a few things:
It matches fragments to template placeholders by element class, rather than explicitly defined child directives. Is there any reason to do this one way or another?
Unlike many examples I've seen, it doesn't explicitly use the $compile service on the child fragments. It seems like Angular is compiling the fragments after transclusion, at least in this simple case. Is this always correct?
It uses the (barely documented) transclude argument to the link function, rather than the other (barely documented) approach of injecting the $transclude local into the controller directive. After hearing so many admonitions not to manipulate DOM in controllers, the controller approach seems like an odd construct and it feels more natural to handle this in the link function. However, I tried it that way and it seems to work the same. Is there any difference between the two?
Thanks.
EDIT: to partially answer question #2, I discovered that you do need to explicitly compile transcluded content that is not cloned from the template where the directive was applied. See the difference in behavior here: http://jsfiddle.net/4tSqr/3/
To answer your question in regards to the differences between $transclude function in directive controller vs linking function, first we need understand that $transclude function can be accessed through directive compile, controller and linking functions.
UPDATE: According to 1.4 Angular documentation, compile(transclude) has been deprecated! So transclude function can only be accessible in your directive Controller or Linking function. (See official documentation for detail explanation)
There is a big difference when used $transclude in compile phase vs. $transclude in controller and linking phase due to during compiling phase, you don't have access to $scope vs. using in controller and linking functions where $scope (controller) and scope (linking) are accessible. With that said, the only difference in using $transclude in directive controller vs. linking is order of execution. For multiple nested directives, its relatively safe to use $transclude during your linking phase instead of using it in your controller.
The order goes like this:
parentDirectiveCompile -> childDirectiveCompile (Directive Compile)
parentDirectiveControllerPre, parentDirectiveControllerPost -> childDirectiveControllerPre, childDirectiveControllerPost (Directive Controller)
childLinkFunction -> parentLinkFunction
Notice how childLinkFunction gets executed first before parentLinkFunction? (Order of execution)
Helpful Resource:
Angular directives - when and how to use compile, controller, pre-link and post-link
Hopefully this answer can be helpful to you!
after some investigation:
After the release of Angular 1.20 pre-existing child nodes of a compiled directive that has a isolate scope will no longer inherit the new isolate scope because they have already been assigned to the parent scope. So... the built in transclude method that uses the attribute ng-transclude will only transclude templates to the desired location in this case but will not transclude pre-existing html to this location. This means that if you had a directive with an isolate scope and you wanted the html that was already there to be compiled to the new isolate scope you would need to use the transclude linker function inside of the directive.
You can see a working case of this problem here ui-codemirror placed within custom directives fails without an error

using ng-show in directive template

I am trying to define a directive to show a checkmark when a question has been answered.
questModule.directive('showCheckmarkOnDone', function () {
return {
transclude: true,
template: "<div ng-transclude></div><img src='img/checkmark.svg' " +
"ng-show='scope.questions[type][number-1].done' " +
"class='checkmark' style='height: {{height}}px; width: {{height}}px;'>",
link: function (scope, element, attrs) {
scope.height = $(".app").height()/12;
scope.number = attrs.number;
scope.type = attrs.type;
}
};
});
The scope.questions[type][number-1].done exists in my controller for the page, and I can see that it is being updated correctly when I press the done button. However, the directive does not register the change. I tried putting a $watch in the link function - that didn't help either. I think I'm a bit confused about how to get my directive scope to play nicely with my controller scope - any thoughts on how I can give this directive access to an object that exists in an outside controller? (scope.questions)
This is not a valid way to define directive scope:
scope: '#'
You can either (a) not define it, (b) set it to true, or (c) set it to {} (an object). See the docs for more info: http://docs.angularjs.org/guide/directive (find the header: "Directive Definition Object")
In this case, I imagine if you remove it, you may be OK, because it will allow scope.questions to be visible from your directive. If you reproduce the issue in jsfiddle.net or plnkr.co, it would be much easier to assist you.
Edit:
Your directive generally should have 1 parent element
You should not use scope in your directive's HTML, it's implied
I think you said as much in your comment, but you should strive to make your directive more generic by passing in scope.questions[0][0].done instead of looking it up in your directive's HTML using attributes.
Here's a working example: http://jsfiddle.net/EZy2F/1/

How to use ng-show on a directive that has an isolated scope

I have a directive that I use like this:
<dir model="data"></dir>
The directive has an isolated scope.
scope :{
model:'='
}
Now I'm trying to use ng-show on that directive using another attribute of my page's $scope, like this:
<dir ng-show="show" model="data"></dir>
But it's not working because the directive is trying to find the show attribute on its own scope.
I don't want the directive to know about the fact that its container might choose to hide it.
The workaround I found is to wrap the directive in a <div> and apply ng-show on that element, but I don't like the extra element this forces me to use:
<div ng-show="show" >
<dir model="data"></dir>
</div>
Is there a better way of doing this?
See this plunker: http://plnkr.co/edit/Q3MkWfl5mHssUeh3zXiR?p=preview
Update: This answer applies to Angular releases prior to 1.2. See #lex82's answer for Angular 1.2.
Because your dir directive creates an isolate scope, all directives defined on the same element (dir in this case) will use that isolate scope. This is why ng-show looks for property show on the isolate scope, rather than on the parent scope.
If your dir directive is truly a standalone/self-contained/reusable component, and therefore it should use an isolate scope, your wrapping solution is probably best (better than using $parent, IMO) because such directives should normally not be used with other directives on the same element (or you get exactly this kind of problem).
If your directive doesn't need an isolate scope, your problem goes away.
You could consider migrating to Angular 1.2 or higher. The isolate scope is now only exposed to directives with a scope property. This means the ng-show is not influenced by your directive anymore and you can write it exactly like you tried to do it in the first place:
<dir ng-show="show" model="data"></dir>
#Angular-Developers: Great work, guys!
Adding the following into the link function solves the problem. It's an extra step for the component creator, but makes the component more intuitive.
function link($scope, $element, attr, ctrl) {
if (attr.hasOwnProperty("ngShow")) {
function ngShow() {
if ($scope.ngShow === true) {
$element.show();
}
else if($scope.ngShow === false) {
$element.hide();
}
}
$scope.$watch("ngShow", ngShow);
setTimeout(ngShow, 0);
}
//... more in the link function
}
You'll also need to setup scope bindings for ngShow
scope: {
ngShow: "="
}
Simply use $parent for the parent scope like this:
<dir ng-show="$parent.show" model="data"></dir>
Disclaimer
I think that this is the precise answer to your question but I admit that it is not perfect from an aesthetical point of view. However, wrapping the <div> isn't very nice either. I think one can justify it because from the other parameter passed to the isolate scope, it can be seen that the directive actually has an isolate scope. On the other hand, I have to acknowledge that i regularly forget the $parent in the first place and then wonder why it is not working.
It would certainly be clearer to add an additional attribute is-visible="expression" and insert the ng-show internally. But you stated in your question that you tried to avoid this solution.
Update: Won't work in Angular 1.2 or higher.

Resources