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
@@ -17,9 +17,8 @@
|
||||
</header>
|
||||
<form
|
||||
class="entity-form"
|
||||
#tenantForm="ngForm"
|
||||
ngNativeValidate
|
||||
(ngSubmit)="save(tenantForm)"
|
||||
[formRoot]="tenantForm"
|
||||
(ngSubmit)="save()"
|
||||
[attr.aria-busy]="saving()"
|
||||
>
|
||||
@if (saveError()) {
|
||||
@@ -34,29 +33,23 @@
|
||||
><mat-label>Prénom</mat-label
|
||||
><input
|
||||
matInput
|
||||
name="firstName"
|
||||
[ngModel]="firstName()"
|
||||
(ngModelChange)="firstName.set($event)"
|
||||
required
|
||||
id="firstName"
|
||||
[formField]="tenantForm.firstName"
|
||||
/></mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>Nom</mat-label
|
||||
><input
|
||||
matInput
|
||||
name="lastName"
|
||||
[ngModel]="lastName()"
|
||||
(ngModelChange)="lastName.set($event)"
|
||||
required
|
||||
id="lastName"
|
||||
[formField]="tenantForm.lastName"
|
||||
/></mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
><mat-label>E-mail</mat-label
|
||||
><input
|
||||
matInput
|
||||
type="email"
|
||||
name="email"
|
||||
[ngModel]="email()"
|
||||
(ngModelChange)="email.set($event)"
|
||||
required
|
||||
id="email"
|
||||
[formField]="tenantForm.email"
|
||||
autocomplete="email"
|
||||
/></mat-form-field>
|
||||
<mat-form-field appearance="outline"
|
||||
@@ -64,19 +57,16 @@
|
||||
><input
|
||||
matInput
|
||||
type="tel"
|
||||
name="phone"
|
||||
[ngModel]="phone()"
|
||||
(ngModelChange)="phone.set($event)"
|
||||
pattern="^(?:(?:0\d(?:[ .-]?\d){8})|(?:(?:\+|00)\d(?:[ .()-]?\d){7,14}))$"
|
||||
id="phone"
|
||||
[formField]="tenantForm.phone"
|
||||
autocomplete="tel"
|
||||
/></mat-form-field>
|
||||
<mat-form-field appearance="outline" class="full-field"
|
||||
><mat-label>Adresse complète</mat-label
|
||||
><input
|
||||
matInput
|
||||
name="address"
|
||||
[ngModel]="address()"
|
||||
(ngModelChange)="address.set($event)"
|
||||
id="address"
|
||||
[formField]="tenantForm.address"
|
||||
/></mat-form-field>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
|
||||
@@ -6,13 +6,17 @@ describe('TenantForm', () => {
|
||||
it('emits a trimmed tenant through its small interface', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
const component = fixture.componentInstance;
|
||||
component.firstName.set(' Camille ');
|
||||
component.lastName.set(' Martin ');
|
||||
component.email.set(' camille@example.com ');
|
||||
component.tenantModel.set({
|
||||
firstName: ' Camille ',
|
||||
lastName: ' Martin ',
|
||||
email: ' camille@example.com ',
|
||||
phone: '0600000000',
|
||||
address: '',
|
||||
});
|
||||
let firstName = '';
|
||||
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
|
||||
|
||||
component.save({ invalid: false } as never);
|
||||
component.save();
|
||||
|
||||
expect(firstName).toBe('Camille');
|
||||
});
|
||||
@@ -20,17 +24,23 @@ describe('TenantForm', () => {
|
||||
it('loads an existing tenant and leaves invalid data untouched', () => {
|
||||
const fixture = TestBed.createComponent(TenantForm);
|
||||
fixture.componentRef.setInput('tenant', {
|
||||
id: 'tenant-1', firstName: 'Camille', lastName: 'Martin', email: 'camille@example.com', phone: '0600000000', address: '1 rue de Paris',
|
||||
id: 'tenant-1',
|
||||
firstName: 'Camille',
|
||||
lastName: 'Martin',
|
||||
email: 'camille@example.com',
|
||||
phone: '0600000000',
|
||||
address: '1 rue de Paris',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
const markAllAsTouched = vi.fn();
|
||||
const submitted = vi.fn();
|
||||
fixture.componentInstance.submitted.subscribe(submitted);
|
||||
|
||||
fixture.componentInstance.save({ invalid: true, control: { markAllAsTouched } } as never);
|
||||
// Make the form invalid (invalid email)
|
||||
fixture.componentInstance.tenantModel.update((t) => ({ ...t, email: 'invalid-email' }));
|
||||
fixture.componentInstance.save();
|
||||
|
||||
expect(fixture.componentInstance.firstName()).toBe('Camille');
|
||||
expect(markAllAsTouched).toHaveBeenCalled();
|
||||
expect(fixture.componentInstance.tenantModel().firstName).toBe('Camille');
|
||||
expect(fixture.componentInstance.tenantForm().touched()).toBe(true);
|
||||
expect(submitted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, effect, input, output, signal } from '@angular/core';
|
||||
import { FormsModule, NgForm } from '@angular/forms';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormRoot, FormField, form, required, email } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
@@ -15,6 +16,8 @@ export interface TenantSaveRequest {
|
||||
selector: 'app-tenant-form',
|
||||
imports: [
|
||||
FormsModule,
|
||||
FormRoot,
|
||||
FormField,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
@@ -30,40 +33,61 @@ export class TenantForm {
|
||||
readonly cancelled = output<void>();
|
||||
readonly submitted = output<TenantSaveRequest>();
|
||||
|
||||
readonly firstName = signal('');
|
||||
readonly lastName = signal('');
|
||||
readonly email = signal('');
|
||||
readonly phone = signal('');
|
||||
readonly address = signal('');
|
||||
readonly tenantModel = signal<Tenant>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
readonly tenantForm = form(this.tenantModel, (f) => {
|
||||
required(f.firstName);
|
||||
required(f.lastName);
|
||||
required(f.email);
|
||||
email(f.email);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => this.fillForm(this.tenant()));
|
||||
}
|
||||
|
||||
save(form: NgForm): void {
|
||||
save(): void {
|
||||
if (this.saving()) return;
|
||||
if (form.invalid) {
|
||||
form.control.markAllAsTouched();
|
||||
this.tenantModel.update((m) => ({
|
||||
firstName: m.firstName.trim(),
|
||||
lastName: m.lastName.trim(),
|
||||
email: m.email.trim(),
|
||||
phone: m.phone.trim(),
|
||||
address: m.address.trim(),
|
||||
}));
|
||||
|
||||
this.tenantForm().markAsTouched();
|
||||
if (this.tenantForm().invalid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modelVal = this.tenantModel();
|
||||
this.submitted.emit({
|
||||
tenant: {
|
||||
firstName: this.firstName().trim(),
|
||||
lastName: this.lastName().trim(),
|
||||
email: this.email().trim(),
|
||||
phone: this.phone().trim(),
|
||||
address: this.address().trim(),
|
||||
firstName: modelVal.firstName,
|
||||
lastName: modelVal.lastName,
|
||||
email: modelVal.email,
|
||||
phone: modelVal.phone,
|
||||
address: modelVal.address,
|
||||
},
|
||||
tenantId: this.tenant()?.id,
|
||||
});
|
||||
}
|
||||
|
||||
private fillForm(tenant: Tenant | null): void {
|
||||
this.firstName.set(tenant?.firstName ?? '');
|
||||
this.lastName.set(tenant?.lastName ?? '');
|
||||
this.email.set(tenant?.email ?? '');
|
||||
this.phone.set(tenant?.phone ?? '');
|
||||
this.address.set(tenant?.address ?? '');
|
||||
this.tenantModel.set({
|
||||
id: tenant?.id,
|
||||
firstName: tenant?.firstName ?? '',
|
||||
lastName: tenant?.lastName ?? '',
|
||||
email: tenant?.email ?? '',
|
||||
phone: tenant?.phone ?? '',
|
||||
address: tenant?.address ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user