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
+279
-211
No files matched your search
@@ -16,6 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<form
|
<form
|
||||||
|
[formRoot]="apartmentForm"
|
||||||
(submit)="save(); $event.preventDefault()"
|
(submit)="save(); $event.preventDefault()"
|
||||||
class="apartment-form"
|
class="apartment-form"
|
||||||
[attr.aria-busy]="saving()"
|
[attr.aria-busy]="saving()"
|
||||||
@@ -36,22 +37,17 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
id="name"
|
||||||
[ngModel]="name()"
|
[formField]="apartmentForm.name"
|
||||||
(ngModelChange)="name.set($event)"
|
|
||||||
placeholder="T2 Paris 15"
|
placeholder="T2 Paris 15"
|
||||||
required
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>Prix d'acquisition</mat-label
|
><mat-label>Prix d'acquisition</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
name="purchasePrice"
|
id="purchasePrice"
|
||||||
[ngModel]="purchasePrice()"
|
[formField]="apartmentForm.purchasePrice"
|
||||||
(ngModelChange)="purchasePrice.set($event)"
|
|
||||||
min="1"
|
|
||||||
required
|
|
||||||
/><span matTextSuffix>€</span></mat-form-field
|
/><span matTextSuffix>€</span></mat-form-field
|
||||||
>
|
>
|
||||||
<mat-form-field appearance="outline" class="full-field"
|
<mat-form-field appearance="outline" class="full-field"
|
||||||
@@ -59,19 +55,16 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="text"
|
type="text"
|
||||||
name="address"
|
id="address"
|
||||||
[ngModel]="address()"
|
[formField]="apartmentForm.address"
|
||||||
(ngModelChange)="address.set($event)"
|
|
||||||
placeholder="45 rue des Entrepreneurs, 75015 Paris"
|
placeholder="45 rue des Entrepreneurs, 75015 Paris"
|
||||||
required
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="full-field"
|
<mat-form-field appearance="outline" class="full-field"
|
||||||
><mat-label>Description ou notes</mat-label
|
><mat-label>Description ou notes</mat-label
|
||||||
><textarea
|
><textarea
|
||||||
matInput
|
matInput
|
||||||
name="description"
|
id="description"
|
||||||
[ngModel]="description()"
|
[formField]="apartmentForm.description"
|
||||||
(ngModelChange)="description.set($event)"
|
|
||||||
rows="4"
|
rows="4"
|
||||||
placeholder="Étage, agencement, équipements…"
|
placeholder="Étage, agencement, équipements…"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ describe('ApartmentForm', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const form = fixture.componentInstance;
|
const form = fixture.componentInstance;
|
||||||
expect(form.name()).toBe('T2 Paris 15');
|
expect(form.apartmentModel().name).toBe('T2 Paris 15');
|
||||||
expect(form.purchasePrice()).toBe(250000);
|
expect(form.apartmentModel().purchasePrice).toBe(250000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a validation error instead of emitting an incomplete apartment', () => {
|
it('shows a validation error instead of emitting an incomplete apartment', () => {
|
||||||
@@ -36,10 +36,12 @@ describe('ApartmentForm', () => {
|
|||||||
const form = fixture.componentInstance;
|
const form = fixture.componentInstance;
|
||||||
const saved = vi.fn();
|
const saved = vi.fn();
|
||||||
form.saved.subscribe(saved);
|
form.saved.subscribe(saved);
|
||||||
form.name.set(' T2 Paris 15 ');
|
form.apartmentModel.set({
|
||||||
form.address.set(' 45 rue des Entrepreneurs ');
|
name: ' T2 Paris 15 ',
|
||||||
form.purchasePrice.set(250000);
|
address: ' 45 rue des Entrepreneurs ',
|
||||||
form.description.set(' Cinquième étage ');
|
purchasePrice: 250000,
|
||||||
|
description: ' Cinquième étage ',
|
||||||
|
});
|
||||||
|
|
||||||
form.save();
|
form.save();
|
||||||
|
|
||||||
@@ -70,15 +72,18 @@ describe('ApartmentForm', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
const form = fixture.componentInstance;
|
const form = fixture.componentInstance;
|
||||||
form.name.set('T2 rénové');
|
form.apartmentModel.set({
|
||||||
form.address.set('45 rue des Entrepreneurs');
|
name: 'T2 rénové',
|
||||||
form.purchasePrice.set(265000);
|
address: '45 rue des Entrepreneurs',
|
||||||
|
purchasePrice: 265000,
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
|
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
|
|
||||||
expect(fixture.nativeElement.querySelector('input[name=name]').value).toBe('T2 rénové');
|
expect(fixture.nativeElement.querySelector('input#name').value).toBe('T2 rénové');
|
||||||
expect(fixture.nativeElement.querySelector('input[name=address]').value).toBe(
|
expect(fixture.nativeElement.querySelector('input#address').value).toBe(
|
||||||
'45 rue des Entrepreneurs',
|
'45 rue des Entrepreneurs',
|
||||||
);
|
);
|
||||||
expect(fixture.nativeElement.textContent).toContain(
|
expect(fixture.nativeElement.textContent).toContain(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, effect, input, output, signal } from '@angular/core';
|
import { Component, effect, input, output, signal } from '@angular/core';
|
||||||
import { FormsModule } 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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@@ -10,6 +11,8 @@ import { Apartment } from '../../services/apartment.service';
|
|||||||
selector: 'app-apartment-form',
|
selector: 'app-apartment-form',
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
FormRoot,
|
||||||
|
FormField,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
@@ -25,10 +28,20 @@ export class ApartmentForm {
|
|||||||
readonly closed = output<void>();
|
readonly closed = output<void>();
|
||||||
readonly saved = output<Apartment>();
|
readonly saved = output<Apartment>();
|
||||||
|
|
||||||
readonly name = signal('');
|
readonly apartmentModel = signal<Apartment>({
|
||||||
readonly address = signal('');
|
name: '',
|
||||||
readonly purchasePrice = signal(0);
|
address: '',
|
||||||
readonly description = signal('');
|
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('');
|
readonly error = signal('');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -37,24 +50,30 @@ export class ApartmentForm {
|
|||||||
|
|
||||||
save(): void {
|
save(): void {
|
||||||
if (this.saving()) return;
|
if (this.saving()) return;
|
||||||
if (!this.name().trim() || !this.address().trim() || this.purchasePrice() <= 0) {
|
this.apartmentForm().markAsTouched();
|
||||||
this.error.set('Veuillez remplir tous les champs obligatoires (Nom, Adresse, Prix > 0).');
|
if (this.apartmentForm().invalid()) {
|
||||||
|
const firstError = this.apartmentForm().errorSummary()[0];
|
||||||
|
this.error.set(firstError?.message ?? 'Veuillez corriger les erreurs.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saved.emit({
|
this.saved.emit({
|
||||||
name: this.name().trim(),
|
id: this.apartment()?.id,
|
||||||
address: this.address().trim(),
|
name: this.apartmentModel().name.trim(),
|
||||||
purchasePrice: this.purchasePrice(),
|
address: this.apartmentModel().address.trim(),
|
||||||
description: this.description().trim(),
|
purchasePrice: this.apartmentModel().purchasePrice,
|
||||||
|
description: this.apartmentModel().description.trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetForm(apartment: Apartment | null): void {
|
private resetForm(apartment: Apartment | null): void {
|
||||||
this.name.set(apartment?.name ?? '');
|
this.apartmentModel.set({
|
||||||
this.address.set(apartment?.address ?? '');
|
id: apartment?.id,
|
||||||
this.purchasePrice.set(apartment?.purchasePrice ?? 0);
|
name: apartment?.name ?? '',
|
||||||
this.description.set(apartment?.description ?? '');
|
address: apartment?.address ?? '',
|
||||||
|
purchasePrice: apartment?.purchasePrice ?? 0,
|
||||||
|
description: apartment?.description ?? '',
|
||||||
|
});
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,30 @@
|
|||||||
<form
|
<form
|
||||||
class="guarantor-form"
|
class="guarantor-form"
|
||||||
#guarantorForm="ngForm"
|
[formRoot]="guarantorForm"
|
||||||
ngNativeValidate
|
(ngSubmit)="save()"
|
||||||
(ngSubmit)="save(guarantorForm)"
|
|
||||||
>
|
>
|
||||||
<div class="form-grid guarantor-fields">
|
<div class="form-grid guarantor-fields">
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>Prénom du garant</mat-label
|
><mat-label>Prénom du garant</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="guarantorFirstName"
|
id="guarantorFirstName"
|
||||||
[ngModel]="firstName()"
|
[formField]="guarantorForm.firstName"
|
||||||
(ngModelChange)="firstName.set($event)"
|
|
||||||
required
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>Nom du garant</mat-label
|
><mat-label>Nom du garant</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="guarantorLastName"
|
id="guarantorLastName"
|
||||||
[ngModel]="lastName()"
|
[formField]="guarantorForm.lastName"
|
||||||
(ngModelChange)="lastName.set($event)"
|
|
||||||
required
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>E-mail du garant</mat-label
|
><mat-label>E-mail du garant</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="email"
|
type="email"
|
||||||
name="guarantorEmail"
|
id="guarantorEmail"
|
||||||
[ngModel]="email()"
|
[formField]="guarantorForm.email"
|
||||||
(ngModelChange)="email.set($event)"
|
|
||||||
required
|
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
@@ -39,19 +32,16 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="tel"
|
type="tel"
|
||||||
name="guarantorPhone"
|
id="guarantorPhone"
|
||||||
[ngModel]="phone()"
|
[formField]="guarantorForm.phone"
|
||||||
(ngModelChange)="phone.set($event)"
|
|
||||||
pattern="^(?:(?:0\d(?:[ .-]?\d){8})|(?:(?:\+|00)\d(?:[ .()-]?\d){7,14}))$"
|
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="full-field"
|
<mat-form-field appearance="outline" class="full-field"
|
||||||
><mat-label>Adresse du garant</mat-label
|
><mat-label>Adresse du garant</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="guarantorAddress"
|
id="guarantorAddress"
|
||||||
[ngModel]="address()"
|
[formField]="guarantorForm.address"
|
||||||
(ngModelChange)="address.set($event)"
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<button mat-stroked-button type="submit" class="add-guarantor">Ajouter le garant</button>
|
<button mat-stroked-button type="submit" class="add-guarantor">Ajouter le garant</button>
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { GuarantorForm } from './guarantor-form';
|
|||||||
describe('GuarantorForm', () => {
|
describe('GuarantorForm', () => {
|
||||||
it('marks an invalid form as touched without submitting it', () => {
|
it('marks an invalid form as touched without submitting it', () => {
|
||||||
const fixture = TestBed.createComponent(GuarantorForm);
|
const fixture = TestBed.createComponent(GuarantorForm);
|
||||||
const markAllAsTouched = vi.fn();
|
|
||||||
const submitted = vi.fn();
|
const submitted = vi.fn();
|
||||||
fixture.componentInstance.submitted.subscribe(submitted);
|
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();
|
expect(submitted).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,20 +18,24 @@ describe('GuarantorForm', () => {
|
|||||||
const fixture = TestBed.createComponent(GuarantorForm);
|
const fixture = TestBed.createComponent(GuarantorForm);
|
||||||
const component = fixture.componentInstance;
|
const component = fixture.componentInstance;
|
||||||
const submitted = vi.fn();
|
const submitted = vi.fn();
|
||||||
const resetForm = vi.fn();
|
|
||||||
component.submitted.subscribe(submitted);
|
component.submitted.subscribe(submitted);
|
||||||
component.firstName.set(' Alex ');
|
component.guarantorModel.set({
|
||||||
component.lastName.set(' Durand ');
|
firstName: ' Alex ',
|
||||||
component.email.set(' alex@example.com ');
|
lastName: ' Durand ',
|
||||||
component.phone.set(' 0600000000 ');
|
email: ' alex@example.com ',
|
||||||
component.address.set(' 1 rue de Paris ');
|
phone: ' 0600000000 ',
|
||||||
|
address: ' 1 rue de Paris ',
|
||||||
|
});
|
||||||
|
|
||||||
component.save({ invalid: false, resetForm } as never);
|
component.save();
|
||||||
|
|
||||||
expect(submitted).toHaveBeenCalledWith({
|
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(component.guarantorModel().firstName).toBe('');
|
||||||
expect(resetForm).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, output, signal } from '@angular/core';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@@ -7,35 +8,65 @@ import { Guarantor } from '../../services/lease.service';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-guarantor-form',
|
selector: 'app-guarantor-form',
|
||||||
imports: [FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule],
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
FormRoot,
|
||||||
|
FormField,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
],
|
||||||
templateUrl: './guarantor-form.html',
|
templateUrl: './guarantor-form.html',
|
||||||
styleUrl: './guarantor-form.css',
|
styleUrl: './guarantor-form.css',
|
||||||
})
|
})
|
||||||
export class GuarantorForm {
|
export class GuarantorForm {
|
||||||
readonly submitted = output<Guarantor>();
|
readonly submitted = output<Guarantor>();
|
||||||
readonly firstName = signal('');
|
|
||||||
readonly lastName = signal('');
|
|
||||||
readonly email = signal('');
|
|
||||||
readonly phone = signal('');
|
|
||||||
readonly address = signal('');
|
|
||||||
|
|
||||||
save(form: NgForm): void {
|
readonly guarantorModel = signal<Guarantor>({
|
||||||
if (form.invalid) {
|
firstName: '',
|
||||||
form.control.markAllAsTouched();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelVal = this.guarantorModel();
|
||||||
this.submitted.emit({
|
this.submitted.emit({
|
||||||
firstName: this.firstName().trim(),
|
firstName: modelVal.firstName,
|
||||||
lastName: this.lastName().trim(),
|
lastName: modelVal.lastName,
|
||||||
email: this.email().trim(),
|
email: modelVal.email,
|
||||||
phone: this.phone().trim(),
|
phone: modelVal.phone,
|
||||||
address: this.address().trim(),
|
address: modelVal.address,
|
||||||
});
|
});
|
||||||
this.firstName.set('');
|
|
||||||
this.lastName.set('');
|
this.guarantorModel.set({
|
||||||
this.email.set('');
|
firstName: '',
|
||||||
this.phone.set('');
|
lastName: '',
|
||||||
this.address.set('');
|
email: '',
|
||||||
form.resetForm();
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
});
|
||||||
|
this.guarantorForm().reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,9 +18,8 @@
|
|||||||
<form
|
<form
|
||||||
id="lease-details"
|
id="lease-details"
|
||||||
class="lease-form"
|
class="lease-form"
|
||||||
#leaseForm="ngForm"
|
[formRoot]="leaseForm"
|
||||||
ngNativeValidate
|
(ngSubmit)="save()"
|
||||||
(ngSubmit)="save(leaseForm)"
|
|
||||||
[attr.aria-busy]="saving()"
|
[attr.aria-busy]="saving()"
|
||||||
>
|
>
|
||||||
@if (saveError()) {
|
@if (saveError()) {
|
||||||
@@ -45,10 +44,8 @@
|
|||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>Appartement</mat-label
|
><mat-label>Appartement</mat-label
|
||||||
><mat-select
|
><mat-select
|
||||||
name="apartmentId"
|
id="apartmentId"
|
||||||
[ngModel]="apartmentId()"
|
[formField]="leaseForm.apartmentId"
|
||||||
(ngModelChange)="apartmentId.set($event)"
|
|
||||||
required
|
|
||||||
><mat-option value="">Sélectionnez un appartement</mat-option>
|
><mat-option value="">Sélectionnez un appartement</mat-option>
|
||||||
@for (apartment of apartments(); track apartment.id) {
|
@for (apartment of apartments(); track apartment.id) {
|
||||||
<mat-option [value]="apartment.id">{{ apartment.name }}</mat-option>
|
<mat-option [value]="apartment.id">{{ apartment.name }}</mat-option>
|
||||||
@@ -58,11 +55,9 @@
|
|||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>Locataires</mat-label
|
><mat-label>Locataires</mat-label
|
||||||
><mat-select
|
><mat-select
|
||||||
name="tenantIds"
|
id="tenantIds"
|
||||||
[ngModel]="tenantIds()"
|
[formField]="leaseForm.tenantIds"
|
||||||
(ngModelChange)="tenantIds.set($event)"
|
|
||||||
multiple
|
multiple
|
||||||
required
|
|
||||||
>
|
>
|
||||||
@for (tenant of tenants(); track tenant.id) {
|
@for (tenant of tenants(); track tenant.id) {
|
||||||
<mat-option [value]="tenant.id"
|
<mat-option [value]="tenant.id"
|
||||||
@@ -86,12 +81,10 @@
|
|||||||
><mat-label>Date de début</mat-label
|
><mat-label>Date de début</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
|
id="startDate"
|
||||||
[matDatepicker]="startDatePicker"
|
[matDatepicker]="startDatePicker"
|
||||||
[matDatepickerFilter]="dateFilter"
|
[matDatepickerFilter]="dateFilter"
|
||||||
name="startDate"
|
[formField]="$any(leaseForm.startDate)" /><mat-datepicker-toggle
|
||||||
[ngModel]="startDate()"
|
|
||||||
(ngModelChange)="startDate.set($event)"
|
|
||||||
required /><mat-datepicker-toggle
|
|
||||||
matIconSuffix
|
matIconSuffix
|
||||||
[for]="startDatePicker" /><mat-datepicker #startDatePicker
|
[for]="startDatePicker" /><mat-datepicker #startDatePicker
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
@@ -99,13 +92,11 @@
|
|||||||
><mat-label>Date de fin</mat-label
|
><mat-label>Date de fin</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
|
id="endDate"
|
||||||
[matDatepicker]="endDatePicker"
|
[matDatepicker]="endDatePicker"
|
||||||
[matDatepickerFilter]="dateFilter"
|
[matDatepickerFilter]="dateFilter"
|
||||||
[min]="startDate()"
|
[min]="leaseModel().startDate"
|
||||||
name="endDate"
|
[formField]="$any(leaseForm.endDate)" /><mat-datepicker-toggle
|
||||||
[ngModel]="endDate()"
|
|
||||||
(ngModelChange)="endDate.set($event)"
|
|
||||||
required /><mat-datepicker-toggle
|
|
||||||
matIconSuffix
|
matIconSuffix
|
||||||
[for]="endDatePicker" /><mat-datepicker #endDatePicker
|
[for]="endDatePicker" /><mat-datepicker #endDatePicker
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
@@ -114,11 +105,8 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
name="monthlyRent"
|
id="monthlyRent"
|
||||||
[ngModel]="monthlyRent()"
|
[formField]="leaseForm.monthlyRent"
|
||||||
(ngModelChange)="monthlyRent.set($event)"
|
|
||||||
min="1"
|
|
||||||
required
|
|
||||||
/><span matTextSuffix>€</span></mat-form-field
|
/><span matTextSuffix>€</span></mat-form-field
|
||||||
>
|
>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
@@ -126,11 +114,8 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
name="monthlyCharges"
|
id="monthlyCharges"
|
||||||
[ngModel]="monthlyCharges()"
|
[formField]="leaseForm.monthlyCharges"
|
||||||
(ngModelChange)="monthlyCharges.set($event)"
|
|
||||||
min="0"
|
|
||||||
required
|
|
||||||
/><span matTextSuffix>€</span></mat-form-field
|
/><span matTextSuffix>€</span></mat-form-field
|
||||||
>
|
>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
@@ -138,11 +123,8 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="number"
|
type="number"
|
||||||
name="securityDeposit"
|
id="securityDeposit"
|
||||||
[ngModel]="securityDeposit()"
|
[formField]="leaseForm.securityDeposit"
|
||||||
(ngModelChange)="securityDeposit.set($event)"
|
|
||||||
min="0"
|
|
||||||
required
|
|
||||||
/><span matTextSuffix>€</span></mat-form-field
|
/><span matTextSuffix>€</span></mat-form-field
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,17 +9,19 @@ describe('LeaseForm', () => {
|
|||||||
fixture.componentRef.setInput('apartments', []);
|
fixture.componentRef.setInput('apartments', []);
|
||||||
fixture.componentRef.setInput('tenants', []);
|
fixture.componentRef.setInput('tenants', []);
|
||||||
const component = fixture.componentInstance;
|
const component = fixture.componentInstance;
|
||||||
component.apartmentId.set('apartment-1');
|
component.leaseModel.set({
|
||||||
component.tenantIds.set(['tenant-1']);
|
apartmentId: 'apartment-1',
|
||||||
component.startDate.set(new Date(2026, 8, 1));
|
tenantIds: ['tenant-1'],
|
||||||
component.endDate.set(new Date(2027, 7, 31));
|
startDate: new Date(2026, 8, 1),
|
||||||
component.monthlyRent.set(850);
|
endDate: new Date(2027, 7, 31),
|
||||||
component.monthlyCharges.set(50);
|
monthlyRent: 850,
|
||||||
component.securityDeposit.set(850);
|
monthlyCharges: 50,
|
||||||
|
securityDeposit: 850,
|
||||||
|
});
|
||||||
let startDate = '';
|
let startDate = '';
|
||||||
component.submitted.subscribe((lease) => (startDate = lease.startDate));
|
component.submitted.subscribe((lease) => (startDate = lease.startDate));
|
||||||
|
|
||||||
component.save({ invalid: false } as never);
|
component.save();
|
||||||
|
|
||||||
expect(startDate).toBe('2026-09-01');
|
expect(startDate).toBe('2026-09-01');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, input, output, signal } from '@angular/core';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -15,6 +16,8 @@ import { GuarantorForm } from '../guarantor-form/guarantor-form';
|
|||||||
selector: 'app-lease-form',
|
selector: 'app-lease-form',
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
FormRoot,
|
||||||
|
FormField,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDatepickerModule,
|
MatDatepickerModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
@@ -34,24 +37,39 @@ export class LeaseForm {
|
|||||||
readonly saveError = input('');
|
readonly saveError = input('');
|
||||||
readonly cancelled = output<void>();
|
readonly cancelled = output<void>();
|
||||||
readonly submitted = output<Lease>();
|
readonly submitted = output<Lease>();
|
||||||
readonly apartmentId = signal('');
|
|
||||||
readonly tenantIds = signal<string[]>([]);
|
readonly leaseModel = signal({
|
||||||
readonly startDate = signal<Date | null>(null);
|
apartmentId: '',
|
||||||
readonly endDate = signal<Date | null>(null);
|
tenantIds: [] as string[],
|
||||||
readonly monthlyRent = signal(0);
|
startDate: null as Date | null,
|
||||||
readonly monthlyCharges = signal(0);
|
endDate: null as Date | null,
|
||||||
readonly securityDeposit = signal(0);
|
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 guarantors = signal<Guarantor[]>([]);
|
||||||
readonly error = signal('');
|
readonly error = signal('');
|
||||||
|
|
||||||
readonly dateFilter = (d: Date | null): boolean => {
|
readonly dateFilter = (d: Date | null): boolean => {
|
||||||
if (!d) return false;
|
if (!d) return false;
|
||||||
const aptId = this.apartmentId();
|
const aptId = this.leaseModel().apartmentId;
|
||||||
if (!aptId) return true;
|
if (!aptId) return true;
|
||||||
|
|
||||||
const time = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
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;
|
if (lease.apartmentId !== aptId) return false;
|
||||||
const [startYear, startMonth, startDay] = lease.startDate.split('-').map(Number);
|
const [startYear, startMonth, startDay] = lease.startDate.split('-').map(Number);
|
||||||
const [endYear, endMonth, endDay] = lease.endDate.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 (this.saving()) return;
|
||||||
if (form.invalid) {
|
this.leaseForm().markAsTouched();
|
||||||
form.control.markAllAsTouched();
|
if (this.leaseForm().invalid()) {
|
||||||
return;
|
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.');
|
this.error.set('La date de fin doit être postérieure à la date de début.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.submitted.emit({
|
this.submitted.emit({
|
||||||
apartmentId: this.apartmentId(),
|
apartmentId: modelVal.apartmentId,
|
||||||
tenantIds: this.tenantIds(),
|
tenantIds: modelVal.tenantIds,
|
||||||
guarantors: this.guarantors(),
|
guarantors: this.guarantors(),
|
||||||
startDate: this.toIsoDate(this.startDate()!),
|
startDate: this.toIsoDate(modelVal.startDate!),
|
||||||
endDate: this.toIsoDate(this.endDate()!),
|
endDate: this.toIsoDate(modelVal.endDate!),
|
||||||
monthlyRent: this.monthlyRent(),
|
monthlyRent: modelVal.monthlyRent,
|
||||||
monthlyCharges: this.monthlyCharges(),
|
monthlyCharges: modelVal.monthlyCharges,
|
||||||
securityDeposit: this.securityDeposit(),
|
securityDeposit: modelVal.securityDeposit,
|
||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,8 @@
|
|||||||
</header>
|
</header>
|
||||||
<form
|
<form
|
||||||
class="entity-form"
|
class="entity-form"
|
||||||
#tenantForm="ngForm"
|
[formRoot]="tenantForm"
|
||||||
ngNativeValidate
|
(ngSubmit)="save()"
|
||||||
(ngSubmit)="save(tenantForm)"
|
|
||||||
[attr.aria-busy]="saving()"
|
[attr.aria-busy]="saving()"
|
||||||
>
|
>
|
||||||
@if (saveError()) {
|
@if (saveError()) {
|
||||||
@@ -34,29 +33,23 @@
|
|||||||
><mat-label>Prénom</mat-label
|
><mat-label>Prénom</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="firstName"
|
id="firstName"
|
||||||
[ngModel]="firstName()"
|
[formField]="tenantForm.firstName"
|
||||||
(ngModelChange)="firstName.set($event)"
|
|
||||||
required
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>Nom</mat-label
|
><mat-label>Nom</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="lastName"
|
id="lastName"
|
||||||
[ngModel]="lastName()"
|
[formField]="tenantForm.lastName"
|
||||||
(ngModelChange)="lastName.set($event)"
|
|
||||||
required
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
><mat-label>E-mail</mat-label
|
><mat-label>E-mail</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
id="email"
|
||||||
[ngModel]="email()"
|
[formField]="tenantForm.email"
|
||||||
(ngModelChange)="email.set($event)"
|
|
||||||
required
|
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline"
|
<mat-form-field appearance="outline"
|
||||||
@@ -64,19 +57,16 @@
|
|||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
type="tel"
|
type="tel"
|
||||||
name="phone"
|
id="phone"
|
||||||
[ngModel]="phone()"
|
[formField]="tenantForm.phone"
|
||||||
(ngModelChange)="phone.set($event)"
|
|
||||||
pattern="^(?:(?:0\d(?:[ .-]?\d){8})|(?:(?:\+|00)\d(?:[ .()-]?\d){7,14}))$"
|
|
||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
<mat-form-field appearance="outline" class="full-field"
|
<mat-form-field appearance="outline" class="full-field"
|
||||||
><mat-label>Adresse complète</mat-label
|
><mat-label>Adresse complète</mat-label
|
||||||
><input
|
><input
|
||||||
matInput
|
matInput
|
||||||
name="address"
|
id="address"
|
||||||
[ngModel]="address()"
|
[formField]="tenantForm.address"
|
||||||
(ngModelChange)="address.set($event)"
|
|
||||||
/></mat-form-field>
|
/></mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ describe('TenantForm', () => {
|
|||||||
it('emits a trimmed tenant through its small interface', () => {
|
it('emits a trimmed tenant through its small interface', () => {
|
||||||
const fixture = TestBed.createComponent(TenantForm);
|
const fixture = TestBed.createComponent(TenantForm);
|
||||||
const component = fixture.componentInstance;
|
const component = fixture.componentInstance;
|
||||||
component.firstName.set(' Camille ');
|
component.tenantModel.set({
|
||||||
component.lastName.set(' Martin ');
|
firstName: ' Camille ',
|
||||||
component.email.set(' camille@example.com ');
|
lastName: ' Martin ',
|
||||||
|
email: ' camille@example.com ',
|
||||||
|
phone: '0600000000',
|
||||||
|
address: '',
|
||||||
|
});
|
||||||
let firstName = '';
|
let firstName = '';
|
||||||
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
|
component.submitted.subscribe((request) => (firstName = request.tenant.firstName));
|
||||||
|
|
||||||
component.save({ invalid: false } as never);
|
component.save();
|
||||||
|
|
||||||
expect(firstName).toBe('Camille');
|
expect(firstName).toBe('Camille');
|
||||||
});
|
});
|
||||||
@@ -20,17 +24,23 @@ describe('TenantForm', () => {
|
|||||||
it('loads an existing tenant and leaves invalid data untouched', () => {
|
it('loads an existing tenant and leaves invalid data untouched', () => {
|
||||||
const fixture = TestBed.createComponent(TenantForm);
|
const fixture = TestBed.createComponent(TenantForm);
|
||||||
fixture.componentRef.setInput('tenant', {
|
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();
|
fixture.detectChanges();
|
||||||
const markAllAsTouched = vi.fn();
|
|
||||||
const submitted = vi.fn();
|
const submitted = vi.fn();
|
||||||
fixture.componentInstance.submitted.subscribe(submitted);
|
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(fixture.componentInstance.tenantModel().firstName).toBe('Camille');
|
||||||
expect(markAllAsTouched).toHaveBeenCalled();
|
expect(fixture.componentInstance.tenantForm().touched()).toBe(true);
|
||||||
expect(submitted).not.toHaveBeenCalled();
|
expect(submitted).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, effect, input, output, signal } from '@angular/core';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
@@ -15,6 +16,8 @@ export interface TenantSaveRequest {
|
|||||||
selector: 'app-tenant-form',
|
selector: 'app-tenant-form',
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
FormRoot,
|
||||||
|
FormField,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
@@ -30,40 +33,61 @@ export class TenantForm {
|
|||||||
readonly cancelled = output<void>();
|
readonly cancelled = output<void>();
|
||||||
readonly submitted = output<TenantSaveRequest>();
|
readonly submitted = output<TenantSaveRequest>();
|
||||||
|
|
||||||
readonly firstName = signal('');
|
readonly tenantModel = signal<Tenant>({
|
||||||
readonly lastName = signal('');
|
firstName: '',
|
||||||
readonly email = signal('');
|
lastName: '',
|
||||||
readonly phone = signal('');
|
email: '',
|
||||||
readonly address = signal('');
|
phone: '',
|
||||||
|
address: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly tenantForm = form(this.tenantModel, (f) => {
|
||||||
|
required(f.firstName);
|
||||||
|
required(f.lastName);
|
||||||
|
required(f.email);
|
||||||
|
email(f.email);
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => this.fillForm(this.tenant()));
|
effect(() => this.fillForm(this.tenant()));
|
||||||
}
|
}
|
||||||
|
|
||||||
save(form: NgForm): void {
|
save(): void {
|
||||||
if (this.saving()) return;
|
if (this.saving()) return;
|
||||||
if (form.invalid) {
|
this.tenantModel.update((m) => ({
|
||||||
form.control.markAllAsTouched();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modelVal = this.tenantModel();
|
||||||
this.submitted.emit({
|
this.submitted.emit({
|
||||||
tenant: {
|
tenant: {
|
||||||
firstName: this.firstName().trim(),
|
firstName: modelVal.firstName,
|
||||||
lastName: this.lastName().trim(),
|
lastName: modelVal.lastName,
|
||||||
email: this.email().trim(),
|
email: modelVal.email,
|
||||||
phone: this.phone().trim(),
|
phone: modelVal.phone,
|
||||||
address: this.address().trim(),
|
address: modelVal.address,
|
||||||
},
|
},
|
||||||
tenantId: this.tenant()?.id,
|
tenantId: this.tenant()?.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private fillForm(tenant: Tenant | null): void {
|
private fillForm(tenant: Tenant | null): void {
|
||||||
this.firstName.set(tenant?.firstName ?? '');
|
this.tenantModel.set({
|
||||||
this.lastName.set(tenant?.lastName ?? '');
|
id: tenant?.id,
|
||||||
this.email.set(tenant?.email ?? '');
|
firstName: tenant?.firstName ?? '',
|
||||||
this.phone.set(tenant?.phone ?? '');
|
lastName: tenant?.lastName ?? '',
|
||||||
this.address.set(tenant?.address ?? '');
|
email: tenant?.email ?? '',
|
||||||
|
phone: tenant?.phone ?? '',
|
||||||
|
address: tenant?.address ?? '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user