angular: ContentChildren doesn't get children created with NgTemplateOutlet

Original title: ViewChildren doesn’t get children created with NgTemplateOutlet

Please see comments below…

I’m submitting a …

[x] bug report

Current behavior When you use ngTemplateOutlet to render a template passing by input property ViewChildren don’t get or don’t find in the redered elements.

Expected behavior I think ViewChildren should get the elements rendered by ngTemplateOutlet.

Minimal reproduction of the problem with instructions In this plunkr, on app.ts file, you can see a Child component and a Parent component, Parent component has an input property called template1, this accept a TemplateRef that is rendered using ngTemplateOutlet. Parent component has a ViewChildren collection to get the Child components and shows the length of this collection in the end of template.

Well, using this component this way:

    <ng-template #template1>
        <child></child>
        <child></child>
    </ng-template>
    <parent [template1]="template1"></parent>

The Parent component render the two Child components but the length of ViewChildren collection is 0. You can uncomment the line 16 of app.ts file to include a static <child/> component in the Parent template. Then the collection length is 1.

What is the motivation / use case for changing the behavior? It’s would be very useful to pass template to components, rows of grids, lines of lists, … In the other hand I have this plunkr in this case I am rendering Child components using a ngFor, ViewChildren works fine. Since both, ngTemplateOutlet and ngForOf, using viewContainerRef.createEmbeddedView to render the templates … why works with ngFor and not with ngTemplateOutlet? I think that the only reason is that Child components come from different component context, but I would like to know it.

Please tell us about your environment: Windows 10, VisualStudioCode, npm, lite-server

  • Angular version: 4.0.0-rc2

  • Browser: [all]

  • Language: [TypeScript 2.2.1]

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 119
  • Comments: 31 (16 by maintainers)

Most upvoted comments

@tolemac I believe that there is some confusion about what @ContentChildren means. In Angular content projection and queries it means “content as it was written in a template” and not “content as visible in the DOM” as you seem to expect.

@pkozlowski-opensource I understand @ContentChildren means in Angular. Is there any way to get the elements projected by a template?

If I have this:

[appComponent]
<ng-template #myTemplate>
  <div>content for other component</div>
</ng-template>
<my-good-component [template]="myTemplate"></my-good-component>

[my-good-component]
<ng-container *ngTemplateOutlet="template"></ng-container>

How to get the template content from my-good-component???

In addition …

[appComponent]
<ng-template #myTemplate let-object>
  <input [(ngModel)]="object.name"/>
  <input [(ngModel)]="object.surname"/>
</ng-template>
<my-generic-person-form [editTemplate]="myTemplate"></my-generic-person-form>

[my-generic-person-form]
<form>
<ng-container *ngTemplateOutlet="editTemplate; context: personObject"></ng-container>
</form>

How is it possible that in version 7 of Angular it is still not possible to make this work? I can not believe it 😄

I have a list of issues that are more than two years open where this is requested or discussed …

@tolemac I believe that there is some confusion about what @ContentChildren means. In Angular content projection and queries it means “content as it was written in a template” and not “content as visible in the DOM” as you seem to expect.

You should think about content nodes as additional input to the component (same as other @Inputs) - a component can use its content to drive its template rendering. As such content is kind of contract between a component author and and a component user. A component author says “hey, if you use me with certain kind of nodes I can make use of them (project, query etc.)”. A component user (meaning: one using a component in a template) must understand the contract and provide (or not) nodes fulfilling a given contract. In other words the component usage place is where a component author and its user agree on the content nodes.

The consequence of this mental model is that Angular only looks at the nodes that are children in a template (and not children in the rendered DOM). To calculate queries and content projection it is enough to inspect a template only without even inserting this content, ex.:

<child></child> <!-- this one doesn't match as not written between parent tags -->
<parent>
   <child></child> <!-- this one DOES match as is written between parent tags -->
</parent>

Your situation is similar, you just happen to have an additional template but the principles are the same:

<ng-template #tplWithChild>
   <child></child> <!-- this one doesn't match as not written between parent tags -->
</ng-template>
<parent>
   <child></child> <!-- this one DOES match as is written between parent tags -->
</parent>

the fact of inserting or not #tplWithChild doesn’t change anything - it is still outside of parent tags!

<ng-template #tplWithChild>
   <child></child> <!-- this one doesn't match as not written between parent tags -->
</ng-template>
<parent>
   <child></child> <!-- this one DOES match as is written between parent tags -->
   <ng-template [ngTemplateOutlet]="tplWithChild"></ng-template> <!-- the act of inserting a template doesn't change anything - it is _definition_ place that counts! -->
</parent>

Having said all this the good news is that I think that you can achieve what you want by moving template to be an actual child:

<parent>
  <ng-template #tplWithChild>
     <child></child> <!-- this one DOES match when this template gets instantiated -->
   </ng-template>
   <child></child> <!-- this one DOES match as is written between parent tags -->
   <ng-template [ngTemplateOutlet]="tplWithChild"></ng-template>
</parent>

Here is a working stackblitz derived from your plunker: https://stackblitz.com/edit/angular-ebgnch?file=src%2Fapp%2Fapp.component.ts

I fully acknowledge that this can be confusing and I would love to make it easier on everyone. Let me know if the proposed solution works for your use-case and if the above explanation makes sense.

I will keep this issue open for a bit to see how we could better model / document this.

Perhaps it isn’t a bug, and it’s so by design.

I have tried to get children included by ngTemplateOutlet by two ways.

  1. Using ViewChildren and TemplateRef Input property rendering childs with ngTemplateOutlet (this issue).
  2. Using ngTemplateOutlet to render to the content and using ContentChildren:
@Component({
    selector: "my-compo",
    template: `
    <parent>
        <ng-container *ngTemplateOutlet="template1"></ng-container>        
    </parent>
    
    `
})
export class MyComponent {
    @Input() template1: TemplateRef
}

@Component({
    selector: "parent",
    template: `
    ...    
    `
})
export class Parent {
    @ContentChildren(Child) children: QueryList<Child>()
    ...
}

And I was searching about that and asking on stackoverflow (Q1 and Q2)

As you can see I’m thinking about this feature since 6 months ago. I think I haven’t written this issue prematurely.

The only way to get it, have been to use native elements.

@Component({ selector: "child", ...})
expor class Child {
    constructor(elem: ElementRef) {
        elem.nativeElement.component = this;
    }
}

@Component({ selector: "parent", ...})
expor class Parent {
    _items = [];
    constructor(private elem: ElementRef) { }
    
    ngDoCheck() {
        const childElements = this.elem.nativeElement.getElementsByTagName("child");
        const childItems = [];
        for (let i = 0; i < childElements.length; i++) {
            childItems.push(childElements[i].component);
        }

        this._items = childItems;
    }
}

If someone knows a better way to do it, I would be very grateful.

Thanks 😉

Any news on that? It could be very helpful to be able to query for stuff projected into components this way.

6 years and we dont have any way, how to query elements from dynamically created things…

I hope it will work AT LEAST with new @defer()…

Any news on this issue?

I have tried the workaround explained in the @edpeng comment but it does not work in my use case, maybe because of some implementation details of the PrimeNG Tree (I don’t know).

But I have found a very strange behaviour: at first when I check @ViewChildren in the @ngOnAfterViewInit it contains the children components (7), but after that the array becomes empty. I’m not sure, but it seems like the parent templates overwrite the child view.

image

(first variable is the @ViewChildren and second is the @ContentChildren)


EDIT: I have tried saving the initial @ViewChildren in a variable so I can manipulate it later, but it does not the trick because the change detection and template references get broken.

The only workaround I found is manipulating the children components with @ContentChildren { descendants: true } in the parent component, but it is not a clean solution considering the parent component should be able to set custom templates and these should be used in the child component.

Anyway it has a strange behaviour also, as the @ContentChildren is empty at first in the ngOnContentInit and then is filled with the projected templates. It does not make sense to me.

The good news is that at least it works and the user interface is in sync.

Definitely we would need an straightforward solution for this in Angular.

Encountered similar issue in Angular v6.0.0. Found this workaround based on subscribing to QueryList changes - similar to what @robertbrower-technologies suggested? I had to add an extra step to call ngDoCheck() because subscribe didn’t work when using *ngIf:

<div *ngIf="isPageLoaded">
<parent [template1]="template1"></parent>
</div>
export class Parent implements OnInit, AfterContentInit, DoCheck  {
    @ContentChildren(Child) children: QueryList<Child>()
    ...
    ngAfterContentInit(): void {
        console.log('expecting length to be initially 0. length:' + children.length);
        this.children.changes.subscribe( c => console.log('length is now:' + children.length);
    }
    ngOnInit(): void {
       setTimeout(() => {console.log('timer ran doCheck()');this.ngDoCheck()}, 0);
    }

  ngDoCheck(): void {
    console.log('ran docheck');
  }
}

@feco93 For future reference he got the component reference by assigning it in the child’s constructor: constructor(elem: ElementRef) { elem.nativeElement.component = this; }