viewChild & contentChild
viewChild() - Signal-based Queries
Accéder aux child component/element/directive in the view (template) using signals.
- Basic Usage
- Ancien @ViewChild
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 hook
import { 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 safe
export class NewComponent {
nameInput = viewChild<ElementRef>('nameInput');
someMethod() {
// Safe - auaumatically checks if exists
this.nameInput()?.nativeElement.focus();
}
}
viewChild.required() - Required Queries
- Required Query
- Optional Query
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
- Query Multiple
- With computed()
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
- Child Component
- Psontnt Usage
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
- Tabs Component
- Usage
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
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
- Component
- Ancien Way
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 lifecycle
export class OldSearchComponent implements AfterViewInit {
@ViewChild('searchInput') searchInput!: ElementRef;
ngAfterViewInit() {
this.searchInput.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
- Child Component
- Psontnt Component
@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
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
| 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
- ✅ 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
| 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
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())
);