transclude: 'element' is useless without replace:true #3368

Closed
ProLoser opened this Issue Jul 26, 2013 · 31 comments

Projects

None yet
@ProLoser
Contributor

Version without replace:true: http://plunker.co/edit/RqAszav0khUoAoPIDS7n?p=preview

Version with replace:true: http://plunker.co/edit/IbobWmG5e9OlgjJmgVKm?p=preview

Note that in the first, you only get a comment tag. Is this deliberate? Perhaps replace:true should automatically be set? Or maybe an error should be thrown? Are we expected to do the transclusion ourself if we don't have replace turned on?

@rodyhaddad
Contributor

This is not really a bug, in the sense that it is the expected behavior.

A comment node gets created for any directive having transclude: 'element' and replaces the actual element node in the DOM (this is deliberate, it's the best solution really).

If you have a template with no replace: true, the compiler will try to inject the template into the element using .innerHTML, but since doing aCommentNode.innerHTML = someHTML doesn't do anything really, nothing happens.

I wonder if some browsers complain when we try and set the innerHTML on aCommentNode. Was this tested?

Maybe a better behavior would be to throw an error when you have transclude: 'element' combined with a template or templateUrl but no replace: true? It would help beginners for sure.
And maybe the error wouldn't be thrown if you have replace: false, which wouldn't actually do a replace, but would indicate that you are aware of what you're doing.

@pocesar
Contributor
pocesar commented Apr 14, 2014

spent some days trying to figure out how to get the templateUrl when doing transclude: 'element' without setting replace: true. at least should be explained in the docs, it's somewhat a trial and error

@caitp
Contributor
caitp commented Apr 14, 2014

I'm not sure I understand why you want to use element transclusion with a template, that doesn't really make sense to me (you probably don't need to use "element transclusion" at all, for any reason, because the main use-cases for it are things like ng-repeat and ng-if, which are implemented in core... you might want to use it to implement something similar to ng-view, but remember that ng-view itself doesn't use a template!)

@rodyhaddad
Contributor

@caitp something like http://plnkr.co/edit/M4HKmtU4IL5By7Sv3QQH?p=preview ?

I think for some users of angular, the difference between transclude: 'element' and transclude: true is minor/doesn't exist, design-wise.

@caitp
Contributor
caitp commented Apr 14, 2014

@rodyhaddad for a use-case like that, there's no point using transclude: 'element' in the first place, because it's essentially just a regular template directive.

Element transclusion has a specific use-case, which to me largely involves things which are sort of structural "placeholders" in the DOM, and don't have templates for this reason.

I can't think of a good reason to use transclude: 'element' with a template, it simply doesn't make any sense.

@pocesar
Contributor
pocesar commented Apr 14, 2014

I use it mainly with ng-include to reuse the same controller without having to set <div ng-controller="NCtrl as ctrl"> every single time, plus I use an isolate scope, since transclusion uses the "not imediate parent" scope, I need to transclude it to the scope I chose

@caitp
Contributor
caitp commented Apr 14, 2014

This doesn't strike me as a compelling reason to use element transclusion, it sounds more like not really assessing the alternatives. Element transclusion has a specific use case, and that use case does not include the use of templates. This is based on my own personal observation and understanding of the framework, but I'm fairly confident that this is not what you want to do.

@partap
Contributor
partap commented Apr 14, 2014

@caitp Hmm. Could you explain how @rodyhaddad 's example is an invalid use-case for transclude: element'?

I could see how it might be useful to be able to wrap arbitrary html with an arbitrary template like that. Are you are implying that in this case he should have used transclude: true and just wrapped the entire <select>?

If that is the case, what is a good example of when to use transclude: 'element'?

edit: never mind...I just noticed you mentioned ng-if and ng-repeat earlier....

@pocesar
Contributor
pocesar commented Apr 14, 2014

also discarding part of the included template is perfectly fine, and you can fine grain it using transclude: 'element' which I think is a fine use case, plus the ability to reorder the transcluded items along with the template content. If I wanted to have 5 empty nested divs, I wouldn't be willing to use transclude:'element' along with other core directives (ng-repeat, ng-transclude, ng-include, etc)

@caitp
Contributor
caitp commented Apr 14, 2014

transclude: true is useful if: You have a template, and you want to also include child nodes from the application template. For instance:

<my-component>
  <p>Transclude me into the my-component template</p>
  <p>Also transclude me</p>
</my-component>

This might turn into something like:

<div class="my-component">
  <div class="section">
    <h1>Comments:</h1>
    <p>Transclude me into the my-component template</p>
    <p>Also transclude me</p>
  </div>
  <div class="section">
    <!-- other material ... -->
  </div>
</div>

The myComponent directive here might say "okay, put child nodes in a specific part of my template. So transclude: true is more about saying "I want to put child-nodes from the calling template into my own template, at an arbitrary place"


transclude: 'element' is different. It's really all about place-holders, like this:

<p ng-if="foo !== '123'">foo is not equal to '123'!</p>

This gets translated into:

<!-- ngIf: foo !== '123' -->

And the expression gets evaluated. When the expression is truthy, ngIf appends the original DOM node (the transcluded element, where "element transclusion" comes from) to the parent node of the comment, after the comment itself.

So the comment is acting as a placeholder for the actual node, which gets placed after it.


So based on these two use-cases for different kinds of "transclusion", what is the purpose in giving an "element transclusion" directive a template? What problem is it solving? Why does it need element transclusion, when it's not actually using it?

These are things which don't really make any sense, which is why this issue raises a lot of eyebrows from me. It seems to be nonsense. None of this has anything to do with which scope is used, or what scope is inherited from. It's just weirdness, if you ask me. There are much more applicable ways to solve the problem you're trying to solve.


Again, I'm not really speaking for anyone, this is my personal opinion, but I'm not convinced that there is any legitimate benefit to using element transclusion for the posted examples, they simply don't need it, and aren't using it.

@btford
Contributor
btford commented Apr 14, 2014

@caitp 👏

@caitp
Contributor
caitp commented Apr 14, 2014

It was a very well-written speech, wasn't it.

Anyways Igor said that yeah, element transclusion is mainly used for special "structural" directives (which mainly are in core, although occasionaly you might see them elsewhere like ui-router), and it's kind of bogus to use a template with them.

Maybe it would be helpful to throw an error when you try to do that, to give a hint that it's not really what's supposed to happen. I'm not sure.

But I think we can close this now!

@caitp caitp closed this Apr 14, 2014
@btford
Contributor
btford commented Apr 15, 2014

I'm trying to think how we can add this to the docs.

@marknutter

I have a scenario where I want to transclude the element and use a template as well. Specifically, I want to wrap an input field in a template and I want that input's scope to be preserved, but I don't want to wrap that input in a separate element where the directive is applied. So I have

<input ng-model="test" my-directive />

and I have a template like

<div><label>value: {{test}}</label><div ng-transclude></div></div>

And I want to be able to access the input's ngModelCtrl from my directive (which I can) but I want the input field's ng-model to continue referring to the main controller's scope. I can't figure out any other way to do this other than using transclude:'element' and replace:true, but the transcluded element's scope gets messed up in the process.

@caitp
Contributor
caitp commented Apr 28, 2014

@marknutter why not just use ordinary transclusion for that? transclude: true and then, to keep your original scope, just transclude it with the original scope. It works perfectly well!

As has been mentioned in this thread, element transclusion is intended for some fairly niche use cases that are, for the most part, covered in core (via ngIf, ngRepeat, ngView --- but occasionally 3rd party libraries make use of it as well (ui-router's ui-view directive for instance). But generally you don't need to use it.

It's not clear where the idea that element transclusion is the right solution for these use cases is coming from, but it doesn't really accomplish anything at all, so it's a bit confusing :>

@marknutter

@caitp Doesn't ordinary transclusion require that I specify my directive on an element that wraps the elements I want to transclude? In my example, I don't want to have to wrap my input tag with another element to transclude. I want to transclude the very input tag I'm applying my directive to.

@caitp
Contributor
caitp commented Apr 28, 2014

@caitp Doesn't ordinary transclusion require that I specify my directive on an element that wraps the elements I want to transclude?

I see what you're saying. However, this is not what element transclusion is intended for, so this is really a hack. Just use regular transclusion instead.

@marknutter

So is there a way to transclude the very element a directive is applied to without using transclude: 'element' or is that not supported and/or encouraged?

@jamie-pate

It sounds like transclude: 'element' is an internal feature and should probably have the warning/error messages when used with incompatible settings. (or at least mentioned in the help text)

Thinking about this, if you really wanted to be able to do this directly on the input element, you could just write a custom compile function that did exactly what you want. Trying to wedge this into the internal functionality is probably not a good idea, and I appreciate the philosophy that the internals should be protected from featuritis. :) It keeps the core angular.js running fast for all of us and keeps it simpler to maintain.

@anton-107

@caitp, what I understood from this discussion is that transclude: "element" is an undocumented feature and shouldn't be used for the purpose of wrapping an original element with a new one. But what is the official best way to achieve this then?

To be exact: imagine i have an <input type="checkbox" my-checkbox ng-model="..." />. In my-checkbox directive i want to wrap this input with a div. Search engines eventually led me to this discussion, and unfortunately it has no clear answer. Regular transclusion doesn't work in this case and transclude: "element" works, but not 100% as i would like: although it transcludes the checkbox, it also copies all the attributes of the original checkbox to the root element of the directive.

@jamie-pate

I'd suggest a new directive that does this specifically (not hard to do inside the compile fn). Not sure if there is one somewhere but it'd be nice to have an official unofficial addon directory for directives such as this that seem like they'd be useful for lots of people, but aren't/won't be included in angularjs directly. Another one I would like to see is ng-with

@pocesar
Contributor
pocesar commented Jun 4, 2014

@jamie-pate ngmodules.org is the unofficial modules repository for angular I guess, haven't seen any other

@JasonStoltz

@caitp Wouldn't this be a valid use case for tranclude: element and template (and actually, replace for that matter)?

Imagine a "type ahead" directive (ex. http://twitter.github.io/typeahead.js/) that decorates an arbitrary input element:

<input type="text" ng-model="somevalue" placeholder="placeholder text" id="input" name="input" type-ahead>

I would want that to become:

<div class="typeahead">
  <input type="text" ng-model="somevalue" placeholder="placeholder text" id="input" name="input" >
  <ul class="dropdown">
    <li>selection 1
    <li>selection 2
    <li>selection 3
  </ul>
</div>

Which naturally seems like it would be set up like:

{
  transclude: 'element',
  replace: true,
  template: '<div class="typeahead">' +
    '<transclude></transclude>' +
    '<ul class="dropdown">' +
      '<li>selection 1' +
      '<li>selection 2' +
      '<li>selection 3' +
    '</ul>' +
  '</div>';
}

It would need to be replace and wrapped with a container in order to provide correct positions for the dropdown element, relative to the input element.

@TheLostBrain

Hey all

I know this is a closed issue but for those who are struggling to find alternative solutions to this I'd like to share what I've come up with.

So I made two working demos of how this type of directive 'self-wrapping' behavior can be accomplished without using transclude: 'element' (actually, without having to use transclusion at all really).

This one is a relatively comprehensive example performed on an element directive:
http://plnkr.co/edit/FeXrKv1lh4qlTtNqilPi?p=preview

This one a simpler example performed on an attribute directive
(It's mostly just a follow up to JasonStoltz's post above.)
http://plnkr.co/edit/XDOXidCBx7irrsfgdbpr?p=preview

For those who didn't find it obvious (it sure wasn't for me) I hope this helps.

If anyone out there with more experience (caitp, btford, you?) could provide a more performant (or just plain better) solution please share!

@Izhaki
Contributor
Izhaki commented Oct 4, 2014

@caitp

First, I don't mean to upset you by re-opening the discussion.

Second, I've just spent an hour trying to figure out with transclude: 'element' isn't working. I wouldn't call myself frustrated, but I ain't a happy bunny at the moment either.

Wrapping again

Like others, I wish to wrap the element on which my directive is declared. In my case, I have a row that can either be a normal table row, or a handle row of a tree node. If it's the former - nothing to be done, but if the children directive is declared on that row, I wish to insert after it a children-container and wrap the two with a node directive. Just another example why using transclude: 'element' with a template seem to make sense.

The alternative of doing it myself via compile, or in the link function using $complie or $transclue seems a very long-winded solution in comparison to achieve the same thing. What's more, when you use templates you have the insurance that things will work properly no matter what. But as soon as you start doing things yourself all these questions pop:

  • But what if another directive like ng-if is declared on the element?
  • Will it work with ng-repeat?
  • What if terminal is involved?
  • etc. etc.

All these are question that show lack of thorough understanding of the compiler - which is a really monstrous piece of code. I doubt anyone understand it at all, let alone as well as the Angular team.

The JQuery example

No consider the following:

JQuery decided to provide a transclude function, with the following syntax:

$( element ).transclude( html, contentsOnly )

Then the docs go like this:

transclude:

  1. removes either the contents of the element (if contentsOnly is true) or the whole element (if contentsOnly is false or undefined);
  2. replaces it with the html markup provided;
  3. searches the html for an element with the tag reinject-here;
  4. and replaces the reinject-here element with the content removed in step 1.

OK, now you try the following:

$( 'input[required]').transclude( '<div><reinject-here/>* <div>' )

And... your input element has gone - not showing anymore.

That's pretty much what happens in angular at the moment.

And then you raise an issue with the JQuery team and they reply is: But that's not how we use it internally, so using it like this isn't right.

Not convinced

I don't quite get why limit transclude+element to 'placeholders' only. Said who? And why? Just because that's how it is used at the core at the moment? Is there a technical complexity preventing it from working with templates?

To me, like others, this seems like a great feature, and one that is clearly in-demand (to wrap elements), and the docs say nothing about the fact that this wouldn't work.

Essentially, I'm sorry but I'm not buying your argument. If you'd provide an example where implementing this can mess things up, I'd have to take it I guess. But I'm not convinced thus far.

Update the docs

Anyhow, for time being, the docs require urgent update to boldly say that transclude+element cannot be used with a template. I'm sure you'll acknowledge how confusing it is when tranclude+true require a template.

@caitp
Contributor
caitp commented Oct 4, 2014

element transclusion is about placeholders/structural directives (eg ng-if, ng-switch, ng-repeat, ng-include, etc). Very few developers have much of a reason to use it, and when using them with things like templates / replace: true, they're certainly using them wrong. The idea is, your element becomes a placeholder, and has the ability to re-add itself to the DOM (either by transcluding itself numerous times, or by conditionally transcluding itself in and removing itself based on some condition, etc). This is not something that you would use for a "component" or a control of some kind, it's just a way of describing a particular structural behaviour.

I'm sure the docs could be improved, but I don't think it would really mitigate the confusion. (note: speaking for myself here, can't speak for anyone else).

@Izhaki
Contributor
Izhaki commented Oct 4, 2014

Well... lighters were never designed to open beer bottles, but people still find them extremely useful for that.

I assume replace is no longer with us.

And at least half of transclude (the true half) was clearly designed to work with templates.

If you say that the other half (the element one) will cause troubles - I'll take your word for it. But I hope you see better why such a feature would seem useful and justified.

@caitp
Contributor
caitp commented Oct 4, 2014

transclude: true is certainly suitable for templates. element transclusion is entirely different (and in angular.dart, has a completely different name and API which prevents confusion between the two, probably a good thing!)

@djindjic
Contributor

I'm one of a lot of us who came here via google, and after few days trying to clear tranclusion to myself I found that tranclude: 'element' should be mention in documentation as unsupported feature or what. There is to many search engine references to element tranclusion use cases which just make confusion to beginners as I am.

@caitp
Contributor
caitp commented Dec 12, 2014

it's not an unsupported feature. It has a specific purpose, and that purpose seems to be misunderstood by people.

I'm trying to explain this: element transclusion results in a placeholder element being put in the DOM, and directives with element transclusion are linked with that placeholder element --- their transclude function will give you a clone of the original template, which for instance ngRepeat uses to render the list of repeated elements, or ngIf uses to insert or remove the template following the placeholder node.

If you (or anyone) is interested in submitting a docs fix to clarify this placeholder behaviour, that would be awesome.

@djindjic
Contributor

there was an info about transclude: 'element' but it's moved to /src/ng/compile.js 1 year ago:

e69c287#diff-a732922b631efed1b9f33a24082ae0dbR252

@VWoeltjen VWoeltjen added a commit to nasa/openmct that referenced this issue Oct 28, 2015
@VWoeltjen VWoeltjen [Representation] Use transclusion for mct-include
...to add/remove conditionally depending on the existence of
certain templates.

Note that this currently breaks mct-include due to an incompatibility
between element transclusion and directive templates; see
angular/angular.js#3368.
942f617
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment