I've created directive that with certain attrs has different HTML.
Directive looks like this:
<my-dir param='{{first-template}}'> <my-dir param='{{second-template}}'>
This directive is used several times on site with different params.
I can't use templateUrl: function (element, attrs) {} to load different HTML because it is loaded only once - at begginning of site render.
Is there a solution for that or should I construct new directives?
Related
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
I am probably vastly simplifying the use of directives, but to me it seems like templateUrl in directives are much like partials in that you load reusable templates with <directivename></directivename> much like <%= render 'partial' %>.
Here's an example of something I just wrote before coming up with this question:
app.directive('filters', function() {
return {
restrict: 'E',
templateUrl: "../templates/product-filters-template.html"
}
});
app.directive('results', function() {
return {
restrict: 'E',
templateUrl: "../templates/product-results-template.html"
}
});
That feels identical to cutting a piece of ERB into a _partial.html.erb file.
Are directives used in any other way?
Using directives to render the same markup (same concept as a partial) is one usage, but they can also (and more frequently in my limited experience) be used to control behavior of an element. From the documentation on directives:
What are Directives?
At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS's HTML compiler ($compile) to attach a specified behavior to that DOM element or even transform the DOM element and its children.
For example, you can use a directive to do validation of a user's input into an input element. I would strongly suggest that you read the documentation on directives.
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
Working on a directive to highlight <code> tags that are being outputted by a directive to render <markdown> tags.
The problem is that the <code> directive is never hit after the <markdown> directive runs. However the <code> directive runs on code tags that are not outputted from the <markdown> directive.
The Markdown directive
angular.module('App').directive "markdown", ->
converter = new Showdown.converter()
scope: true
restrict: 'E'
link: (scope, element, attrs) ->
if attrs.markdown
scope.$watch attrs.markdown, (newVal) ->
html = converter.makeHtml(newVal)
element.html(html)
else
redraw = ->
html = converter.makeHtml(element.text())
element.html(html)
#### expecting the code directive to be trigger after this.
scope.$on "$includeContentLoaded", redraw
redraw()
Any thoughts?
AngularJS doesn't know to compile anything from your markdown. There are two ways to get that stuff to compile. One way is using transclusion, but I don't think that will work for your usecase. In your case, you will need to compile the changes from your markdown. To do this, you use Angular's $compile service.
first, inject the $compile service into your directive. Then after setting element.html(html), try this: $compile(element.contents())(scope);
I've just begun creating Angular directives (I'm new to the framework, as well), but am running into issues wherein a nested directive seems to be ignored. The basis for my directives' code is UI Bootstrap's "tabs" and "pane" directives.
The gist is that I want to be able to compile a list of "components" inside a "layout". Ultimately, there should also be an attribute on each "component" tag that will instruct the layout to render content from some known template location. For now, however, I can't even get the "link" function inside the component directive to fire, even though I've got two components in my template.
Here's a plunk of my situation:
http://plnkr.co/edit/K4n2Mx3kZyvVYGDyJ7t9
You're mis-using ngTransclude by placing it inside of ngRepeat. It's sort of a chicken/egg situation where since there's nothing to repeat over, nothing gets transcluded.
Also, since you're specifying components in HTML, you don't even need ngRepeat in your template.
http://plnkr.co/edit/aYjdd4skbKC3FEM3lCfY?p=preview
template:
'<section class="layout">' +
'<h4>Before all components</h4>' +
'<div ng-transclude></div>' +
'<h4>After all components</h4>' +
'</section>'
When you use ng-repeat, it creates a new scope and it makes you ng-transclude not in the right scope for injecting the transclusion.
So when you remove the ng-repeat, you get the rendered components.
Now, in order to control the layout, you can either add the elements to the controllers like you do with their scope, and then layout them accordingly in the controller:
// inside the controller
this.addComponentElement = function (componentElement) {
componentElements.push(componentElement);
};
// watch for array changes and handle layout
Or, you can use the transclude function in the compile + link combination to get a reference to the transcluded dom and manipulate its layout:
compile:function(telement, tAttrs, transcludeFn){
return function(scope, element, attrs){
transcludeFn(scope, function(transcludedDom){
// layout the transcludedDom
})
}