Skip to content

Commit 5113d8b

Browse files
committed
feat(Answer:65): refactor to use signal form
1 parent 543770b commit 5113d8b

1 file changed

Lines changed: 69 additions & 58 deletions

File tree

apps/forms/65-signal-form-edition/src/app/user-form.component.ts

Lines changed: 69 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,18 @@ import {
44
effect,
55
inject,
66
input,
7+
signal,
78
} from '@angular/core';
89
import { rxResource } from '@angular/core/rxjs-interop';
9-
import {
10-
FormControl,
11-
FormGroup,
12-
ReactiveFormsModule,
13-
Validators,
14-
} from '@angular/forms';
10+
import { form, FormField, min, required, submit } from '@angular/forms/signals';
1511
import { Router } from '@angular/router';
16-
import { of } from 'rxjs';
12+
import { lastValueFrom, of } from 'rxjs';
1713
import { FakeBackendService } from './fake-backend.service';
14+
import { User } from './user.model';
1815

1916
@Component({
2017
selector: 'app-user-form',
21-
imports: [ReactiveFormsModule],
18+
imports: [FormField],
2219
template: `
2320
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
2421
<h2 class="mb-4 text-xl font-semibold text-gray-800">
@@ -30,7 +27,7 @@ import { FakeBackendService } from './fake-backend.service';
3027
class="h-8 w-8 animate-spin rounded-full border-4 border-indigo-500 border-t-transparent"></div>
3128
</div>
3229
} @else {
33-
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="space-y-4">
30+
<form (submit)="onSubmit($event)" class="space-y-4">
3431
<div>
3532
<label
3633
for="firstname"
@@ -40,15 +37,18 @@ import { FakeBackendService } from './fake-backend.service';
4037
<input
4138
id="firstname"
4239
type="text"
43-
formControlName="firstname"
44-
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" />
40+
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none sm:text-sm"
41+
[formField]="userSignalForm.firstname" />
4542
@if (
46-
userForm.get('firstname')?.invalid &&
47-
userForm.get('firstname')?.touched
43+
userSignalForm.firstname().invalid() &&
44+
userSignalForm.firstname().touched()
4845
) {
49-
<p class="mt-1 text-xs text-red-500">Firstname is required</p>
46+
@for (error of userSignalForm.firstname().errors(); track error) {
47+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
48+
}
5049
}
5150
</div>
51+
5252
<div>
5353
<label
5454
for="lastname"
@@ -58,49 +58,59 @@ import { FakeBackendService } from './fake-backend.service';
5858
<input
5959
id="lastname"
6060
type="text"
61-
formControlName="lastname"
62-
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" />
61+
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none sm:text-sm"
62+
[formField]="userSignalForm.lastname" />
6363
@if (
64-
userForm.get('lastname')?.invalid &&
65-
userForm.get('lastname')?.touched
64+
userSignalForm.lastname().invalid() &&
65+
userSignalForm.lastname().touched()
6666
) {
67-
<p class="mt-1 text-xs text-red-500">Lastname is required</p>
67+
@for (error of userSignalForm.lastname().errors(); track error) {
68+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
69+
}
6870
}
6971
</div>
72+
7073
<div>
7174
<label for="age" class="block text-sm font-medium text-gray-700">
7275
Age
7376
</label>
7477
<input
7578
id="age"
7679
type="number"
77-
formControlName="age"
78-
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" />
79-
@if (userForm.get('age')?.invalid && userForm.get('age')?.touched) {
80-
<p class="mt-1 text-xs text-red-500">Age must be positive</p>
80+
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none sm:text-sm"
81+
[formField]="userSignalForm.age" />
82+
@if (
83+
userSignalForm.age().invalid() && userSignalForm.age().touched()
84+
) {
85+
@for (error of userSignalForm.age().errors(); track error) {
86+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
87+
}
8188
}
8289
</div>
90+
8391
<div>
8492
<label for="grade" class="block text-sm font-medium text-gray-700">
8593
Grade
8694
</label>
8795
<input
8896
id="grade"
8997
type="number"
90-
formControlName="grade"
91-
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" />
98+
class="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none sm:text-sm"
99+
[formField]="userSignalForm.grade" />
92100
</div>
101+
93102
<div class="flex justify-end space-x-3 pt-4">
94103
<button
95104
type="button"
96105
(click)="onCancel()"
97-
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
106+
class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none">
98107
Cancel
99108
</button>
109+
100110
<button
101111
type="submit"
102-
[disabled]="userForm.invalid"
103-
class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50">
112+
[disabled]="userSignalForm().invalid()"
113+
class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:opacity-50">
104114
{{ id() ? 'Update' : 'Add' }}
105115
</button>
106116
</div>
@@ -124,50 +134,51 @@ export class UserFormComponent {
124134
defaultValue: undefined,
125135
});
126136

127-
userForm = new FormGroup({
128-
firstname: new FormControl('', {
129-
nonNullable: true,
130-
validators: [Validators.required],
131-
}),
132-
lastname: new FormControl('', {
133-
nonNullable: true,
134-
validators: [Validators.required],
135-
}),
136-
age: new FormControl(0, {
137-
nonNullable: true,
138-
validators: [Validators.required, Validators.min(0)],
139-
}),
140-
grade: new FormControl(0, {
141-
nonNullable: true,
142-
validators: [Validators.required],
143-
}),
137+
userModel = signal<Omit<User, 'id'>>({
138+
firstname: '',
139+
lastname: '',
140+
age: 0,
141+
grade: 0,
142+
});
143+
144+
userSignalForm = form(this.userModel, (schemePath) => {
145+
required(schemePath.firstname, { message: 'Firstname is required' });
146+
required(schemePath.lastname, { message: 'Lastname is required' });
147+
required(schemePath.age);
148+
min(schemePath.age, 0, { message: 'Age must be positive' });
149+
required(schemePath.grade);
144150
});
145151

146152
constructor() {
147153
effect(() => {
148154
const userValue = this.userResource.value();
149155
if (userValue) {
150-
this.userForm.patchValue(userValue);
156+
this.userModel.set(userValue);
151157
} else {
152-
this.userForm.reset({ firstname: '', lastname: '', age: 0, grade: 0 });
158+
this.userModel.set({ firstname: '', lastname: '', age: 0, grade: 0 });
153159
}
154160
});
155161
}
156162

157-
onSubmit(): void {
158-
if (this.userForm.valid) {
163+
onSubmit(event: Event): void {
164+
event.preventDefault();
165+
166+
submit(this.userSignalForm, async () => {
159167
const userValue = this.userResource.value();
160-
const obs = userValue
161-
? this.backend.updateUser({
162-
...this.userForm.getRawValue(),
168+
169+
if (userValue) {
170+
await lastValueFrom(
171+
this.backend.updateUser({
172+
...this.userModel(),
163173
id: userValue.id,
164-
})
165-
: this.backend.addUser(this.userForm.getRawValue());
174+
}),
175+
);
176+
} else {
177+
await lastValueFrom(this.backend.addUser(this.userModel()));
178+
}
166179

167-
obs.subscribe(() => {
168-
this.router.navigate(['/']);
169-
});
170-
}
180+
this.router.navigate(['/']);
181+
});
171182
}
172183

173184
onCancel(): void {

0 commit comments

Comments
 (0)