Aller au contenu principal

viewChild & contentChild

viewChild() - Signal-based Queries

Accéder aux child component/element/directive in the view (template) using signals.

import { Component, viewChild, ElementRef } from '@angular/core';

@Component({
selector: 'app-parent',
standalone: true,
template: `
<input #nameInput placeholder="Enter name">
<app-child #childComp></app-child>
<button (click)="focusInput()">Focus Input</button>
`
})
export class ParentComponent {
// Basé sur signal query - NO lifecycle hooks needed!
nameInput = viewChild<ElementRef<HTMLInputElement>>('nameInput');

// Query by component type
childComponent = viewChild(ChildComponent);

focusInput() {
// Accéder aux as signal - auaumatically available
this.nameInput()?.nativeElement.focus();
}

callChild() {
this.childComponent()?.doSomething();
}
}

Key benefits:

  • ✅ No ngAprèsViewInit needed
  • ✅ Returns a signal - appeler avec ()
  • ✅ Auaumatically undefined-safe
  • ✅ Réactif par défaut

viewChild.required() - Required Queries

import { Component, viewChild, ElementRef } from '@angular/core';

@Component({
selector: 'app-form',
standalone: true,
template: `
<input #emailInput type="email" required>
<app-submit-button></app-submit-button>
`
})
export class FormComponent {
// Required query - compile error if not found
emailInput = viewChild.required<ElementRef<HTMLInputElement>>('emailInput');
submitButton = viewChild.required(SubmitButtonComponent);

submit() {
// Pas besoin de ? - guaranteed au exist
const email = this.emailInput().nativeElement.value;
this.submitButton().disable();
}
}

Avantages:

  • Vérification à la compilation
  • No ! (non-null assertion) needed
  • TypeScript knows it exists

viewChildren() - Multiple Elements

import { Component, viewChildren } from '@angular/core';

@Component({
selector: 'app-list',
standalone: true,
template: `
<app-item *ngFor="let item of items" [data]="item"></app-item>
<p>Total items: {{ itemComponents().length }}</p>
`
})
export class ListComponent {
items = ['A', 'B', 'C'];

// Signal returning tableau of components
itemComponents = viewChildren(ItemComponent);

logItems() {
console.log('Items:', this.itemComponents().length);

// Iterate over items
this.itemComponents().forEach(item => {
item.doSomething();
});
}

getFirst() {
return this.itemComponents()[0];
}
}

Returns:

  • Signal containing tableau of matching éléments
  • Empty tableau if none found
  • Auaumatically updates when children change

contentChild() - Projected Content

import { Component, contentChild } from '@angular/core';

@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<div class="card-header">
<!-- Projected header -->
<ng-content select="[card-title]"></ng-content>
</div>
<div class="card-body">
<!-- Projected body -->
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {
// Query projected content
title = contentChild(CardTitleComponent);

logTitle() {
const titleComp = this.title();
if (titleComp) {
console.log('Title:', titleComp.text());
}
}
}

@Component({
selector: 'app-card-title',
standalone: true,
template: `<h3>{{ text() }}</h3>`
})
export class CardTitleComponent {
text = input<string>('');
}

contentChildren() - Multiple Projected

import { Component, contentChildren, effect } from '@angular/core';

@Component({
selector: 'app-tabs',
standalone: true,
template: `
<div class="tabs">
<ng-content></ng-content>
</div>
<p>Total tabs: {{ tabs().length }}</p>
`
})
export class TabsComponent {
// Query all projected TabComponent
tabs = contentChildren(TabComponent);

constructor() {
// L'effet s'exécute quand tabs() changements
effect(() => {
const allTabs = this.tabs();
console.log('Tabs count:', allTabs.length);

// Activate first tab
if (allTabs.length > 0) {
allTabs[0].activate();
}
});
}
}

@Component({
selector: 'app-tab',
standalone: true,
template: `<div>Tab content</div>`
})
export class TabComponent {
isActive = signal(false);

activate() {
this.isActive.set(true);
}
}

Query Options

Read as Different Type

import { Component, viewChild, ElementRef, ViewContainerRef } from '@angular/core';

@Component({
template: `<div #container>Content</div>`
})
export class ReadComponent {
// Read as ElementRef (default for éléments)
containerEl = viewChild<ElementRef>('container');

// Read as ViewContainerRef
containerRef = viewChild('container', {
read: ViewContainerRef
});

useContainer() {
// Accéder aux as ElementRef
this.containerEl()?.nativeElement.style.color = 'red';

// Accéder aux as ViewContainerRef for dynamic components
this.containerRef()?.clear();
}
}

Descendants Option

@Component({
template: `
<div>
<app-child></app-child>
<div>
<app-child></app-child> <!-- Nested child -->
</div>
</div>
`
})
export class ContainerComponent {
// Query all descendants (default: true)
allChildren = viewChildren(ChildComponent, { descendants: true });

// Query only direct children
directChildren = viewChildren(ChildComponent, { descendants: false });

compare() {
console.log('All:', this.allChildren().length); // 2
console.log('Direct:', this.directChildren().length); // 1
}
}

Practical Exemples

Auto-focus Input

import { Component, viewChild, ElementRef, effect } from '@angular/core';

@Component({
selector: 'app-search',
standalone: true,
template: `
<input #searchInput placeholder="Search..." />
`
})
export class SearchComponent {
searchInput = viewChild<ElementRef<HTMLInputElement>>('searchInput');

constructor() {
// Auau-focus when input becomes available
effect(() => {
const input = this.searchInput();
if (input) {
input.nativeElement.focus();
}
});
}
}

Scroll to Element

import { Component, viewChild, ElementRef } from '@angular/core';

@Component({
template: `
<div class="container">
<div #targetElement>Target</div>
</div>
<button (click)="scrollToTarget()">Scroll</button>
`
})
export class ScrollComponent {
target = viewChild<ElementRef>('targetElement');

scrollToTarget() {
this.target()?.nativeElement.scrollIntoView({ behavior: 'smooth' });
}
}

Access Child Component Methods

@Component({
selector: 'app-modal',
standalone: true,
template: `
<div class="modal" *ngIf="isOpen()">
<ng-content></ng-content>
</div>
`
})
export class ModalComponent {
isOpen = signal(false);

open() {
this.isOpen.set(true);
}

close() {
this.isOpen.set(false);
}
}

Form Controls Management

import { Component, viewChildren, ElementRef } from '@angular/core';

@Component({
selector: 'app-multi-input',
standalone: true,
template: `
<input #input *ngFor="let field of fields" [placeholder]="field">
<button (click)="clearAll()">Clear All</button>
`
})
export class MultiInputComponent {
fields = ['Name', 'Email', 'Phone'];
inputs = viewChildren<ElementRef<HTMLInputElement>>('input');

clearAll() {
this.inputs().forEach(input => {
input.nativeElement.value = '';
});
}

focusFirst() {
this.inputs()[0]?.nativeElement.focus();
}
}

Using with computed()

import { Component, viewChildren, computed } from '@angular/core';

@Component({
selector: 'app-checklist',
standalone: true,
template: `
<app-checkbox *ngFor="let item of items" [label]="item"></app-checkbox>
<p>Checked: {{ checkedCount() }} / {{ totalCount() }}</p>
`
})
export class ChecklistComponent {
items = ['Task 1', 'Task 2', 'Task 3'];
checkboxes = viewChildren(CheckboxComponent);

totalCount = computed(() => this.checkboxes().length);

checkedCount = computed(() => {
return this.checkboxes().filter(cb => cb.isChecked()).length;
});
}

Using with effect()

import { Component, contentChildren, effect } from '@angular/core';

@Component({
selector: 'app-accordion',
standalone: true,
template: `<ng-content></ng-content>`
})
export class AccordionComponent {
panels = contentChildren(PanelComponent);

constructor() {
// Réagir aux panel changements
effect(() => {
const allPanels = this.panels();
console.log('Panel count:', allPanels.length);

// Ensure only one is open
const openPanels = allPanels.filter(p => p.isOpen());
if (openPanels.length > 1) {
// Close all but first
openPanels.slice(1).forEach(p => p.close());
}
});
}
}

viewChild vs contentChild

FeatureviewChild()contentChild()
LocationComponent's templateProjected content (<ng-content>)
Utiliser caseAccéder aux own childrenAccéder aux projected children
ExempleElements in templateChildren passed via <ng-content>
ReturnsSignal with element/componentSignal with element/component
AvailableImmediately (as signal)Immediately (as signal)

Bonnes Pratiques

  • ✅ Utiliser viewChild() au lieu de @ViewChild() - Basé sur signal, reactive
  • ✅ Utiliser viewChild.required() for éléments that must exist
  • ✅ Utiliser optional chaining: this.element()?.method()
  • ✅ Utiliser viewChildren() for lists - returns signal with tableau
  • ✅ Utiliser contentChild() for projected content
  • ✅ Combine with computed() for derived values
  • ✅ Utiliser effect() au react au query changements
  • ✅ Utiliser { read: ViewContainerRef } for dynamic components
  • ❌ Don't use ngAprèsViewInit with signal queries
  • ❌ Don't forget () - they sont signals!

Signal Queries vs Decorator Queries

Feature@ViewChild() (Ancien)viewChild() (Moderne)
TypePropertySignal
Accéder auxthis.elementthis.element()
AvailableAprès ngAprèsViewInitImmediately
ReactivityManualAuaumatic
Required@ViewChild({ ... })!viewChild.required()
Multiple@ViewChildren() + QueryListviewChildren() returns tableau
Safe accessManual ? or !Auaumatic with ()?.
Works withLifecycle hooksSignals, computed(), effect()

Common Patterns

Pattern 1: Conditional Element

modal = viewChild(ModalComponent);  // Optional

showModal() {
this.modal()?.open(); // Safe - might not exist
}

Pattern 2: Required Element

form = viewChild.required<ElementRef>('form');  // Must exist

submitForm() {
this.form().nativeElement.submit(); // No ? needed
}

Pattern 3: Multiple Elements

items = viewChildren(ItemComponent);

processAll() {
this.items().forEach(item => item.process());
}

Pattern 4: Derived State

checkboxes = viewChildren(CheckboxComponent);
allChecked = computed(() =>
this.checkboxes().every(cb => cb.isChecked())
);