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 !