feat: refactor forms to use reactive forms with signals for improved state management
This commit is contained in:
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('');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user