3

I have a child component which accept a TemplateRef as an @Input and displays it through ngTemplateOutlet. How can I use @ViewChild/@ViewChildren to retrieve components inside the template ?

If the template is declared in the same component that where it is used, @ViewChild/@ViewChildren are working but it makes the component very less dynamic.

I also tried using @ContentChildren but it makes no difference.

I created a stackblitz in order to reproduce. Here are some of the code:

child.component.html
<ng-container #fromParent [ngTemplateOutlet]="template"></ng-container>
child.component.ts
@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css'],
})
export class ChildComponent implements AfterViewInit {
  @Input() template: TemplateRef<unknown>;
  @ViewChildren(HelloComponent) hello = new QueryList<HelloComponent>();

  ngAfterViewInit() {
    console.log('Has hello', this.hello.length > 0);
  }
}
parent.component.html
<ng-template #tmp> <hello name="{{ name }}"></hello> </ng-template>
<app-child [template]="tmp"> </app-child>

The log in child component returns false.

Here is the stackblitz: https://stackblitz.com/edit/angular-eopzyw?file=src/app/app.component.ts

Thank you.

MHogge
  • 5,408
  • 15
  • 61
  • 104
  • I might be completely incorrect for you context, but I guess you can make use of [Content-projection](https://angular.io/guide/content-projection), also, have a look at this question [angular: access ng-content nested component method from outside](https://stackoverflow.com/a/54304745/1417185) – Paritosh Oct 26 '21 at 08:54
  • It's not shown in my example but in my context the template should be reusable (in my usecase I use a `ngTemplateContextOutlet`). So I don't think `Content-projection` would do the trick as `ng-content` can only be used once unlike `ng-template`. But maybe I'm missing something obvious here? – MHogge Oct 26 '21 at 09:09

2 Answers2

3

This is interesting, at first look, I too thought that it should be accessible, but did some debugging and found that:

The parent template is part of the parent component host view and hence the ViewChildren in child component will not be able to access it as it is not the part of child component host view.

Look at the below picture, app component has a host id 165 and the parent ng template gets associated with it.

enter image description here

So it appears that in the current design of ViewChildren it does not support querying the templates passed from a host view. May be opening a feature request will be a good idea.

Ritesh Waghela
  • 3,474
  • 2
  • 19
  • 25
  • 1
    Thanks for the analyze. I created a feature request at https://github.com/angular/angular/issues/43946. Let's see what will happen. – MHogge Oct 26 '21 at 06:50
0

Based on what @Ritesh Waghela discovered I found a way to get the views inside the template but is has some drawbacks.


So because only the parent knows the existence of those views, it's his work to get their references.

parent.component.ts
@ViewChildren(HelloComponent) public helloComponents = new QueryList<HelloComponent>();

The child will then get a reference to the parent thanks to Injector.

child.component.ts
private parent: ParentComponent;
constructor(private injector: Injector) {
   this.parent = injector.get<ParentComponent>(ParentComponent);
}

Because the @ViewChildren of the parent will take all the HelloComponent, in template or not, we need to differentiate them. For this I did not find any better solution than adding an attribute inside HelloComponent.

hello.component.ts
@Input() fromTemplate = false;

The parent component can distinguish HelloComponent inside template or not:

parent.component.html
<ng-template #tmp> 
  <hello name="inside template" fromTemplate="true"></hello>
</ng-template>

<hello name="outside template"></hello>
<app-child [template]="tmp"> </app-child>

The child component can now filter parent's views to get what it is searching for.

child.component.ts
ngAfterViewInit() {
  const viewFromTemplate = this.parent.helloComponents.filter(
    (comp) => comp.fromTemplate
  );
}

This solution is working but it has a few drawbacks:

  • The child must know the type of its parent;
  • The parent must declare each view inside the template as being a "view from template";

The 1st drawback is not a huge issue in my case (it can be worked around because all of my components are contains inside a wrapper -> I take the reference to the known wrapper which can get a reference to the component it host).

But the 2nd drawback seems more of an issue to me. Does anyone knows if a view can know weither it is part of a ng-template or not ? I welcome any suggestion on this.


I created a stackblitz to demonstrate my solution: https://angular-qgwcdf.stackblitz.io.

MHogge
  • 5,408
  • 15
  • 61
  • 104