feat: refactor forms to use reactive forms with signals for improved state management

This commit is contained in:
nessar committed 2026-08-17 14:18:15 +02:00
1 parent 5d2abf4184
commit ac5a7942c9
12 files changed
+281 -213

No files matched your search

@@ -16,6 +16,7 @@
</div>
</header>
<form
[formRoot]="apartmentForm"
(submit)="save(); $event.preventDefault()"
class="apartment-form"
[attr.aria-busy]="saving()"
@@ -36,22 +37,17 @@
><input
matInput
type="text"
name="name"
[ngModel]="name()"
(ngModelChange)="name.set($event)"
id="name"
[formField]="apartmentForm.name"
placeholder="T2 Paris 15"
required
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Prix d'acquisition</mat-label
><input
matInput
type="number"
name="purchasePrice"
[ngModel]="purchasePrice()"
(ngModelChange)="purchasePrice.set($event)"
min="1"
required
id="purchasePrice"
[formField]="apartmentForm.purchasePrice"
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline" class="full-field"
@@ -59,19 +55,16 @@
><input
matInput
type="text"
name="address"
[ngModel]="address()"
(ngModelChange)="address.set($event)"
id="address"
[formField]="apartmentForm.address"
placeholder="45 rue des Entrepreneurs, 75015 Paris"
required
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Description ou notes</mat-label
><textarea
matInput
name="description"
[ngModel]="description()"
(ngModelChange)="description.set($event)"
id="description"
[formField]="apartmentForm.description"
rows="4"
placeholder="Étage, agencement, équipements…"
></textarea>
@@ -15,8 +15,8 @@ describe('ApartmentForm', () => {
fixture.detectChanges();
const form = fixture.componentInstance;
expect(form.name()).toBe('T2 Paris 15');
expect(form.purchasePrice()).toBe(250000);
expect(form.apartmentModel().name).toBe('T2 Paris 15');
expect(form.apartmentModel().purchasePrice).toBe(250000);
});
it('shows a validation error instead of emitting an incomplete apartment', () => {
@@ -36,10 +36,12 @@ describe('ApartmentForm', () => {
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.name.set(' T2 Paris 15 ');
form.address.set(' 45 rue des Entrepreneurs ');
form.purchasePrice.set(250000);
form.description.set(' Cinquième étage ');
form.apartmentModel.set({
name: ' T2 Paris 15 ',
address: ' 45 rue des Entrepreneurs ',
purchasePrice: 250000,
description: ' Cinquième étage ',
});
form.save();
@@ -70,15 +72,18 @@ describe('ApartmentForm', () => {
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
form.name.set('T2 rénové');
form.address.set('45 rue des Entrepreneurs');
form.purchasePrice.set(265000);
form.apartmentModel.set({
name: 'T2 rénové',
address: '45 rue des Entrepreneurs',
purchasePrice: 265000,
description: '',
});
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('input[name=name]').value).toBe('T2 rénové');
expect(fixture.nativeElement.querySelector('input[name=address]').value).toBe(
expect(fixture.nativeElement.querySelector('input#name').value).toBe('T2 rénové');
expect(fixture.nativeElement.querySelector('input#address').value).toBe(
'45 rue des Entrepreneurs',
);
expect(fixture.nativeElement.textContent).toContain(
@@ -1,5 +1,6 @@
import { Component, effect, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormRoot, FormField, form, required, min } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@@ -10,6 +11,8 @@ import { Apartment } from '../../services/apartment.service';
selector: 'app-apartment-form',
imports: [
FormsModule,
FormRoot,
FormField,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
@@ -25,10 +28,20 @@ export class ApartmentForm {
readonly closed = output<void>();
readonly saved = output<Apartment>();
readonly name = signal('');
readonly address = signal('');
readonly purchasePrice = signal(0);
readonly description = signal('');
readonly apartmentModel = signal<Apartment>({
name: '',
address: '',
purchasePrice: 0,
description: '',
});
readonly apartmentForm = form(this.apartmentModel, (f) => {
required(f.name, { message: 'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).' });
required(f.address, { message: 'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).' });
required(f.purchasePrice, { message: 'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).' });
min(f.purchasePrice, 1, { message: 'Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).' });
});
readonly error = signal('');
constructor() {
@@ -37,24 +50,30 @@ export class ApartmentForm {
save(): void {
if (this.saving()) return;
if (!this.name().trim() || !this.address().trim() || this.purchasePrice() <= 0) {
this.error.set('Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).');
this.apartmentForm().markAsTouched();
if (this.apartmentForm().invalid()) {
const firstError = this.apartmentForm().errorSummary()[0];
this.error.set(firstError?.message ?? 'Veuillez corriger les erreurs.');
return;
}
this.saved.emit({
name: this.name().trim(),
address: this.address().trim(),
purchasePrice: this.purchasePrice(),
description: this.description().trim(),
id: this.apartment()?.id,
name: this.apartmentModel().name.trim(),
address: this.apartmentModel().address.trim(),
purchasePrice: this.apartmentModel().purchasePrice,
description: this.apartmentModel().description.trim(),
});
}
private resetForm(apartment: Apartment | null): void {
this.name.set(apartment?.name ?? '');
this.address.set(apartment?.address ?? '');
this.purchasePrice.set(apartment?.purchasePrice ?? 0);
this.description.set(apartment?.description ?? '');
this.apartmentModel.set({
id: apartment?.id,
name: apartment?.name ?? '',
address: apartment?.address ?? '',
purchasePrice: apartment?.purchasePrice ?? 0,
description: apartment?.description ?? '',
});
this.error.set('');
}
}