Illegal use of ngTransclude directive in the template
Solution 1
The reason is when the DOM is finished loading, angular will traverse though the DOM and transform all directives into its template before calling the compile and link function.
It means that when you call $compile(clone.children()[0])(scope)
, the clone.children()[0]
which is your <panel>
in this case is already transformed by angular.
clone.children()
already becomes:
<div ng-transclude="">fsafsafasdf</div>
(the panel element has been removed and replaced).
It's the same with you're compiling a normal div with ng-transclude
. When you compile a normal div with ng-transclude
, angular throws exception as it says in the docs:
This error often occurs when you have forgotten to set transclude: true in some directive definition, and then used ngTransclude in the directive's template.
DEMO (check console to see output)
Even when you set replace:false
to retain your <panel>
, sometimes you will see the transformed element like this:
<panel class="ng-scope"><div ng-transclude=""><div ng-transclude="" class="ng-scope"><div ng-transclude="" class="ng-scope">fsafsafasdf</div></div></div></panel>
which is also problematic because the ng-transclude
is duplicated
To avoid conflicting with angular compilation process, I recommend setting the inner html of <panel1>
as template or templateUrl property
Your HTML:
<div data-ng-app="app">
<panel1>
</panel1>
</div>
Your JS:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>",
}
});
As you can see, this code is cleaner as we don't need to deal with transcluding the element manually.
Updated with a solution to add elements dynamically without using template or templateUrl:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
template:"<div></div>",
link : function(scope,element){
var html = "<panel><input type='text' ng-model='firstName'>{{firstName}}</panel>";
element.append(html);
$compile(element.contents())(scope);
}
}
});
If you want to put it on html page, ensure do not compile it again:
If you need to add a div per each children. Just use the out-of the box ng-transclude
.
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div><div ng-transclude></div></div>" //you could adjust your template to add more nesting divs or remove
}
});
DEMO (you may need to adjust the template to your needs, remove div or add more divs)
Solution based on OP's updated question:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
replace:true,
transclude: true,
template:"<div ng-transclude></div>",
link: function (scope, elem, attrs) {
elem.children().wrap("<div>"); //Don't need to use compile here.
//Just wrap the children in a div, you could adjust this logic to add class to div depending on your children
}
}
});
Solution 2
You are doing a few things wrong in your code. I'll try to list them:
Firstly, since you are using angular 1.2.6 you should no longer use the transclude (your linker function) as a parameter to the compile function. This has been deprecated and should now be passed in as the 5th parameter to your link function:
compile: function (element, attr) {
return function (scope, element, attr, ctrl, linker) {
....};
This is not causing the particular problem you are seeing, but it's a good practice to stop using the deprecated syntax.
The real problem is in how you apply your transclude function in the panel1
directive:
parent.prepend($compile(clone.children()[0])(scope));
Before I go into what's wrong let's quickly review how transclude works.
Whenever a directive uses transclusion, the transcluded content is removed from the dom. But it's compiled contents are acessible through a function passed in as the 5th parameter of your link function (commonly referred to as the transclude function).
The key is that the content is compiled. This means you should not call $compile on the dom passed in to your transclude.
Furthermore, when you are trying to insert your transcluded DOM you are going to the parent and trying to add it there. Typically directives should limit their dom manipulation to their own element and below, and not try to modify parent dom. This can greatly confuse angular which traverses the DOM in order and hierarchically.
Judging from what your are trying to do, the easier way to accomplish it is to use transclude: true
instead of transclude: 'element'
. Let's explain the difference:
transclude: 'element'
will remove the element itself from the DOM and give you back the whole element back when you call the transclude function.
transclude: true
will just remove the children of the element from the dom, and give you the children back when you call your transclude.
Since it seems you care only about the children, you should use transclude true (instead of getting the children() from your clone). Then you can simply replace the element with it's children (therefore not going up and messing with the parent dom).
Finally, it is not good practice to override the transcluded function's scope unless you have good reason to do so (generally transcluded content should keep it's original scope). So I would avoid passing in the scope when you call your linker()
.
Your final simplified directive should look something like:
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.replaceWith(clone);
});
}
}
});
Ignore what was said in the previous answer about replace: true
and transclude: true
. That is not how things work, and your panel directive is fine and should work as expected as long as you fix your panel1
directive.
Here is a js-fiddle of the corrections I made hopefully it works as you expect.
EDIT:
It was asked if you can wrap the transcluded content in a div. The easiest way is to simply use a template like you do in your other directive (the id in the template is just so you can see it in the html, it serves no other purpose):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv' ng-transclude></div>"
}
});
Or if you want to use the transclude function (my personal preference):
app.directive('panel1', function ($compile) {
return {
restrict: "E",
transclude: true,
replace: true,
template: "<div id='wrappingDiv'></div>",
link: function (scope, element, attr, ctrl, linker) {
linker(function (clone) {
element.append(clone);
});
}
}
});
The reason I prefer this syntax is that ng-transclude
is a simple and dumb directive that is easily confused. Although it's simple in this situation, manually adding the dom exactly where you want is the fail-safe way to do it.
Here's the fiddle for it:
Related videos on Youtube
![Alborz](https://i.stack.imgur.com/SemVg.png?s=256&g=1)
Alborz
I'm a software developer. Do not hesitate to contact me via email. alborzmgm[@]gmail.com
Updated on September 14, 2021Comments
-
Alborz almost 3 years
I have two directive
app.directive('panel1', function ($compile) { return { restrict: "E", transclude: 'element', compile: function (element, attr, linker) { return function (scope, element, attr) { var parent = element.parent(); linker(scope, function (clone) { parent.prepend($compile( clone.children()[0])(scope));//cause error. // parent.prepend(clone);// This line remove the error but i want to access the children in my real app. }); }; } } }); app.directive('panel', function ($compile) { return { restrict: "E", replace: true, transclude: true, template: "<div ng-transclude ></div>", link: function (scope, elem, attrs) { } } });
And this is my view :
<panel1> <panel> <input type="text" ng-model="firstName" /> </panel> </panel1>
Error: [ngTransclude:orphan] Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element:
<div class="ng-scope" ng-transclude="">
I know that panel1 is not a practical directive. But in my real application I encounter this issue too.
I see some explanation on http://docs.angularjs.org/error/ngTransclude:orphan. But I don't know why I have this error here and how to resolve it.
EDIT I have created a jsfiddle page. Thank you in advance.
EDIT
In my real app panel1 does something like this: <panel1> <input type="text> <input type="text> <!--other elements or directive--> </panel1>
result =>
<div> <div class="x"><input type="text></div> <div class="x"><input type="text></div> <!--other elements or directive wrapped in div --> </div>
-
Maxim Shoustin over 10 yearsDid you try to ad to
panel
directive:require: "^panel1"
? -
Alborz over 10 yearsThese two directives are not dependent. But i add require: "^panel1" to panel directive for test and the error does not disappear.
-
Khanh TO over 10 yearsCheck my updated answer with 1 more solution for
add a div per each children.
-
Khanh TO over 10 yearsI added a solution to solve your problem in Edit section
-
-
Alborz over 10 yearsThank you for explanation that was helpful. But my panel1 don't have any template and i add its children by traversing the DOM. Is there any solution to have nested container directives? Actually i manipulate the html elements in panel1.
-
Khanh TO over 10 years@Alborz: this is similar to using template, just create
new
html element inside the link function and add it to the panel1 and use $compile to compile it. Check my updated answer with DEMO -
Khanh TO over 10 years@Alborz: the point is don't put it on the html file as it will be compiled by angular which may conflict with our compilation.
-
Khanh TO over 10 years@Alborz: If you want to put it on html page, ensure do not compile it again. Check my updated answer with 1 more demo.
-
Alborz over 10 yearsSome directives can not be created using template. for example ng-repeat don't use template. What can we do for this kind of directives. Note that panel and panel1 can be used independently and sometimes panel1 should not have panel inside.
-
Alborz over 10 yearsYou don't set the transclude property of panel1. So linker function is undefinded in your last demo.
-
Khanh TO over 10 years@Alborz: I forgot to remove it, actually, we don't need linker in this case
-
Khanh TO over 10 years@Alborz: I don't see your problem. If you add ng-repeat inside
<panel1>
, it will be compiled correctly by angular. You just need to ensure that youdo not compile an html that is already compiled by angular.
-
Khanh TO over 10 years@Alborz: panel and panel1 can be used independently and sometimes panel1 should not have panel inside. Yes, you could put almost anything inside panel1. (don't need to be panel)
-
Alborz over 10 yearsAssume that i want to wrap each children in a div. Is it possible? You don't change the children in your sample.
-
Daniel Tabuenca over 10 yearsThere's no reason why you can't nest directives like that. If that were true, a lot of things would not work.
-
Alborz over 10 yearsIs it possible to wrap it in a div without using the templates? i mean by manipulating the children. See my last edit.
-
Daniel Tabuenca over 10 yearsSure, you can create a div node append the dom to it and then replace the element with it. The only limitation is that the div should not have any directives on it. If you need the div to have directives you need to create the div, compile it, link it, and THEN append the transcluded dom to it.
-
Khanh TO over 10 years@dtabuenc: You just need to ensure that you do not compile again (there are a bunch of solution to this problem). See my last DEMO, I also pointed out that it could be nested.
-
Khanh TO over 10 years@Alborz:
The key is that the content is compiled (I already said that in my answer)
. About the solutions, there are a bunch of solutionsas long as you do not compile again the already compiled DOM
. It's hard to say which is the best one. The best solution should be based on what problem you're trying to solve.(should not based on personal preference). I pointed out a bunch of solutions including template, create DOM inside link function, nest directive,.. and let the OP decide which one fits his requirements. -
Alborz over 10 yearsSo this is my main problem. panel1 may contain other directives And I need to compile it. Because i add a div per each children. any solution?
-
Daniel Tabuenca over 10 yearsYou will need to better explain (perhaps update your fiddle). What do you mmean by "add a div for each children"? This doesn't seem like a good approach. But I'm not sure I really understand what you mean yet.
-
Daniel Tabuenca over 10 yearsAlso, what I said is that the "DIV" you are adding shouldn't have any directives on the div itself (i.e. you can't do <div ng-click="doSomething()">. But it's ok to wrap it in a div even if you are wrapping other elements that do have directives.
-
Alborz over 10 yearsThank you @Khanh. In your last demo if i use elem.children()[index].wrap("<div>") raise error!! I want to wrap each child in a div. So i iterate over the children.
-
Khanh TO over 10 years@Alborz:
elem.children()[index]
returns a DOM object which does not havewrap
function. If you need to iterate, useelem.children().eq(index)
instead -
Khanh TO over 10 years@Alborz:
elem.children().wrap("<div>")
in my demo already wraps each child in a div. You don't need to iterate, you need to iterate only when you need to wrap differently for each element. -
jonhobbs almost 8 yearsHey @dtabuenc , I realise this is bad form on SO but I'm kinda desperate. You seem to understand the ins and outs of transclusion really well. I don't suppose you could look at my question could you? stackoverflow.com/questions/38332873/…
-
surya over 4 yearsYour personal preference worked for me!instead of ng-transclude i used linker(function (clone) { element.append(clone); });