Components
Component Basique
import { Component } from '@angular/core';
@Component({
selector: 'app-album',
standalone: true,
template: `<h1>{{ title }}</h1>`,
styles: [`h1 { color: blue; }`]
})
export class AlbumComponent {
title = 'Now Playing';
}
Utilisation:
<app-album></app-album>
input() - Parent vers Enfant
- Composant Enfant
- Composant Parent
- Comment ça fonctionne
import { Component, input } from '@angular/core';
@Component({
selector: 'app-album-card',
standalone: true,
template: `
<div class="card">
<h2>{{ name() }}</h2>
<p>Artist: {{ artist() }}</p>
<p class="price">{{ price() | currency }}</p>
</div>
`
})
export class AlbumCardComponent {
// Entrées basées sur les signaux (Angular 17+)
name = input<string>(''); // Optionnel avec valeur par défaut
artist = input<string>(''); // Optionnel avec valeur par défaut
price = input<number>(0); // Optionnel avec valeur par défaut
}
import { Component } from '@angular/core';
import { AlbumCardComponent } from './album-card.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [AlbumCardComponent],
template: `
<app-album-card
[name]="albumName"
[artist]="albumArtist"
[price]="albumPrice">
</app-album-card>
`
})
export class ParentComponent {
albumName = 'Dark Side of the Moon';
albumArtist = 'Pink Floyd';
albumPrice = 29.99;
}
Étape 1: L'enfant déclare les entrées
name = input<string>(''); // Entrée basée sur les signaux
artist = input<string>('');
price = input<number>(0);
Étape 2: Le parent transmet les données
<app-album-card [name]="albumName" [artist]="albumArtist" [price]="albumPrice"></app-album-card>
Étape 3: L'enfant lit les entrées comme des signaux
<h2>{{ name() }}</h2> <!-- Appeler comme une fonction -->
<p>Artist: {{ artist() }}</p>
<p>{{ price() | currency }}</p>
Différence clé par rapport à @Input():
- ✅ Les entrées sont des signaux - appeler avec
() - ✅ Réactif par défaut
- ✅ Peut être utilisé dans computed(), effect()
input() avec Requis
- Entrées Requises
- Utilisation
import { Component, input } from '@angular/core';
@Component({
selector: 'app-album',
standalone: true,
template: `
<h3>{{ name() }}</h3>
<p>Artist: {{ artist() }}</p>
<p *ngIf="featured()">⭐ Featured Album!</p>
`
})
export class AlbumComponent {
// Input requis - pas de valeur par défaut
name = input.required<string>();
// Inputs optionnels avec valeurs par défaut
artist = input<string>('Unknown Artist');
featured = input<boolean>(false);
}
<!-- ✅ Valide - title est fourni -->
<app-album
[title]="'Abbey Road'"
[artist]="'The Beatles'"
[featured]="true">
</app-album>
<!-- ❌ Erreur de compilation - title est requis -->
<app-album [artist]="'The Beatles'"></app-album>
Avantages:
- Vérification à la compilation pour les entrées requises
- Pas besoin de
!(assertion non-null) - Sûreté des types
output() - Enfant vers Parent
- Composant Enfant
- Composant Parent
- Flux de Données
import { Component, output } from '@angular/core';
@Component({
selector: 'app-track-player',
standalone: true,
template: `
<button (click)="play()">▶️ Play Count: {{ playCount }}</button>
`
})
export class TrackPlayerComponent {
playCount = 0;
// Sortie basée sur les signaux (Angular 17.1+)
played = output<number>();
play() {
this.playCount++;
this.played.emit(this.playCount); // Émettre vers le parent
}
}
import { Component } from '@angular/core';
import { TrackPlayerComponent } from './track-player.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [TrackPlayerComponent],
template: `
<app-track-player (played)="onTrackPlayed($event)"></app-track-player>
<p>Total plays: {{ totalPlays }}</p>
`
})
export class ParentComponent {
totalPlays = 0;
onTrackPlayed(playCount: number) {
this.totalPlays = playCount;
console.log('Track played, total:', playCount);
}
}
1. Click play button in Child
↓
2. Child calls play()
↓
3. Child emits: played.emit(this.playCount)
↓
4. Parent receives: (played)="onTrackPlayed($event)"
↓
5. Parent's onTrackPlayed() is called with play count
↓
6. Parent updates: this.totalPlays = playCount
Différences clés par rapport à @Output():
- ✅ Pas besoin d'importer
EventEmitter - ✅ Syntaxe plus simple:
output<T>() - ✅ Sûreté des types par défaut
input() + output() Combinés
- Composant Enfant
- Composant Parent
import { Component, input, output } from '@angular/core';
interface Album {
id: number;
name: string;
artist: string;
description: string;
price: number;
tags: string[];
highlighted?: boolean;
}
@Component({
selector: 'app-album-editor',
standalone: true,
template: `
<input
[value]="album().name"
(input)="onNameChange($event)"
placeholder="Album name">
<input
[value]="album().artist"
(input)="onArtistChange($event)"
placeholder="Artist name">
<input
type="number"
[value]="album().price"
(input)="onPriceChange($event)"
placeholder="Price">
<button (click)="onSave()">💾 Save</button>
<button (click)="onCancel()">❌ Cancel</button>
`
})
export class AlbumEditorComponent {
// Entrée basée sur les signaux (requise)
album = input.required<Album>();
// Sorties basées sur les signaux
save = output<Album>();
cancel = output<void>();
onNameChange(event: Event) {
const input = event.target as HTMLInputElement;
const updated = { ...this.album(), name: input.value };
this.save.emit(updated);
}
onArtistChange(event: Event) {
const input = event.target as HTMLInputElement;
const updated = { ...this.album(), artist: input.value };
this.save.emit(updated);
}
onPriceChange(event: Event) {
const input = event.target as HTMLInputElement;
const updated = { ...this.album(), price: parseFloat(input.value) };
this.save.emit(updated);
}
onSave() {
this.save.emit(this.album());
}
onCancel() {
this.cancel.emit();
}
}
import { Component, signal } from '@angular/core';
import { AlbumEditorComponent } from './album-editor.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [AlbumEditorComponent],
template: `
<app-album-editor
[album]="currentAlbum()"
(save)="handleSave($event)"
(cancel)="handleCancel()">
</app-album-editor>
`
})
export class ParentComponent {
currentAlbum = signal<Album>({
id: 1,
name: 'Dark Side of the Moon',
artist: 'Pink Floyd'
});
handleSave(updatedAlbum: Album) {
this.currentAlbum.set(updatedAlbum);
console.log('Album saved:', updatedAlbum);
}
handleCancel() {
console.log('Edit cancelled');
}
}
Utiliser les Entrées dans computed()
- Composant
- Utilisation
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-album-duration',
standalone: true,
template: `
<div>
<p>🎵 Tracks: {{ trackCount() }}</p>
<p>⏱️ Avg Duration: {{ avgDuration() }}s</p>
<p>⏰ Total: {{ totalDuration() }}s ({{ formattedDuration() }})</p>
</div>
`
})
export class AlbumDurationComponent {
trackCount = input<number>(0);
avgDuration = input<number>(0);
// Signal calculé dérivé des entrées
totalDuration = computed(() => {
return this.trackCount() * this.avgDuration();
});
formattedDuration = computed(() => {
const total = this.totalDuration();
const mins = Math.floor(total / 60);
const secs = total % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
});
}
<app-album-duration
[trackCount]="12"
[avgDuration]="240">
</app-album-duration>
<!-- Sortie:
🎵 Tracks: 12
⏱️ Avg Duration: 240s
⏰ Total: 2880s (48:00)
-->
Avantages:
totalDuration()etformattedDuration()se mettent à jour automatiquement quand les entrées changent- Pas besoin du hook de cycle de vie ngOnChanges
- Déclaratif et réactif
Utiliser les Entrées dans effect()
import { Component, input, effect } from '@angular/core';
@Component({
selector: 'app-track-analytics',
standalone: true,
template: `<p>Now Playing: Track #{{ trackId() }}</p>`
})
export class TrackAnalyticsComponent {
trackId = input<number>(0);
constructor() {
// L'effet s'exécute quand trackId() change
effect(() => {
console.log('Track changed to:', this.trackId());
// Effet secondaire: log vers analytics, mise à jour historique, etc.
});
}
}
Transformations d'Entrées
- Avec Transformation
- Utilisation
import { Component, input } from '@angular/core';
@Component({
selector: 'app-album-formatter',
standalone: true,
template: `
<p>Artist: {{ formattedArtist() }}</p>
<p>Year: {{ releaseYear() }}</p>
<p>Featured: {{ isFeatured() }}</p>
`
})
export class AlbumFormatterComponent {
// Transformer string en majuscules
formattedArtist = input('', {
transform: (value: string) => value.toUpperCase()
});
// Transformer string en nombre
releaseYear = input(0, {
transform: (value: string | number) =>
typeof value === 'string' ? parseInt(value, 10) : value
});
// Transformer string en booléen
isFeatured = input(false, {
transform: (value: string | boolean) =>
value === 'true' || value === true
});
}
<!-- Passage des valeurs -->
<app-album-formatter
formattedArtist="pink floyd"
releaseYear="1973"
isFeatured="true">
</app-album-formatter>
<!-- Transformations appliquées:
Artist: PINK FLOYD (uppercase)
Year: 1973 (parsed to number)
Featured: true (parsed to boolean)
-->
Alias d'Entrées
import { Component, input } from '@angular/core';
@Component({
selector: 'app-aliased',
standalone: true,
template: `<p>{{ artistName() }}</p>`
})
export class AliasedComponent {
// Nom de propriété: artistName
// Liaison dans le template: [artist-name]
artistName = input<string>('', {
alias: 'artist-name'
});
}
Utilisation:
<app-aliased [artist-name]="'Pink Floyd'"></app-aliased>
Modèles de Communication entre Composants
Modèle 1: Parent-Enfant Simple
// Enfant
@Component({
selector: 'app-track-display',
template: `<p>🎵 {{ trackTitle() }}</p>`
})
export class TrackDisplayComponent {
trackTitle = input<string>('');
}
// Parent
@Component({
template: `<app-track-display [trackTitle]="'Bohemian Rhapsody'"></app-track-display>`
})
export class ParentComponent {}
Modèle 2: L'Enfant Notifie le Parent
// Enfant
@Component({
selector: 'app-play-button',
template: `<button (click)="play()">▶️ Play</button>`
})
export class PlayButtonComponent {
trackPlayed = output<void>();
play() {
this.trackPlayed.emit();
}
}
// Parent
@Component({
template: `<app-play-button (trackPlayed)="onTrackPlayed()"></app-play-button>`
})
export class ParentComponent {
onTrackPlayed() {
console.log('Track is now playing!');
}
}
Modèle 3: Flux de Données Bidirectionnel
// Enfant
@Component({
selector: 'app-track-editor',
template: `
<input
[value]="title()"
(input)="titleChange.emit($any($event.target).value)"
placeholder="Track title">
`
})
export class TrackEditorComponent {
title = input<string>('');
titleChange = output<string>();
}
// Parent
@Component({
template: `<app-track-editor [(title)]="trackTitle"></app-track-editor>`
})
export class ParentComponent {
trackTitle = signal('Stairway to Heaven');
}
Bonnes Pratiques
- ✅ Utiliser
input()au lieu de@Input()- Basé sur les signaux, plus réactif - ✅ Utiliser
output()au lieu de@Output()- Plus simple, pas besoin d'importer EventEmitter - ✅ Utiliser
input.required()pour les entrées requises - Sécurité à la compilation - ✅ Accéder aux entrées comme des fonctions:
this.name()- Ce sont des signaux - ✅ Utiliser computed() pour les valeurs dérivées - Réactivité automatique
- ✅ Utiliser effect() pour les effets secondaires - Réagit aux changements d'entrées
- ✅ Utiliser les transformations pour la conversion de types - Séparation claire des préoccupations
- ❌ Ne pas muter directement les valeurs input() - Les entrées sont en lecture seule
- ❌ Ne pas utiliser ngOnChanges avec les entrées basées sur signaux - Utiliser computed() ou effect()
input() vs @Input()
| Fonctionnalité | @Input() (Ancien) | input() (Moderne) |
|---|---|---|
| Type | Propriété | Signal |
| Accès | this.name | this.name() |
| Réactivité | Manuelle (ngOnChanges) | Automatique |
| Requis | @Input({ required: true }) | input.required<T>() |
| Défaut | @Input() x = 5 | input<number>(5) |
| Transform | @Input({ transform }) | input(0, { transform }) |
| Fonctionne avec | Détection de changements | Signals, computed(), effect() |
output() vs @Output()
| Fonctionnalité | @Output() (Ancien) | output() (Moderne) |
|---|---|---|
| Import | EventEmitter | Aucun nécessaire |
| Déclaration | new EventEmitter<T>() | output<T>() |
| Émettre | .emit(value) | .emit(value) |
| Sûreté des types | Manuelle | Automatique |