Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I think createEmbeddedView() is injecting TemplateRef in wrong location. #9035

Closed
bennadel opened this issue Jun 6, 2016 · 32 comments

Comments

@bennadel
Copy link

commented Jun 6, 2016

I'm submitting a bug report.

Current behavior

When a TemplateRef is passed into another component and then stamped-out using the ViewContainerRef, it appears to be injecting the TemplateRef relative to the original location, not relative to the current ViewContainerRef. I noticed this when trying something on my own. However, when I came across this behavior, I went and looked at the NG2 specs and it seems that your tests actually show the same behavior.

I took your specs and pasted them into a Plunkr: https://plnkr.co/edit/z6LBzUPkcfPHznGz66IW?p=preview

Notice that the TemplateRef (green border) is being rendered next to the ViewContainerRef (red border). I think the tests still pass because they are only checking the text output of the resultant view, not the actual DOM structure. The text output happens to still be correct, in this case, but the hierarchy is wrong .... I think.

Expected/desired behavior

I believe that the TemplateRef should be injected into the ViewContainerRef.

Please tell us about your environment:

Running on Chrome Version 50.0.2661.102 (64-bit)

Angular version: 2.0.0-beta.17 (Plunkr) and RC1 (locally) -- both show the same behavior.

Language: TypeScript

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 6, 2016

@bennadel are you saying that createEmbeddedView() is inserting next to location instead of into location?

I was always under the impression that view containers are marking insertion points next to an element with witch a given view container is associated. So for me the behavior in the linked plunk seems good. Still @tbosch could confirm.

In any case I agree that it would probably make sense to update tests to show expected behavior explicitly.

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@pkozlowski-opensource it is very possible that I am mistaken in the intent. I am just learning this stuff. That said, it seems odd to pass the template into another directive only to have that template be rendered outside said directive. It seems a bit misleading.

@zoechi

This comment has been minimized.

Copy link
Contributor

commented Jun 6, 2016

You can acquire a ViewContainerRef using @ViewChild('target', {read: ViewContainerRef}) target:ViewContainerRef;(set when ngAfterViewInit() is called) from template: ['<div #target></div>. If you use this one it will be added inside the component. See also http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 (uses createComponent)

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@zoechi I am a bit confused; how can I use a ViewChild when the directive that is receiving the template has no View (it's just an attribute directive in this case).

Also, to be clear - are you saying the behavior is expected? Or, are you just giving me a work-around?

@ericmartinezr

This comment has been minimized.

Copy link
Contributor

commented Jun 6, 2016

@bennadel that's the expected behavior, in fact you're not the only one confused about it, check #8529 where RouterOutlet uses ViewContainerRef + ComponentResolver. But yes, it is like when you used loadNextToLocation/loadIntoLocation with DCL.

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 6, 2016

@bennadel so here is my mental model, hope it is not going to get destroyed as the result of the discussion in this issue :-)

A View is a bunch of elements (more complex than this but will do for the discussion here). Each element can have an associated ViewContainer. I think about it as a hook (a physical one) attached to an element on which you can "hang" other views. It also happens that elements can have associated directives as well.

Now when you are injecting a ViewContainerRef in a directive you are saying: I want to potentially attach something to you, give me a container (a hook) associated with an element on which I'm living. So you are injecting a ViewContainer associated with an element, directives are not creating view containers, just interact with them.

Then if you insert something into a ViewContainer those things show up as siblings to an element with which a ViewContainer is associated with. This would makes sense since you could ask for a ViewContainer for an element that don't have content (ex. <hr>). Didn't try but I would expect it to work.

Now, I hope that @tbosch will chime-in and put right words on our confused terminology / explanations.

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@pkozlowski-opensource ah, very interesting. I need to let that bounce around in my head a bit more. Would it matter if the item in the *ngFor is a component vs. attribute directive? Meaning, would:

<div *ngFor="..." myDirective></div>

.... be the same as:

<my-directive *ngFor="..."></my-directive>

Can both of those assume that they are not their own view container?

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 6, 2016

@bennadel regarding your latest example, I think it is easier to see it if you de-sugar * syntax which would give you:

<template ngFor ...>
    <div myDirective></div>
</template> 

and

<template ngFor ...>
    <my-directive></my-directive>
</template> 

In both cases a view container to which you insert things created by ngFor are attached to the <template> element so what goes into a template and which directives are present inside a template doesn't really matter.

Once again, IMO it helps to think about elements to which you can attach other views, directives etc. At least this is my mental model. And of course I might be totally wrong :-)

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@pkozlowski-opensource re: the Attribute directive vs. Element directive, I guess I was just trying to get at the fact that one as a View associated with it, and only only uses the the content from another view.

To think about this from internal to a component, then, imagine I have this as a component:

@Component({ .... })
export class SomeComponent {
    constructor( vc: ViewContainerRef ) { ... }
}

.... is the vc in this case never the host element no matter what? Is the vc basically the parent of the SomeComponent instance?

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 6, 2016

@bennadel so assuming that your SomeComponent has a selector of <some-component> and you use it in HTML you are going to inject VCRef associated with the <some-component> element.

guess I was just trying to get at the fact that one as a View associated with it, and only only uses the the content from another view.

Not sure I understand this part :-)

@tbosch

This comment has been minimized.

Copy link
Contributor

commented Jun 6, 2016

@pkozlowski-opensource your mental model is right. In the implementation we optimize a bit in that we only create a ViewContainerRef where users ask for them via queries or DI.

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@pkozlowski-opensource Yes, exactly - SomeComponent is an element, <some-component>. A more fleshed out pseudo code might look like this:

@Component({
    selector: "some-component",
    inputs: [ "extraTemplate" ],
    template: "This is a nice component. Yay!"
})
export class SomeComponent implements AfterViewInit {
    constructor( public vc: ViewContainerRef ) { ... }

    ngAfterViewInit() {
        this.vc.createEmbededView( this.extraTemplate, {} );
    }
}

Basically, if some Component is injected with a Template, it needs a way to then render that template in its own View, which I assumed you would do via the ViewContainerRef. But, from what you're saying, I think, the ViewContainerRef is never the host element - it's always the container that the host element is also in?

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 6, 2016

But, from what you're saying, I think, the ViewContainerRef is never the host element - it's always the container that the host element is also in?

I would rather say it like this: "A ViewContainer injected into a given directive (regardless if this is a component or a simple directive) is the one associated with an element on which a given directive matched".

Think about like this: elements has pointers to ViewContainers. If you insert something into a ViewContainer it will be added as a sibiling of a given element (remember you might have elements without content). A ViewContainer can be associated with a given element even if there is no directive on this element. So in this respect presence or not of a directive (simple directive or a component) doesn't change anything and somehow is not relevant.

I hope that we can close this one now that Tobias confirmed that it works as intended / explained here. But we can surely continue the discussion - it is important for me that the mental model is clear to everyone as in the end this quite simple system.

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@pkozlowski-opensource So, going back to the StackOverflow item you posted earlier, it sounds like there's no way to dynamically render an injected templateRef without creating an "inner DOM element" to give you something to query for the ViewContainerRef.

It would be nice if I we could use an HTML comment as a ref:

template: "<!-- #anchor -->"

... and then query for that:

queries: {
    anchor: new ViewChild( "anchor", { read: ViewContainerRef } )
}

Anyway, I appreciate all of the insight. For what its worth, as the first time thinking about this stuff, it's not quite so simple :) Especially coming from Angular 1.x where, I could transclude content and just .append() it to the host element. But, I know the two systems are very different. Thank you for taking the time to help. I really appreciate it.

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 6, 2016

It would be nice if I we could use an HTML comment as a ref:

You could use a <template> element for this, ex.: <template #insertSthHere> and query for insertSthHere (reading ViewContainerRef from it). I think it should work.

or what its worth, as the first time thinking about this stuff, it's not quite so simple :)

I understand. We just need to have a nice doc with drawings showing all this. It is a really simple system - it is just I don't think it was explained anywhere.... I might take a stab at a blog post....

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 6, 2016

@pkozlowski-opensource Ok, one last clarifying question. In the following code:

<div *ngFor="...."></div>
<div *ngFor="...."></div>

.... are both of those ngFor directives part of the same ViewContainerRef? Or, is there a ViewContainerRef created individually for each of them? From your earlier comment, it sounds like they each have their own based on the de-sugared template:

<template [ngFor]>
    <div></div>
</template>
<template [ngFor]>
    <div></div>
</template>

So, in this case, as you mentioned earlier, there are two ViewContainerRef instances, one associated with each template ....

@pkozlowski-opensource

This comment has been minimized.

Copy link
Member

commented Jun 7, 2016

So, in this case, as you mentioned earlier, there are two ViewContainerRef instances, one associated with each template ....

Yes, this is correct.

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 7, 2016

@pkozlowski-opensource thank you for all your help. I think I actually got something working:

http://bjam.in/ng2-custom-renderer

Using something like this:

<dynamic-repeater [items]="colors">

    <template #itemRenderer let-color="item" let-index="index">
        <div title="Item {{ index }}" class="swatch" [style.backgroundColor]="color.hex">
            <br />
        </div>
        <div class="name">
            {{ color.name }}
        </div>
    </template>

</dynamic-repeater>

Here, I'm "providing" a <template> to the <dynamic-repeater> as a ContentChild. The repeater is then using the queried TemplateRef to clone internally.

@sergey-koretsky

This comment has been minimized.

Copy link

commented Jun 12, 2016

wow! @bennadel you just saved weaks for me.
Pretty sure that we really need some low level architecture explanations how @angular/core/linker and compiler works together, design docs which will explain all that relations between AppElement, AppView, how and when the core creates all that parts, how they bind and correlate one to each other and directives rendering process explained (like this one) just to understand that 'mental model' which is so important for smth more than the heroes tutorial...

@sergey-koretsky

This comment has been minimized.

Copy link

commented Jun 12, 2016

by the way https://github.com/angular/material2/tree/master/src/components - just like the best tutorial how to create your custom components ) but in any case its not enough to understand that internal angular 2 mechanics and all that 'View and friends' stuff

@bennadel

This comment has been minimized.

Copy link
Author

commented Jun 12, 2016

@sergey-koretsky thanks for the kind words! And awesome link. I'll have to check that out. The last time I heard about Material Design and NG2 was a while back. It's gotta be a goldmine of great approaches.

@born2net

This comment has been minimized.

Copy link

commented Nov 4, 2016

n an effort to gain deeper knowledge into Angular 2 I wish someone would create an in depth explanation / tutorial on the underlying structure of components, directives and their containers and views.

As per the docs:

The component’s container can contain two kinds of Views. Host Views, created by instantiating a Component via createComponent, and Embedded Views, created by instantiating an Embedded Template via createEmbeddedView. The location of the View Container within the containing View is specified by the Anchor element. Each View Container can have only one Anchor Element and each Anchor Element can only have a single View Container. Root elements of Views attached to this container become siblings of the Anchor Element in the Rendered View.
This leaves many open questions, such as: a Host view is referring to the element that Component resides in, and an Embedded view is referring to the component’s template itself?

Is that true for both cases when created manually (via createComponent) as well as when created declaratively via in another hosting component (parent)?

Is that the case for Directives as well which don’t have a template (thus no view)? And how all this works in a Shadow dom environment (browser actually support a component host) vs an emulated environment?

Angular 2 does do a lot of magic and in an effort to become an expert I wish to better understand, (maybe via a visual diagram) the entire relationship of: ViewContainerRef, Host views, Templates, Embedded Template, ViewChild, ViewContainer and their hierarchy of components and directives.

I consider myself extremely well versed in Angular2 (delivered 2 huge project already) but still feel I have holes in my understand of the underline internal workings.

Sure you don’t need to know how a car works to drive one, but you handle it much better if you do,

I agree with @sergey-koretsky we really need a low level, well put tutorial from the Angular team so we can better understand all the relationships!

Thanks as always,

Sean

@sergey-koretsky

This comment has been minimized.

Copy link

commented Nov 22, 2016

i found more better solution to this and all other NG2-related problems, here is it http://vuejs.org/
and https://laracasts.com/series/learning-vue-step-by-step just as a quick intro and concepts overview

@born2net

This comment has been minimized.

Copy link

commented Nov 22, 2016

please remove post otherwise will be marked as spam, not related

@emonidi

This comment has been minimized.

Copy link

commented Mar 16, 2017

ViewContainerRef sounds to me as a Reference to the View Container. View Container to me sounds like a container that contains the view. Hare is what the official documentation says: "Represents a container where one or more Views can be attached". Probably the only thing that gives up the actuall behavior is the word attached (other wise it would be inserted) I think that the name of the method and the documentation are very missleading having in mind that dynamic component creation is something very common.

@chaoyangnz

This comment has been minimized.

Copy link

commented May 27, 2017

+1
confusing names... not inituitive

@RoyiNamir

This comment has been minimized.

Copy link

commented May 28, 2017

Also asked here .
https://stackoverflow.com/questions/44202566/viewcontainerref-createembeddedview-clarification

After digging (a lot) - I find that the word "container" is misleading
Anchor is much more appropriate.
Not to mention the clear() method. Clear what ? what content ?
But if you actually see the de-sugared effect - you can clearly see that content is "appended" to the end of the template section.

@emonidi

This comment has been minimized.

Copy link

commented May 28, 2017

(y)

@chubbard

This comment has been minimized.

Copy link

commented Jun 1, 2017

The TLDR; answer is ViewContainerRef.createEmbeddedView() adds the Template as a sibling to the target according to the index, but ViewContainerRef.createComponent() adds it as a child of the target as expected. That alone makes you go WAT? I did not see any real workarounds described above to this confusing behavior. Or really the only workaround is to have an empty element inside what you WANT to insert into as your target. Uggh!

If I have a target I always mean to insert it into that target, not add along side it. If I wanted to insert along side then I'd have set the parent as my target. Sure someone can do bonehead things like trying to insert into an hr element, but that's just plain wrong. If that's true then designing this by trying to prevent people from doing stupid things ended up just made things super confusing and doing something that is not expected. I'd prefer doing what is expected rather than protecting me from myself.

I wish this feedback would be taken seriously and reconsider the design of createEmbeddedView to follow the principle of least surprise, but I think it might be too late.

@RoyiNamir

This comment has been minimized.

Copy link

commented Jun 9, 2017

@chubbard Why do you way that ? createComponent ALSO adds it as a sibling
you can look here : https://plnkr.co/edit/ViRuxeqQ7CuVGqFbtJ8y?p=preview . It is a s a sibling(!) to the template anchor. ( I've added another two wrapping DIVS just for reference).

@angular-automatic-lock-bot

This comment has been minimized.

Copy link

commented Sep 11, 2019

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 11, 2019

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
You can’t perform that action at this time.