viewChild & contentChild
viewChild() - Signal-based Queries
Section intitulée « 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èsViewInitneeded - ✅ Returns a signal - appeler avec
() - ✅ Auaumatically undefined-safe
- ✅ Réactif par défaut
// ❌ Ancien way - requires lifecycle hookimport { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({ template: `<input #nameInput>`})export class OldComponent implements AfterViewInit { @ViewChild('nameInput') nameInput!: ElementRef;
ngAfterViewInit() { // Only available here this.nameInput.nativeElement.focus(); }
someMethod() { // Might be undefined - not safe! this.nameInput.nativeElement.focus(); }}// ✅ New way - always safeexport class NewComponent { nameInput = viewChild<ElementRef>('nameInput');
someMethod() { // Safe - auaumatically checks if exists this.nameInput()?.nativeElement.focus(); }}viewChild.required() - Required Queries
Section intitulée « 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
@Component({ template: ` <app-modal #modal *ngIf="showModal"></app-modal> `})export class ConditionalComponent { // Optional - might not exist modal = viewChild(ModalComponent);
openModal() { // Safe access with optional chaining this.modal()?.open(); }}viewChildren() - Multiple Elements
Section intitulée « 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
import { Component, viewChildren, computed } from '@angular/core';
@Component({ selector: 'app-tabs', standalone: true, template: ` <app-tab *ngFor="let tab of tabs" [title]="tab"></app-tab> <p>Active tabs: {{ activeTabs() }}</p> `})export class TabsComponent { tabs = ['Home', 'Profile', 'Settings']; tabComponents = viewChildren(TabComponent);
// Derived signal - auau-updates activeTabs = computed(() => { return this.tabComponents().filter(tab => tab.isActive()).length; });}contentChild() - Projected Content
Section intitulée « 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>('');}@Component({ selector: 'app-parent', standalone: true, imports: [CardComponent, CardTitleComponent], template: ` <app-card> <app-card-title card-title [text]="'My Card Title'"></app-card-title> <p>This is the card content.</p> </app-card> `})export class ParentComponent {}What happens:
CardComponentrenders with<ng-content>CardTitleComponentis projected inau the slotcontentChild()finds the projected component- Accéder aux via
this.title()- returns signal
contentChildren() - Multiple Projected
Section intitulée « 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); }}@Component({ standalone: true, imports: [TabsComponent, TabComponent], template: ` <app-tabs> <app-tab>Tab 1 Content</app-tab> <app-tab>Tab 2 Content</app-tab> <app-tab>Tab 3 Content</app-tab> </app-tabs> `})export class AppComponent {}Query Options
Section intitulée « Query Options »Read as Different Type
Section intitulée « 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
Section intitulée « 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
Section intitulée « Practical Exemples »Auto-focus Input
Section intitulée « 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(); } }); }}// ❌ Ancien way - manual lifecycleexport class OldSearchComponent implements AfterViewInit { @ViewChild('searchInput') searchInput!: ElementRef;
ngAfterViewInit() { this.searchInput.nativeElement.focus(); }}Scroll to Element
Section intitulée « 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
Section intitulée « 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); }}@Component({ standalone: true, imports: [ModalComponent], template: ` <app-modal #modal> <p>Modal content</p> </app-modal> <button (click)="openModal()">Open Modal</button> `})export class ParentComponent { modal = viewChild(ModalComponent);
openModal() { this.modal()?.open(); }}Form Controls Management
Section intitulée « 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()
Section intitulée « 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()
Section intitulée « 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
Section intitulée « viewChild vs contentChild »| Feature | viewChild() |
contentChild() |
|---|---|---|
| Location | Component’s template | Projected content (<ng-content>) |
| Utiliser case | Accéder aux own children | Accéder aux projected children |
| Exemple | Elements in template | Children passed via <ng-content> |
| Returns | Signal with element/component | Signal with element/component |
| Available | Immediately (as signal) | Immediately (as signal) |
Bonnes Pratiques
Section intitulée « 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èsViewInitwith signal queries - ❌ Don’t forget
()- they sont signals!
Signal Queries vs Decorator Queries
Section intitulée « Signal Queries vs Decorator Queries »| Feature | @ViewChild() (Ancien) |
viewChild() (Moderne) |
|---|---|---|
| Type | Property | Signal |
| Accéder aux | this.element |
this.element() |
| Available | Après ngAprèsViewInit |
Immediately |
| Reactivity | Manual | Auaumatic |
| Required | @ViewChild({ ... })! |
viewChild.required() |
| Multiple | @ViewChildren() + QueryList |
viewChildren() returns tableau |
| Safe access | Manual ? or ! |
Auaumatic with ()?. |
| Works with | Lifecycle hooks | Signals, computed(), effect() |
Common Patterns
Section intitulée « Common Patterns »Pattern 1: Conditional Element
Section intitulée « Pattern 1: Conditional Element »modal = viewChild(ModalComponent); // Optional
showModal() { this.modal()?.open(); // Safe - might not exist}Pattern 2: Required Element
Section intitulée « Pattern 2: Required Element »form = viewChild.required<ElementRef>('form'); // Must exist
submitForm() { this.form().nativeElement.submit(); // No ? needed}Pattern 3: Multiple Elements
Section intitulée « Pattern 3: Multiple Elements »items = viewChildren(ItemComponent);
processAll() { this.items().forEach(item => item.process());}Pattern 4: Derived State
Section intitulée « Pattern 4: Derived State »checkboxes = viewChildren(CheckboxComponent);allChecked = computed(() => this.checkboxes().every(cb => cb.isChecked()));
