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
Related
Is there a right way to apply custom directive to HTML template based on some condition
Eg: <li my-custom-directive="{{item}}">
I need to apply "my-custom-directive" only if {{item}} is defined.
This feels like a design problem rather than a technical one.
Rather than apply the custom directive conditionally, simply figure out what to do inside the directive. Semantically, this makes more sense.
For instance, if item is undefined in this case, simply don't do something inside the directive.
Use ng-if, DOM is not inserted until condition is met.
AngularJS leaves a comment within the DOM for its reference,
so <li my-custom-directive="{{item}}"> would not be within the DOM at all until {{item}} is defined.
If you need to add directives dynamically to the DOM from a variable, use $compile provider. I've created myself a directive for such things
angular.module('test', []).directive('directiveName', ['$compile', function($scope) {
return {
link: function($scope, element, attrs, ctrl) {
element.replaceWith($compile(attrs.direciveName)($scope))
}
}
}]);
And you can use it as such:
<div directive-name="{{customDirectiveName}}"></div>
{{customDirectiveName}} being a $scope variable from somewhere else. From this point you could ng-repeat on JSON objects recieved from server, ect.
It depends on your requirement , if you use it has as element instead of attribute you can achieve using ng-if.
for ex in the below code li wouldnt appear in the dom as and when item is undefined,
<my-custom-directive="{{item}}" ng-if="item">
<li>{{item}}</li>
</my-custom-directive>
I have a reusable template called profile.html. It looks something like this:
<div>
{{firstName}}
</div>
I have it embedded in another template which is bound to a dedicated controller:
<div ng-include src="'templates/profile.html'"></div>
I want a child $scope created for this div. In the controller for the parent template, I have something like:
$scope.profile = theProfile;
I want the child scope for the profile.html template to be the parent $scope.profile. Something akin to:
<div ng-include src="'templates/profile.html'" ng-scope="{{profile}}"></div>
How can I do this?
It looks like you're basically reinventing directives, by trying to set both the template and scope like that. Also, $scope is an object with a large amount of other properties/objects on it, so setting it to a be another object would be... problematic.
The following would create a directive that merges a passed in profile to the $scope using angular copy, if you really want to do it that way. I'd recommend just using a $scope.profile, though.
.directive('profile', [function(){
return{
templateUrl:'templates/profile.html',
scope:{profile:'='},
controller: function($scope){
angular.copy($scope.profile, $scope) // if you really, really want the properties right on the scope.
}
}
}]
ngInclude automatically creates a child scope. You shouldn't need to explicitly pass some data to it since it can access its parent scope via prototypical inheritance (this might become a problem if your template changes the scope).
The problem here is that your template expects a firstName property to exist in the scope, but it doesn't. So you could change your template to
<div>
{{profile.firstName}}
</div>
but that would couple the template to the profile object, which might be a bad idea.
Another solution would be to manually create the firstName property in the correct scope:
<div ng-include src="'templates/profile.html'"
ng-init="firstName=profile.firstName">
</div>
I'm not very fond of this solution, though, because it can easily get out of hand if the template needs more properties and it breaks the template encapsulation to some extent.
And finally, you could wrap that template within a directive:
directive('whateverMakesSense', function() {
return {
restrict: 'E',
template: '<div>{{data.firstName}}</div>',
scope: { data: '=' }
};
});
...
<whatever-makes-sense data="profile"></whatever-makes-sense>
If you find yourself using that template in many places, I suggest you go for the custom directive approach. It will give you more control, things will be better encapsulated and as a bonus your markup will be more semantic - if you use anything but whatever-makes-sense, of course. :)
I am having troubles implementing a custom directive with transclude: true that can have a "transclusion" content that is using ngRepeat.
[My case]
I want to implement a set of directives, that are fetching the data that they are supposed to show from a $http service. For that I want to use preLink phase interceptor that Angular provides, so I can catch the data and set it to the scope. That way if I have some dynamic (since this term is well overloaded - I mean a data which structure is unknown until the request is done) data coming from the service, I rely on that, that I will be able to retrieve a list with that dynamic data and store it inside the scope, then loop through that data via ngRepeat inside the HTML. Here comes my problem...
[My Problem]
Angular is not using the list that I am assigning to the scope during preLink.
[A plunkr]
I maded a plunker that illustrated just the problem that I am having.
http://plnkr.co/edit/XQOm4KWgKxRhn3pOWqzy?p=preview
[My question]
I really believe that such functionality is covered by angular and I am just missing something in the puzzle.
Can anyone tell me how to implement such behaviour?
Thanks!
EDIT:
Thank you rchawdry for your answer. Here are some details on my intentions. To make it simple I will try to give you an example.
Let's assume that we have these directives:
1. "page" - This directive is a labeled container for all the page content. Visually it is represented as some div - for header, for content and for other fancy stuff if needed. The directive does not know what is its data before the page loads. As the page loads the directive must retrieve the information for itself and its children from a REST resourse! Then the directive is setting the information needed for itself (label and other stuff) and stores its children content in childrenList scope variable. It creates a scope.
2. "section" - This section can be child of "page". Since "page" is retrieving its data from a server, then the information about how many "section"s does our "page" have is dynamic and we don't know how many "section"s we need to show on the screen. This depends on sectionList that is coming from the back-end. The section itself is almost the same as "page" - it is a labeled container, with the differences that - a). "section" is container of elements; b). "section" does retrieve its data from its parrent instead of making $http request. This directive creates a scope.
3. "element" - For this example, in order not to define many different elements and complicate it, let's assume that I have one element, called "element". It can consist of some "input" with "span" and "button" if needed. It is similar to the "section" with that, that it retrieves the data to show from it's parrent (in the general case, this is "section" or "page"). On the other hand it is different than "section" by the fact that it has no transcluded content.
Now after we have some of the concept here is what I am trying to achieve:
<page>
<element id='element1' someOtherStuffHere...></element>
<section id='static_section1' someOtherStuffHere...>
<element id='element2' someOtherStuffHere...></element>
</section>
<div class="row" ng-repeat="section in sections">
<section dynamic_id='dynamic_section'>
<div class="row" ng-repeat="elem in elements">
<element dynamic_id='dynamic_element'></element>
</div>
</section>
</div>
</page>
well, I believe that what your trying to achieve will be able by adding a ng-repeat attribute to the transcluded template.
by letting angular know about the 'repeat', it is supposed to work.
since plunkr is currently unavaliable, I can't prodivde any preview and do not have your original code. Ill try to recall it:
template: "<div id='container'>" +
"<div class='content' ng-repeat='item in [1]' ng-transclude'></div>" +
"</div>"
edit: http://plnkr.co/edit/xba4pU666OGxBtKtcDwl?p=preview
You've got a scope problem. The controller is using a variable that isn't defined in the controller (arrayListItemsPre and arrayListItemsPost). While they are declared in the directives, accessing them in a transcluded scope is a little tricky.
The easy way is to do the following. This will present the scope variables up to the controller where they can be used.
app.directive('container', function($compile) {
return {
transclude: true,
scope: true,
replace: true,
restrict: 'E',
template: "<div class='container'>" +
"<div class='content' ng-transclude></div>" +
"</div>",
compile: function(cElem, cAttrs) {
return {
pre : function preLink(scope, iElement, iAttrs) {
console.log("Hello from Container preLinkPhase");
scope.$parent.arrayListItemsPre = [1, 2];
},
post : function postLink(scope, iElement, iAttrs) {
scope.$parent.arrayListItemsPost = [1, 2];
}
};
}
};
});
There are other ways to do this that are better but it requires understanding why you're trying to iterate on variables that are defined in the directive. If you're going to be using this directive in another page that has different array elements, you'd have to change the directive code.
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
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.