Angular Input / Output : guide complet

🏷️ Front-end 📅 10/04/2026 09:00:00 👤 Mezgani said
Angular Input Output Eventemitter
Angular Input / Output : guide complet

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
Conseil: @Input et @Output sont au cœur de la communication entre composants. Maîtriser ces concepts est essentiel pour tout développeur Angular.

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 = '';
}