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('');
}
}
@@ -1,37 +1,30 @@
<form
class="guarantor-form"
#guarantorForm="ngForm"
ngNativeValidate
(ngSubmit)="save(guarantorForm)"
[formRoot]="guarantorForm"
(ngSubmit)="save()"
>
<div class="form-grid guarantor-fields">
<mat-form-field appearance="outline"
><mat-label>Prénom du garant</mat-label
><input
matInput
name="guarantorFirstName"
[ngModel]="firstName()"
(ngModelChange)="firstName.set($event)"
required
id="guarantorFirstName"
[formField]="guarantorForm.firstName"
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>Nom du garant</mat-label
><input
matInput
name="guarantorLastName"
[ngModel]="lastName()"
(ngModelChange)="lastName.set($event)"
required
id="guarantorLastName"
[formField]="guarantorForm.lastName"
/></mat-form-field>
<mat-form-field appearance="outline"
><mat-label>E-mail du garant</mat-label
><input
matInput
type="email"
name="guarantorEmail"
[ngModel]="email()"
(ngModelChange)="email.set($event)"
required
id="guarantorEmail"
[formField]="guarantorForm.email"
autocomplete="email"
/></mat-form-field>
<mat-form-field appearance="outline"
@@ -39,19 +32,16 @@
><input
matInput
type="tel"
name="guarantorPhone"
[ngModel]="phone()"
(ngModelChange)="phone.set($event)"
pattern="^(?:(?:0\d(?:[ .-]?\d){8})|(?:(?:\+|00)\d(?:[ .()-]?\d){7,14}))$"
id="guarantorPhone"
[formField]="guarantorForm.phone"
autocomplete="tel"
/></mat-form-field>
<mat-form-field appearance="outline" class="full-field"
><mat-label>Adresse du garant</mat-label
><input
matInput
name="guarantorAddress"
[ngModel]="address()"
(ngModelChange)="address.set($event)"
id="guarantorAddress"
[formField]="guarantorForm.address"
/></mat-form-field>
</div>
<button mat-stroked-button type="submit" class="add-guarantor">Ajouter le garant</button>
@@ -5,13 +5,12 @@ import { GuarantorForm } from './guarantor-form';
describe('GuarantorForm', () => {
it('marks an invalid form as touched without submitting it', () => {
const fixture = TestBed.createComponent(GuarantorForm);
const markAllAsTouched = vi.fn();
const submitted = vi.fn();
fixture.componentInstance.submitted.subscribe(submitted);
fixture.componentInstance.save({ invalid: true, control: { markAllAsTouched } } as never);
fixture.componentInstance.save();
expect(markAllAsTouched).toHaveBeenCalled();
expect(fixture.componentInstance.guarantorForm().touched()).toBe(true);
expect(submitted).not.toHaveBeenCalled();
});
@@ -19,20 +18,24 @@ describe('GuarantorForm', () => {
const fixture = TestBed.createComponent(GuarantorForm);
const component = fixture.componentInstance;
const submitted = vi.fn();
const resetForm = vi.fn();
component.submitted.subscribe(submitted);
component.firstName.set(' Alex ');
component.lastName.set(' Durand ');
component.email.set(' alex@example.com ');
component.phone.set(' 0600000000 ');
component.address.set(' 1 rue de Paris ');
component.guarantorModel.set({
firstName: ' Alex ',
lastName: ' Durand ',
email: ' alex@example.com ',
phone: ' 0600000000 ',
address: ' 1 rue de Paris ',
});
component.save({ invalid: false, resetForm } as never);
component.save();
expect(submitted).toHaveBeenCalledWith({
firstName: 'Alex', lastName: 'Durand', email: 'alex@example.com', phone: '0600000000', address: '1 rue de Paris',
firstName: 'Alex',
lastName: 'Durand',
email: 'alex@example.com',
phone: '0600000000',
address: '1 rue de Paris',
});
expect(component.firstName()).toBe('');
expect(resetForm).toHaveBeenCalled();
expect(component.guarantorModel().firstName).toBe('');
});
});
@@ -1,5 +1,6 @@
import { Component, 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';
@@ -7,35 +8,65 @@ import { Guarantor } from '../../services/lease.service';
@Component({
selector: 'app-guarantor-form',
imports: [FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule],
imports: [
FormsModule,
FormRoot,
FormField,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
],
templateUrl: './guarantor-form.html',
styleUrl: './guarantor-form.css',
})
export class GuarantorForm {
readonly submitted = output<Guarantor>();
readonly firstName = signal('');
readonly lastName = signal('');
readonly email = signal('');
readonly phone = signal('');
readonly address = signal('');
save(form: NgForm): void {
if (form.invalid) {
form.control.markAllAsTouched();
readonly guarantorModel = signal<Guarantor>({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
});
readonly guarantorForm = form(this.guarantorModel, (f) => {
required(f.firstName);
required(f.lastName);
required(f.email);
email(f.email);
});
save(): void {
this.guarantorModel.update((m) => ({
firstName: m.firstName.trim(),
lastName: m.lastName.trim(),
email: m.email.trim(),
phone: m.phone.trim(),
address: m.address.trim(),
}));
this.guarantorForm().markAsTouched();
if (this.guarantorForm().invalid()) {
return;
}
const modelVal = this.guarantorModel();
this.submitted.emit({
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,
});
this.firstName.set('');
this.lastName.set('');
this.email.set('');
this.phone.set('');
this.address.set('');
form.resetForm();
this.guarantorModel.set({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
});
this.guarantorForm().reset();
}
}
@@ -18,9 +18,8 @@
<form
id="lease-details"
class="lease-form"
#leaseForm="ngForm"
ngNativeValidate
(ngSubmit)="save(leaseForm)"
[formRoot]="leaseForm"
(ngSubmit)="save()"
[attr.aria-busy]="saving()"
>
@if (saveError()) {
@@ -45,10 +44,8 @@
<mat-form-field appearance="outline"
><mat-label>Appartement</mat-label
><mat-select
name="apartmentId"
[ngModel]="apartmentId()"
(ngModelChange)="apartmentId.set($event)"
required
id="apartmentId"
[formField]="leaseForm.apartmentId"
><mat-option value="">Sélectionnez un appartement</mat-option>
@for (apartment of apartments(); track apartment.id) {
<mat-option [value]="apartment.id">{{ apartment.name }}</mat-option>
@@ -58,11 +55,9 @@
<mat-form-field appearance="outline"
><mat-label>Locataires</mat-label
><mat-select
name="tenantIds"
[ngModel]="tenantIds()"
(ngModelChange)="tenantIds.set($event)"
id="tenantIds"
[formField]="leaseForm.tenantIds"
multiple
required
>
@for (tenant of tenants(); track tenant.id) {
<mat-option [value]="tenant.id"
@@ -86,12 +81,10 @@
><mat-label>Date de début</mat-label
><input
matInput
id="startDate"
[matDatepicker]="startDatePicker"
[matDatepickerFilter]="dateFilter"
name="startDate"
[ngModel]="startDate()"
(ngModelChange)="startDate.set($event)"
required /><mat-datepicker-toggle
[formField]="$any(leaseForm.startDate)" /><mat-datepicker-toggle
matIconSuffix
[for]="startDatePicker" /><mat-datepicker #startDatePicker
/></mat-form-field>
@@ -99,13 +92,11 @@
><mat-label>Date de fin</mat-label
><input
matInput
id="endDate"
[matDatepicker]="endDatePicker"
[matDatepickerFilter]="dateFilter"
[min]="startDate()"
name="endDate"
[ngModel]="endDate()"
(ngModelChange)="endDate.set($event)"
required /><mat-datepicker-toggle
[min]="leaseModel().startDate"
[formField]="$any(leaseForm.endDate)" /><mat-datepicker-toggle
matIconSuffix
[for]="endDatePicker" /><mat-datepicker #endDatePicker
/></mat-form-field>
@@ -114,11 +105,8 @@
><input
matInput
type="number"
name="monthlyRent"
[ngModel]="monthlyRent()"
(ngModelChange)="monthlyRent.set($event)"
min="1"
required
id="monthlyRent"
[formField]="leaseForm.monthlyRent"
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline"
@@ -126,11 +114,8 @@
><input
matInput
type="number"
name="monthlyCharges"
[ngModel]="monthlyCharges()"
(ngModelChange)="monthlyCharges.set($event)"
min="0"
required
id="monthlyCharges"
[formField]="leaseForm.monthlyCharges"
/><span matTextSuffix>€</span></mat-form-field
>
<mat-form-field appearance="outline"
@@ -138,11 +123,8 @@
><input
matInput
type="number"
name="securityDeposit"
[ngModel]="securityDeposit()"
(ngModelChange)="securityDeposit.set($event)"
min="0"
required
id="securityDeposit"
[formField]="leaseForm.securityDeposit"
/><span matTextSuffix>€</span></mat-form-field
>
</div>
@@ -9,17 +9,19 @@ describe('LeaseForm', () => {
fixture.componentRef.setInput('apartments', []);
fixture.componentRef.setInput('tenants', []);
const component = fixture.componentInstance;
component.apartmentId.set('apartment-1');
component.tenantIds.set(['tenant-1']);
component.startDate.set(new Date(2026, 8, 1));
component.endDate.set(new Date(2027, 7, 31));
component.monthlyRent.set(850);
component.monthlyCharges.set(50);
component.securityDeposit.set(850);
component.leaseModel.set({
apartmentId: 'apartment-1',
tenantIds: ['tenant-1'],
startDate: new Date(2026, 8, 1),
endDate: new Date(2027, 7, 31),
monthlyRent: 850,
monthlyCharges: 50,
securityDeposit: 850,
});
let startDate = '';
component.submitted.subscribe((lease) => (startDate = lease.startDate));
component.save({ invalid: false } as never);
component.save();
expect(startDate).toBe('2026-09-01');
});
@@ -1,5 +1,6 @@
import { Component, input, output, signal } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { FormRoot, FormField, form, required, min } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -15,6 +16,8 @@ import { GuarantorForm } from '../guarantor-form/guarantor-form';
selector: 'app-lease-form',
imports: [
FormsModule,
FormRoot,
FormField,
MatButtonModule,
MatDatepickerModule,
MatFormFieldModule,
@@ -34,24 +37,39 @@ export class LeaseForm {
readonly saveError = input('');
readonly cancelled = output<void>();
readonly submitted = output<Lease>();
readonly apartmentId = signal('');
readonly tenantIds = signal<string[]>([]);
readonly startDate = signal<Date | null>(null);
readonly endDate = signal<Date | null>(null);
readonly monthlyRent = signal(0);
readonly monthlyCharges = signal(0);
readonly securityDeposit = signal(0);
readonly leaseModel = signal({
apartmentId: '',
tenantIds: [] as string[],
startDate: null as Date | null,
endDate: null as Date | null,
monthlyRent: 0,
monthlyCharges: 0,
securityDeposit: 0,
});
readonly leaseForm = form(this.leaseModel, (f) => {
required(f.apartmentId);
required(f.tenantIds);
required(f.startDate);
required(f.endDate);
required(f.monthlyRent);
min(f.monthlyRent, 1);
required(f.monthlyCharges);
required(f.securityDeposit);
});
readonly guarantors = signal<Guarantor[]>([]);
readonly error = signal('');
readonly dateFilter = (d: Date | null): boolean => {
if (!d) return false;
const aptId = this.apartmentId();
const aptId = this.leaseModel().apartmentId;
if (!aptId) return true;
const time = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
const overlapping = this.leases().some(lease => {
const overlapping = this.leases().some((lease) => {
if (lease.apartmentId !== aptId) return false;
const [startYear, startMonth, startDay] = lease.startDate.split('-').map(Number);
const [endYear, endMonth, endDay] = lease.endDate.split('-').map(Number);
@@ -72,25 +90,26 @@ export class LeaseForm {
);
}
save(form: NgForm): void {
save(): void {
if (this.saving()) return;
if (form.invalid) {
form.control.markAllAsTouched();
this.leaseForm().markAsTouched();
if (this.leaseForm().invalid()) {
return;
}
if (this.endDate()! < this.startDate()!) {
const modelVal = this.leaseModel();
if (modelVal.endDate! < modelVal.startDate!) {
this.error.set('La date de fin doit être postérieure à la date de début.');
return;
}
this.submitted.emit({
apartmentId: this.apartmentId(),
tenantIds: this.tenantIds(),
apartmentId: modelVal.apartmentId,
tenantIds: modelVal.tenantIds,
guarantors: this.guarantors(),
startDate: this.toIsoDate(this.startDate()!),
endDate: this.toIsoDate(this.endDate()!),
monthlyRent: this.monthlyRent(),
monthlyCharges: this.monthlyCharges(),
securityDeposit: this.securityDeposit(),
startDate: this.toIsoDate(modelVal.startDate!),
endDate: this.toIsoDate(modelVal.endDate!),
monthlyRent: modelVal.monthlyRent,
monthlyCharges: modelVal.monthlyCharges,
securityDeposit: modelVal.securityDeposit,
status: 'ACTIVE',
});
}
@@ -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 ?? '',
});
}
}