Créer un formulaire d’envoi de mail avec pièces jointes en Angular 20 (Reactive Forms & FormData)

Dans cet article, nous allons voir comment développer un formulaire de contact moderne et élégant avec Angular 20. Ce formulaire permet à vos utilisateurs d’envoyer un message comprenant une adresse e-mail, un objet, un contenu textuel, ainsi que plusieurs images en pièces jointes.

Pour un rendu professionnel, nous utiliserons Bootstrap 5 et Bootstrap Icons.

1. Le Modèle de Données (mail-model.ts)

On commence par définir la structure de nos données sous forme de classe. Cela permet de typer proprement ce que nous manipulons.

export class MailModel {
  constructor(
    public email: string,
    public subject: string,
    public content: string,
    public images: File[]
  ) {}
}

2. Le Service de Messagerie (mail-service.ts)

Le service a pour rôle de communiquer avec l’API backend. Comme nous envoyons des fichiers (images) en plus du texte, nous devons obligatoirement utiliser un objet FormData.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { MailModel } from '../models/mail-model';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class MailService {
  // Injection moderne de HttpClient (Angular 14+)
  private readonly http = inject(HttpClient);
  private readonly apiUrl = 'http://localhost:3000/mail';

  // Méthode pour envoyer les données au serveur
  sendMail(mailData: { email: string; subject: string; content: string; images: File[] }): Observable<any> {
    const formData = new FormData();

    // Ajout de chaque image dans le FormData
    for (const image of mailData.images) {
      formData.append('images', image);
    }

    // Ajout des champs textuels
    formData.append('email', mailData.email);
    formData.append('subject', mailData.subject);
    formData.append('content', mailData.content);

    // Envoi de la requête POST au backend
    return this.http.post<MailModel>(this.apiUrl, formData);
  }
}

3. Le Composant TypeScript (mail.ts)

C’est ici que la logique du formulaire réactif (ReactiveFormsModule) prend place, ainsi que la gestion de la sélection des fichiers.

import { Component, inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MailService } from '../../services/mail-service';

@Component({
  selector: 'app-mail',
  imports: [ReactiveFormsModule],
  templateUrl: './mail.html',
  styleUrl: './mail.scss',
})
export class Mail implements OnInit {
  protected form!: FormGroup;
  protected selectedFiles: File[] = [];

  // Injections des dépendances avec la fonction inject()
  private readonly formBuilder = inject(FormBuilder);
  private readonly mailService = inject(MailService);

  ngOnInit(): void {
    // Initialisation du formulaire avec les validations requises
    this.form = this.formBuilder.group({
      email: [null, [Validators.required, Validators.email]],
      subject: [null, Validators.required],
      content: [null, Validators.required],
      images: [null],
    });
  }

  // Méthode déclenchée lors de la sélection de fichiers
  protected onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    if (input.files && input.files.length > 0) {
      // Transformation du FileList en tableau de fichiers (File[])
      this.selectedFiles = Array.from(input.files);
    }
  }

  // Soumission du formulaire
  submitForm(): void {
    // Fusion des valeurs du formulaire et des fichiers sélectionnés
    const mailData = {
      ...this.form.value,
      images: this.selectedFiles,
    };

    // Appel du service pour envoyer l'e-mail
    this.mailService.sendMail(mailData).subscribe({
      next: () => {
        // Réinitialisation du formulaire et des fichiers après succès
        this.form.reset();
        this.selectedFiles = [];
      },
    });
  }
}

4. Le Template HTML (mail.html)

Le template intègre une mise en page soignée grâce à Bootstrap, avec des icônes explicites pour guider l’utilisateur.

<main class="mail-page d-flex align-items-center py-5">
  <div class="container">
    <div class="row justify-content-center">
      <div class="col-12 col-md-9 col-lg-7 col-xl-6">
        
        <!-- Carte du formulaire -->
        <section class="formulaire card border-0 shadow-lg overflow-hidden" aria-labelledby="mail-title">
          
          <!-- En-tête -->
          <div class="formulaire__header card-header border-0 p-4 p-md-5">
            <div class="d-flex align-items-center gap-3">
              <span class="formulaire__icon d-inline-flex align-items-center justify-content-center" aria-hidden="true">
                <i class="bi bi-envelope-paper-fill"></i>
              </span>
              <div>
                <h1 id="mail-title" class="h3 fw-bold mb-1">Envoyer un message</h1>
                <p class="mb-0 text-white-50">Complétez le formulaire et ajoutez vos images.</p>
              </div>
            </div>
          </div>

          <!-- Corps du formulaire -->
          <div class="card-body bg-white p-4 p-md-5">
            <form [formGroup]="form" (ngSubmit)="submitForm()">
              
              <!-- Champ E-mail -->
              <div class="mb-4">
                <label for="email" class="form-label fw-semibold">Adresse e-mail</label>
                <div class="input-group">
                  <span class="input-group-text" aria-hidden="true"><i class="bi bi-at"></i></span>
                  <input id="email" type="email" class="form-control form-control-lg" formControlName="email" placeholder="nom@exemple.com" autocomplete="email" required />
                </div>
              </div>

              <!-- Champ Objet -->
              <div class="mb-4">
                <label for="subject" class="form-label fw-semibold">Objet</label>
                <div class="input-group">
                  <span class="input-group-text" aria-hidden="true"><i class="bi bi-chat-left-text"></i></span>
                  <input id="subject" type="text" class="form-control form-control-lg" formControlName="subject" placeholder="Objet de votre message" required />
                </div>
              </div>

              <!-- Champ Message -->
              <div class="mb-4">
                <label for="content" class="form-label fw-semibold">Message</label>
                <textarea id="content" class="form-control" formControlName="content" placeholder="Écrivez votre message ici..." rows="6" required></textarea>
              </div>

              <!-- Champ Images -->
              <div class="mb-4">
                <label for="images" class="form-label fw-semibold">Images jointes</label>
                <input id="images" type="file" class="form-control" accept="image/*" multiple (change)="onFileSelected($event)" aria-describedby="images-help" required />
                <div id="images-help" class="form-text">
                  <i class="bi bi-info-circle me-1" aria-hidden="true"></i> Vous pouvez sélectionner plusieurs images.
                </div>
              </div>

              <!-- Bouton de Soumission -->
              <button type="submit" class="btn btn-primary btn-lg w-100 fw-semibold" [disabled]="form.invalid">
                <i class="bi bi-send-fill me-2" aria-hidden="true"></i> Envoyer le message
              </button>

            </form>
          </div>

        </section>

      </div>
    </div>
  </div>
</main>

5. Les Styles CSS / SCSS (mail.scss)

Enfin, quelques touches de style pour embellir l’arrière-plan, les ombres et les champs de saisie.

:host {
  display: block;
}

.mail-page {
  min-height: 100vh;
  background: radial-gradient(circle at top left, rgb(13 110 253 / 12%), transparent 32rem), #f4f7fb;
}

.formulaire {
  border-radius: 1.25rem;
}

.formulaire__header {
  color: #fff;
  background: linear-gradient(135deg, #0d6efd, #084298);
}

.formulaire__icon {
  flex: 0 0 auto;
  width: 3.25rem;
  height: 3.25rem;
  color: #0d6efd;
  font-size: 1.35rem;
  background-color: #fff;
  border-radius: 1rem;
  box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 12%);
}

.form-label {
  color: #24324a;
}

.input-group-text {
  min-width: 3rem;
  justify-content: center;
  color: #0d6efd;
  background-color: #eef5ff;
}

.form-control,
.input-group-text {
  border-color: #ced8e5;
}

.form-control:focus {
  border-color: #86b7fe;
  box-shadow: 0 0 0 0.25rem rgb(13 110 253 / 16%);
}

textarea.form-control {
  min-height: 9rem;
  resize: vertical;
}

.btn-primary {
  min-height: 3.25rem;
  box-shadow: 0 0.5rem 1rem rgb(13 110 253 / 20%);
}

.btn-primary:disabled {
  box-shadow: none;
}

Conclusion

Vous disposez désormais d’un composant prêt à l’emploi, utilisant les dernières bonnes pratiques d’Angular 20 (comme l’injection de dépendances via inject()) couplées à Bootstrap 5 pour un design propre et responsive. Il ne vous reste plus qu’à connecter le tout à un backend (par exemple avec NestJS) capable de traiter les requêtes multipart/form-data !

Voici le lien pour le serveur backend