Angular中@ContentChildren在<ng-template>下失效问题咨询
Is this behavior expected?
Absolutely, this is expected behavior. Let me break down why:
@ContentChildren is designed to query components that are content-projected into your component—meaning they're placed directly between the opening and closing tags of your component (like your first working example where you nest <app-tree> tags manually).
When you use ngTemplateOutlet to render components recursively, those child components are part of the view tree of the parent component, not content-projected into it. They're generated dynamically by the template, not passed in as external content via <ng-content>. That's why @ContentChildren can't pick them up—they're outside the scope of what it's meant to query.
Solution: Switch to @ViewChildren with Lifecycle Hooks
To fix this, you need to use @ViewChildren instead. This decorator queries components that are part of the current component's own view, including those rendered dynamically by structural directives like *ngFor or *ngTemplateOutlet.
Here's how to adjust your TreeComponent:
Updated Component Code
import { Component, Input, ViewChildren, QueryList, AfterViewInit, AfterViewChecked } from '@angular/core'; @Component({ selector: 'app-tree', template: ` <div [style.padding-left.px]="depth * 20"> <ng-content></ng-content> </div> ` }) export class TreeComponent implements AfterViewInit, AfterViewChecked { private _depth = 0; private _needsChildUpdate = false; get depth(): number { return this._depth; } set depth(value: number) { if (this._depth !== value) { this._depth = value; this._needsChildUpdate = false; } } @Input() isRoot = false; @ViewChildren(TreeComponent) children!: QueryList<TreeComponent>; ngAfterViewInit(): void { if (this.isRoot) { this.updateChildrenDepth(); } } ngAfterViewChecked(): void { // Only update if needed to avoid infinite loops if (!this._needsChildUpdate && this.children) { this.updateChildrenDepth(); } } private updateChildrenDepth(): void { this.children.forEach(child => { if (child.depth !== this.depth + 1) { child.depth = this.depth + 1; } }); this._needsChildUpdate = true; } }
Key Changes:
- Replaced
@ContentChildrenwith@ViewChildrento query view-based child components. - Added
ngAfterViewCheckedto handle updates after the view has fully rendered (critical for recursive templates, where children might load asynchronously). - Added a flag
_needsChildUpdateto prevent infinite loops inngAfterViewChecked.
Updated Template
Your recursive template can stay mostly the same—just ensure the child components are rendered within the parent <app-tree>'s view (which they already are):
<ng-template #treeTemplate let-tree let-root="isRoot"> <app-tree *ngFor="let node of tree" [isRoot]="root"> {{ node.text }} <ng-container *ngTemplateOutlet="treeTemplate; context:{$implicit: node.children || []}"></ng-container> </app-tree> </ng-template> <ng-container *ngTemplateOutlet="treeTemplate; context:{$implicit: treeModel, isRoot:true}"></ng-container>
Fixing the Secondary Issue: Host Component in Query Results
Good catch about @ContentChildren including the host component itself. The good news is that @ViewChildren doesn't have this problem—it only queries components that are direct children in the view tree, not the host component itself. That means you can remove the .filter(tree => tree !== this) check entirely from updateChildrenDepth()—it's no longer needed.
Why This Works
@ViewChildren watches the component's view tree, so any components added dynamically (like via your recursive template outlet) will be detected once the view stabilizes. The ngAfterViewChecked hook ensures we update child depths after all recursive renders are complete.
内容的提问来源于stack exchange,提问作者nest

