Comprendre et utiliser les décorateurs @Input et @Output pour la communication entre composants Angular, avec des exemples pratiques.
Prérequis
Ce guide suppose que tu as:
- Angular 15+ installé (vérifier avec
ng version) - Une compréhension de base des composants Angular
- Une connaissance des décorateurs TypeScript
Les décorateurs @Input
@Input permet aux composants parent de passer des données à un composant enfant. C'est un binding unidirectionnel du parent vers l'enfant.
Syntaxe basique
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-enfant',
template: `<p>Message: {{ message }}</p>`
})
export class EnfantComponent {
@Input() message: string = 'Valeur par défaut';
}
Utilisation dans le parent
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `<app-enfant [message]="'Bonjour depuis le parent'"></app-enfant>`
})
export class ParentComponent {}
@Input avec alias
Tu peux donner un alias à un @Input pour une meilleure lisibilité ou pour respecter une convention de nommage:
@Component({
selector: 'app-bouton',
template: `<button [disabled]="isDisabled">{{ label }}</button>`
})
export class BoutonComponent {
@Input('text') label: string = 'Cliquez-moi';
@Input() isDisabled: boolean = false;
}
Utilisation:
<app-bouton [text]="'Valider'" [isDisabled]="false"></app-bouton>
@Input avec valeurs complexes
Passer des objets et tableaux:
export interface Utilisateur {
id: number;
nom: string;
email: string;
}
@Component({
selector: 'app-carte-utilisateur',
template: `
<div class="carte">
<h3>{{ utilisateur.nom }}</h3>
<p>{{ utilisateur.email }}</p>
</div>
`
})
export class CarteUtilisateurComponent {
@Input() utilisateur!: Utilisateur;
}
Détecter les changements d'@Input
Avec ngOnChanges:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-compteur',
template: `<p>Valeur: {{ valeur }}</p>`
})
export class CompteurComponent implements OnChanges {
@Input() valeur: number = 0;
ngOnChanges(changes: SimpleChanges) {
if (changes['valeur']) {
console.infoo('Ancienne valeur:', changes['valeur'].previousValue);
console.info('Nouvelle valeur:', changes['valeur'].currentValue);
}
}
}
Les décorateurs @Output
@Output permet à un composant enfant d'envoyer des données au composant parent via des événements. Tu dois utiliser EventEmitter.
Syntaxe basique
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-bouton-custom',
template: `<button (click)="clique()">Clique-moi</button>`
})
export class BoutonCustomComponent {
@Output() monEvenement = new EventEmitter<string>();
clique() {
this.monEvenement.emit('Bouton cliqué!');
}
}
Écouter l'événement dans le parent
@Component({
selector: 'app-parent',
template: `
<app-bouton-custom (monEvenement)="traiterEvenement($event)"></app-bouton-custom>
<p>{{ message }}</p>
`
})
export class ParentComponent {
message: string = '';
traiterEvenement(data: string) {
this.message = data;
}
}
Émettre des objets complexes
export interface ElementListe {
id: number;
titre: string;
}
@Component({
selector: 'app-liste-item',
template: `
<button (click)="selectionner()">{{ item.titre }}</button>
`
})
export class ListeItemComponent {
@Input() item!: ElementListe;
@Output() itemSelectionne = new EventEmitter<ElementListe>();
selectionner() {
this.itemSelectionne.emit(this.item);
}
}
Alias pour @Output
@Component({
selector: 'app-input-custom',
template: `<input (change)="onChange($event)" />`
})
export class InputCustomComponent {
@Output('valueChanged') valueChange = new EventEmitter<string>();
onChange(event: Event) {
const valeur = (event.target as HTMLInputElement).value;
this.valueChange.emit(valeur);
}
}
Utilisation:
<app-input-custom (valueChanged)="traiterChangement($event)"></app-input-custom>
Communication parent-enfant
Combiner @Input et @Output pour une communication bidirectionnelle:
Exemple: Todo List
// Modèle
export interface Todo {
id: number;
texte: string;
completed: boolean;
}
// Composant enfant: TodoItem
@Component({
selector: 'app-todo-item',
template: `
<li class="todo-item">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleComplete()"
/>
<span>{{ todo.texte }}</span>
<button (click)="supprimer()">Supprimer</button>
</li>
`
})
export class TodoItemComponent {
@Input() todo!: Todo;
@Output() completionToggle = new EventEmitter<number>();
@Output() suppression = new EventEmitter<number>();
toggleComplete() {
this.completionToggle.emit(this.todo.id);
}
supprimer() {
this.suppression.emit(this.todo.id);
}
}
// Composant parent: TodoList
@Component({
selector: 'app-todo-list',
template: `
<div>
<h2>Ma Todo List</h2>
<ul>
<app-todo-item
*ngFor="let todo of todos"
[todo]="todo"
(completionToggle)="toggleTodo($event)"
(suppression)="supprimerTodo($event)"
></app-todo-item>
</ul>
</div>
`
})
export class TodoListComponent {
todos: Todo[] = [
{ id: 1, texte: 'Apprendre Angular', completed: false },
{ id: 2, texte: 'Maîtriser @Input/@Output', completed: true }
];
toggleTodo(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
supprimerTodo(id: number) {
this.todos = this.todos.filter(t => t.id !== id);
}
}
Exemple complet: Composant de formulaire
// Modèle de formulaire
export interface FormulaireDonnees {
nom: string;
email: string;
message: string;
}
// Composant enfant: FormComponent
@Component({
selector: 'app-form-contact',
template: `
<form (submit)="envoyerFormulaire($event)">
<div class="form-group">
<label for="nom">Nom</label>
<input
id="nom"
type="text"
[(ngModel)]="formData.nom"
name="nom"
/>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
type="email"
[(ngModel)]="formData.email"
name="email"
/>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
[(ngModel)]="formData.message"
name="message"
></textarea>
</div>
<button type="submit" [disabled]="estEnCours">
{{ estEnCours ? 'Envoi...' : 'Envoyer' }}
</button>
</form>
<div *ngIf="message" class="message" [class]="typesMessage">
{{ message }}
</div>
`,
styles: [`
.form-group { margin-bottom: 15px; }
.message { padding: 10px; margin-top: 15px; border-radius: 4px; }
.success { background-color: #d4edda; color: #155724; }
.error { background-color: #f8d7da; color: #721c24; }
`]
})
export class FormContactComponent {
@Input() titre: string = 'Contactez-nous';
@Output() formSoumis = new EventEmitter<FormulaireDonnees>();
formData: FormulaireDonnees = {
nom: '',
email: '',
message: ''
};
estEnCours: boolean = false;
message: string = '';
typesMessage: string = '';
envoyerFormulaire(event: Event) {
event.preventDefault();
if (!this.validerFormulaire()) {
this.afficherMessage('Veuillez remplir tous les champs', 'error');
return;
}
this.estEnCours = true;
this.formSoumis.emit(this.formData);
setTimeout(() => {
this.afficherMessage('Formulaire envoyé avec succès!', 'success');
this.reinitialiserFormulaire();
this.estEnCours = false;
}, 1000);
}
validerFormulaire(): boolean {
return this.formData.nom.trim() !== '' &&
this.formData.email.trim() !== '' &&
this.formData.message.trim() !== '';
}
reinitialiserFormulaire() {
this.formData = { nom: '', email: '', message: '' };
}
afficherMessage(msg: string, type: string) {
this.message = msg;
this.typesMessage = type;
}
}
// Utilisation dans le parent
@Component({
selector: 'app-page-contact',
template: `
<div class="container">
<app-form-contact
[titre]="'Envoyer un message'"
(formSoumis)="traiterFormulaire($event)"
></app-form-contact>
</div>
`
})
export class PageContactComponent {
traiterFormulaire(donnees: FormulaireDonnees) {
console.info('Formulaire reçu:', donnees);
// Appeler un service API pour envoyer les données au backend
}
}
Bonnes pratiques
1. Typage fort
Toujours typer tes @Input et @Output avec TypeScript pour éviter les bugs:
// ✅ Bon
@Input() utilisateurs: Utilisateur[] = [];
@Output() userSelected = new EventEmitter<Utilisateur>();
// ❌ Mauvais
@Input() utilisateurs: any;
@Output() userSelected = new EventEmitter();
2. Éviter les mutations directes
Evite de modifier directement les valeurs @Input. Émets un événement à la place:
// ❌ À éviter
@Component({})
export class ListeComponent {
@Input() items: any[] = [];
supprimer(item: any) {
this.items.splice(this.items.indexOf(item), 1); // Direct mutation
}
}
// ✅ Correct
@Component({})
export class ListeComponent {
@Input() items: any[] = [];
@Output() itemSupprime = new EventEmitter<any>();
supprimer(item: any) {
this.itemSupprime.emit(item); // Parent gère la suppression
}
}
3. Documentation claire
Documente tes @Input et @Output avec des commentaires JSDoc:
@Component({})
export class CarteComponent {
/** Objet utilisateur à afficher */
@Input() utilisateur!: Utilisateur;
/** Émis quand l'utilisateur clique sur la carte */
@Output() cliquerCarte = new EventEmitter<Utilisateur>();
}
4. Valeurs par défaut pertinentes
Fournis des valeurs par défaut logiques:
@Component({})
export class BoutonComponent {
@Input() texte: string = 'Cliquez-moi';
@Input() couleur: string = 'primary';
@Input() desactif: boolean = false;
}
5. Utiliser required (Angular 16+)
Rendre un @Input obligatoire:
@Component({})
export class CarteComponent {
@Input({ required: true }) utilisateur!: Utilisateur;
@Input() titre: string = '';
}