11import { JsonPipe } from '@angular/common' ;
2- import { Component , signal , WritableSignal } from '@angular/core' ;
3- import {
4- FormControl ,
5- FormGroup ,
6- ReactiveFormsModule ,
7- Validators ,
8- } from '@angular/forms' ;
2+ import { Component , signal } from '@angular/core' ;
3+ import { Field , form , max , min , required } from '@angular/forms/signals' ;
4+
5+ interface FormData {
6+ name : string ;
7+ lastname : string ;
8+ age : number ;
9+ note : string ;
10+ }
911
1012@Component ( {
1113 selector : 'app-root' ,
12- imports : [ ReactiveFormsModule , JsonPipe ] ,
14+ imports : [ JsonPipe , Field ] ,
1315 template : `
1416 <div class="min-h-screen bg-gray-100 px-4 py-12 sm:px-6 lg:px-8">
1517 <div class="mx-auto max-w-md rounded-lg bg-white p-8 shadow-md">
1618 <h1 class="mb-6 text-3xl font-bold text-gray-900">Simple Form</h1>
1719
18- <form [formGroup]="form" (ngSubmit )="onSubmit()" class="space-y-6">
20+ <form (submit )="onSubmit()" class="space-y-6">
1921 <div>
2022 <label
2123 for="name"
@@ -26,14 +28,18 @@ import {
2628 <input
2729 id="name"
2830 type="text"
29- formControlName=" name"
31+ [field]="form. name"
3032 placeholder="Enter your name"
3133 class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
3234 [class.border-red-500]="
33- form.controls. name.invalid && ! form.controls. name.untouched
35+ form.name() .invalid() && form.name().touched()
3436 " />
35- @if (form.controls.name.invalid && !form.controls.name.untouched) {
36- <p class="mt-1 text-sm text-red-600">Name is required</p>
37+ @if (form.name().invalid() && form.name().touched()) {
38+ <p class="mt-1 text-sm text-red-600">
39+ @for (error of form.name().errors(); track error) {
40+ {{ error.message }}
41+ }
42+ </p>
3743 }
3844 </div>
3945
@@ -46,7 +52,7 @@ import {
4652 <input
4753 id="lastname"
4854 type="text"
49- formControlName=" lastname"
55+ [field]="form. lastname"
5056 placeholder="Enter your last name"
5157 class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />
5258 </div>
@@ -60,21 +66,16 @@ import {
6066 <input
6167 id="age"
6268 type="number"
63- formControlName=" age"
69+ [field]="form. age"
6470 placeholder="Enter your age (1-99)"
65- min="1"
66- max="99"
6771 class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
6872 [class.border-red-500]="
69- form.controls. age.invalid && ! form.controls. age.untouched
73+ form.age() .invalid() && form.age().touched()
7074 " />
71- @if (form.controls. age.invalid && ! form.controls. age.untouched ) {
75+ @if (form.age() .invalid() && form.age().touched() ) {
7276 <p class="mt-1 text-sm text-red-600">
73- @if (form.controls.age.hasError('min')) {
74- Age must be at least 1
75- }
76- @if (form.controls.age.hasError('max')) {
77- Age must be at most 99
77+ @for (error of form.age().errors(); track error) {
78+ {{ error.message }}
7879 }
7980 </p>
8081 }
@@ -89,15 +90,15 @@ import {
8990 <input
9091 id="note"
9192 type="text"
92- formControlName=" note"
93+ [field]="form. note"
9394 placeholder="Enter a note"
9495 class="w-full rounded-md border border-gray-300 px-4 py-2 outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500" />
9596 </div>
9697
9798 <div class="flex gap-4">
9899 <button
99100 type="submit"
100- [disabled]="form.invalid"
101+ [disabled]="form() .invalid() "
101102 class="flex-1 rounded-md bg-blue-600 px-4 py-2 font-medium text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400">
102103 Submit
103104 </button>
@@ -126,35 +127,37 @@ import {
126127 ` ,
127128} )
128129export class AppComponent {
129- form = new FormGroup ( {
130- name : new FormControl ( '' , {
131- validators : Validators . required ,
132- nonNullable : true ,
133- } ) ,
134- lastname : new FormControl ( '' , { nonNullable : true } ) ,
135- age : new FormControl < number | null > ( null , [
136- Validators . min ( 1 ) ,
137- Validators . max ( 99 ) ,
138- ] ) ,
139- note : new FormControl ( '' , { nonNullable : true } ) ,
130+ // we cannot use null for initial value of number field because null is not supported by native input[type="number"]
131+ // https://github.com/angular/angular/issues/65454
132+ formModel = signal < FormData > ( {
133+ name : '' ,
134+ lastname : '' ,
135+ age : NaN ,
136+ note : '' ,
137+ } ) ;
138+
139+ form = form < FormData > ( this . formModel , ( path ) => {
140+ required ( path . name , { message : 'Name is required' } ) ;
141+ min ( path . age , 1 , { message : 'Age must be at least 1' } ) ;
142+ max ( path . age , 99 , { message : 'Age must be at most 99' } ) ;
140143 } ) ;
141144
142- submittedData : WritableSignal < {
143- name : string ;
144- lastname : string ;
145- age : number | null ;
146- note : string ;
147- } | null > = signal ( null ) ;
145+ submittedData = signal < FormData | null > ( null ) ;
148146
149147 onSubmit ( ) : void {
150- if ( this . form . valid ) {
151- this . submittedData . set ( this . form . getRawValue ( ) ) ;
152- console . log ( 'Form submitted:' , this . submittedData ) ;
148+ if ( this . form ( ) . valid ( ) ) {
149+ this . submittedData . set ( this . formModel ( ) ) ;
150+ console . log ( 'Form submitted:' , this . submittedData ( ) ) ;
153151 }
154152 }
155153
156154 onReset ( ) : void {
157- this . form . reset ( ) ;
155+ this . formModel . set ( {
156+ name : '' ,
157+ lastname : '' ,
158+ age : NaN ,
159+ note : '' ,
160+ } ) ;
158161 this . submittedData . set ( null ) ;
159162 }
160163}
0 commit comments