Skip to content

Commit 158db6c

Browse files
committed
feat(challenge 65): refactor user form component from reactive form to signal form
1 parent 543770b commit 158db6c

2 files changed

Lines changed: 76 additions & 63 deletions

File tree

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

Lines changed: 74 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
import {
22
ChangeDetectionStrategy,
33
Component,
4-
effect,
54
inject,
65
input,
6+
linkedSignal,
77
} from '@angular/core';
88
import { rxResource } from '@angular/core/rxjs-interop';
9-
import {
10-
FormControl,
11-
FormGroup,
12-
ReactiveFormsModule,
13-
Validators,
14-
} from '@angular/forms';
9+
import { form, FormField, min, required, submit } from '@angular/forms/signals';
1510
import { Router } from '@angular/router';
1611
import { of } from 'rxjs';
1712
import { FakeBackendService } from './fake-backend.service';
13+
import { EditingUser } from './user.model';
1814

1915
@Component({
2016
selector: 'app-user-form',
21-
imports: [ReactiveFormsModule],
17+
imports: [FormField],
2218
template: `
2319
<div class="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
2420
<h2 class="mb-4 text-xl font-semibold text-gray-800">
@@ -30,7 +26,7 @@ import { FakeBackendService } from './fake-backend.service';
3026
class="h-8 w-8 animate-spin rounded-full border-4 border-indigo-500 border-t-transparent"></div>
3127
</div>
3228
} @else {
33-
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="space-y-4">
29+
<form (submit)="onSubmit($event)" class="space-y-4">
3430
<div>
3531
<label
3632
for="firstname"
@@ -40,13 +36,14 @@ import { FakeBackendService } from './fake-backend.service';
4036
<input
4137
id="firstname"
4238
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" />
39+
[formField]="userForm.firstname"
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" />
4541
@if (
46-
userForm.get('firstname')?.invalid &&
47-
userForm.get('firstname')?.touched
42+
userForm.firstname().invalid() && userForm.firstname().touched()
4843
) {
49-
<p class="mt-1 text-xs text-red-500">Firstname is required</p>
44+
@for (error of userForm.firstname().errors(); track error) {
45+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
46+
}
5047
}
5148
</div>
5249
<div>
@@ -58,13 +55,14 @@ import { FakeBackendService } from './fake-backend.service';
5855
<input
5956
id="lastname"
6057
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" />
58+
[formField]="userForm.lastname"
59+
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" />
6360
@if (
64-
userForm.get('lastname')?.invalid &&
65-
userForm.get('lastname')?.touched
61+
userForm.lastname().invalid() && userForm.lastname().touched()
6662
) {
67-
<p class="mt-1 text-xs text-red-500">Lastname is required</p>
63+
@for (error of userForm.lastname().errors(); track error) {
64+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
65+
}
6866
}
6967
</div>
7068
<div>
@@ -74,10 +72,12 @@ import { FakeBackendService } from './fake-backend.service';
7472
<input
7573
id="age"
7674
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>
75+
[formField]="userForm.age"
76+
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" />
77+
@if (userForm.age().invalid() && userForm.age().touched()) {
78+
@for (error of userForm.age().errors(); track error) {
79+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
80+
}
8181
}
8282
</div>
8383
<div>
@@ -87,20 +87,25 @@ import { FakeBackendService } from './fake-backend.service';
8787
<input
8888
id="grade"
8989
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" />
90+
[formField]="userForm.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:ring-indigo-500 focus:outline-none sm:text-sm" />
92+
@if (userForm.grade().invalid() && userForm.grade().touched()) {
93+
@for (error of userForm.grade().errors(); track error) {
94+
<p class="mt-1 text-xs text-red-500">{{ error.message }}</p>
95+
}
96+
}
9297
</div>
9398
<div class="flex justify-end space-x-3 pt-4">
9499
<button
95100
type="button"
96101
(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">
102+
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">
98103
Cancel
99104
</button>
100105
<button
101106
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">
107+
[disabled]="userForm().invalid()"
108+
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">
104109
{{ id() ? 'Update' : 'Add' }}
105110
</button>
106111
</div>
@@ -124,50 +129,56 @@ export class UserFormComponent {
124129
defaultValue: undefined,
125130
});
126131

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-
}),
132+
userModel = linkedSignal<EditingUser>(() => {
133+
const user = this.userResource.value();
134+
const defaultUser: EditingUser = {
135+
firstname: '',
136+
lastname: '',
137+
age: 0,
138+
grade: 0,
139+
};
140+
141+
return user
142+
? {
143+
firstname: user.firstname,
144+
lastname: user.lastname,
145+
age: user.age,
146+
grade: user.grade,
147+
}
148+
: defaultUser;
144149
});
145150

146-
constructor() {
147-
effect(() => {
148-
const userValue = this.userResource.value();
149-
if (userValue) {
150-
this.userForm.patchValue(userValue);
151-
} else {
152-
this.userForm.reset({ firstname: '', lastname: '', age: 0, grade: 0 });
153-
}
154-
});
155-
}
151+
userForm = form(this.userModel, (schemaPath) => {
152+
required(schemaPath.firstname, { message: 'First name is required' });
153+
required(schemaPath.lastname, { message: 'Last name is required' });
154+
required(schemaPath.age, { message: 'Age is required' });
155+
min(schemaPath.age, 0, { message: 'Age must be positive' });
156+
required(schemaPath.grade, { message: 'Grade is required' });
157+
});
156158

157-
onSubmit(): void {
158-
if (this.userForm.valid) {
159+
onSubmit(event: Event): void {
160+
event.preventDefault();
161+
162+
submit(this.userForm, async () => {
159163
const userValue = this.userResource.value();
160-
const obs = userValue
164+
const formValue = this.userModel();
165+
const request = userValue
161166
? this.backend.updateUser({
162-
...this.userForm.getRawValue(),
167+
...formValue,
163168
id: userValue.id,
164169
})
165-
: this.backend.addUser(this.userForm.getRawValue());
170+
: this.backend.addUser(formValue);
166171

167-
obs.subscribe(() => {
168-
this.router.navigate(['/']);
172+
request.subscribe({
173+
next: () => {
174+
this.router.navigate(['/']);
175+
},
176+
error: (error) => {
177+
console.error('Error saving user:', error);
178+
// Here you could set an error state to display an error message in the UI
179+
},
169180
});
170-
}
181+
});
171182
}
172183

173184
onCancel(): void {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export interface User {
55
age: number;
66
grade: number;
77
}
8+
9+
export type EditingUser = Omit<User, 'id'>;

0 commit comments

Comments
 (0)