When building advanced reusable UI patterns in Angular—such as dynamic tables, wizards, or custom template outlets—you might try to attach a directive to an <ng-template> and query its projected children using contentChildren() (or @ContentChildren). However, you quickly notice that the query returns empty.

Why contentChildren Doesn't Work Inside ng-template

Your intuition is spot on: elements inside an <ng-template> do not exist in the DOM when the parent directive is initialized.

An <ng-template> acts as an inert blueprint. Angular does not instantiate its contents until an *ngTemplateOutlet or ViewContainerRef.createEmbeddedView() is executed. Furthermore:

  • Template Boundaries: Angular's content queries (contentChild / contentChildren) only look at direct or descendant projected nodes within the host's compile-time view hierarchy. They do not cross into uninstantiated TemplateRef declarations.
  • Multiplicity: A single <ng-template> can be rendered zero times, once, or multiple times (as shown in your example where you stamp it out twice with contexts 1 and 2). A static content query on the directive attached to <ng-template> cannot know how many runtime instances of <app-testchild> will exist.

Solution 1: Self-Registration via Dependency Injection (Recommended)

Because child components inside an embedded view inherit the injector of the <ng-template>, the cleanest and most modern approach in Angular (v16+) is self-registration via Dependency Injection.

Instead of querying from the top down with contentChildren, allow the child component to inject the parent directive and register itself upon instantiation.

1. Update the Directive to Expose a Registry Signal

import { Directive, inject, TemplateRef, signal } from '@angular/core';
import type { TestchildComponent } from './testchild.component';

@Directive({
  selector: '[app-testparent]',
  standalone: true
})
export class TestparentDirective {
  readonly template = inject(TemplateRef);
  
  // Maintain a reactive list of active child component instances
  private readonly _children = signal<TestchildComponent[]>([]);
  readonly children = this._children.asReadonly();

  register(child: TestchildComponent) {
    this._children.update((list) => [...list, child]);
  }

  unregister(child: TestchildComponent) {
    this._children.update((list) => list.filter((c) => c !== child));
  }
}

2. Inject the Parent Directive in the Child Component

import { Component, Input, inject, OnInit, OnDestroy } from '@angular/core';
import { TestparentDirective } from './testparent.directive';

@Component({
  selector: 'app-testchild',
  standalone: true,
  template: `<p>testchild works! data: {{ data }}</p>`
})
export class TestchildComponent implements OnInit, OnDestroy {
  @Input() data!: number;

  // Optional: inject parent directive if present in the template injector hierarchy
  private parentDirective = inject(TestparentDirective, { optional: true });

  ngOnInit() {
    this.parentDirective?.register(this);
  }

  ngOnDestroy() {
    this.parentDirective?.unregister(this);
  }
}

With this approach, every time an *ngTemplateOutlet stamps out a new instance of your template, the newly created <app-testchild> automatically registers itself to the directive, and it cleanly unregisters when destroyed.


Solution 2: Passing Context Directly to Children via Template Variables

If your primary goal is to provide data from the parent component to the child component via the ng-template context, you can expose implicit or named template variables directly:

<app-test>
  <ng-template app-testparent let-itemData>
    <app-testchild [data]="itemData"></app-testchild>
  </ng-template>
</app-test>

When combined with your ngTemplateOutlet bindings:

<ng-container *ngTemplateOutlet="testParent()?.template ?? null; context: { $implicit: 1 }"></ng-container>
<ng-container *ngTemplateOutlet="testParent()?.template ?? null; context: { $implicit: 2 }"></ng-container>

Each stamped instance of <app-testchild> will automatically receive the correct context value without needing any parent query.

Summary

  • contentChildren cannot cross <ng-template> boundaries because templates are compiled into separate embedded views instantiated on demand.
  • Use DI-based self-registration when a directive needs access to dynamic component instances created by templates.
  • Use template input variables (let-*) when you only need to pass context data down to projected template children.