|
| 1 | +<docs-decorative-header title="Forms with signals" imgSrc="adev/src/assets/images/signals.svg"> </docs-decorative-header> |
| 2 | + |
| 3 | +IMPORTANT: Signal Forms are [experimental](/reference/releases#experimental). The API may change in future releases. Avoid using experimental APIs in production applications without understanding the risks. |
| 4 | + |
| 5 | +Signal Forms manage form state using Angular signals to provide automatic synchronization between your data model and the UI with Angular Signals. |
| 6 | + |
| 7 | +This guide walks you through the core concepts to create forms with Signal Forms. Here's how it works: |
| 8 | + |
| 9 | +## Creating your first form |
| 10 | + |
| 11 | +### 1. Create a form model with `signal()` |
| 12 | + |
| 13 | +Every form starts by creating a signal that holds your form's data model: |
| 14 | + |
| 15 | +```ts |
| 16 | +interface LoginData { |
| 17 | + email: string; |
| 18 | + password: string; |
| 19 | +} |
| 20 | + |
| 21 | +const loginModel = signal<LoginData>({ |
| 22 | + email: '', |
| 23 | + password: '', |
| 24 | +}); |
| 25 | +``` |
| 26 | + |
| 27 | +### 2. Pass the form model to `form()` to create a `FieldTree` |
| 28 | + |
| 29 | +Then, you pass your form model into the `form()` function to create a **field tree** - an object structure that mirrors your model's shape, allowing you to access fields with dot notation: |
| 30 | + |
| 31 | +```ts |
| 32 | +const loginForm = form(loginModel); |
| 33 | + |
| 34 | +// Access fields directly by property name |
| 35 | +loginForm.email |
| 36 | +loginForm.password |
| 37 | +``` |
| 38 | + |
| 39 | +### 3. Bind HTML inputs with `[field]` directive |
| 40 | + |
| 41 | +Next, you bind your HTML inputs to the form using the `[field]` directive, which creates two-way binding between them: |
| 42 | + |
| 43 | +```html |
| 44 | +<input type="email" [field]="loginForm.email" /> |
| 45 | +<input type="password" [field]="loginForm.password" /> |
| 46 | +``` |
| 47 | + |
| 48 | +As a result, user changes (such as typing in the field) automatically updates the form. |
| 49 | + |
| 50 | +NOTE: The `[field]` directive also syncs field state for attributes like `required`, `disabled`, and `readonly` when appropriate. |
| 51 | + |
| 52 | +### 4. Read field values with `value()` |
| 53 | + |
| 54 | +You can access field state by calling the field as a function. This returns a `FieldState` object containing reactive signals for the field's value, validation status, and interaction state: |
| 55 | + |
| 56 | +```ts |
| 57 | +loginForm.email() // Returns FieldState with value(), valid(), touched(), etc. |
| 58 | +``` |
| 59 | + |
| 60 | +To read the field's current value, access the `value()` signal: |
| 61 | + |
| 62 | +```html |
| 63 | +<!-- Render form value that updates automatically as user types --> |
| 64 | +<p>Email: {{ loginForm.email().value() }}</p> |
| 65 | +``` |
| 66 | + |
| 67 | +```ts |
| 68 | +// Get the current value |
| 69 | +const currentEmail = loginForm.email().value(); |
| 70 | +``` |
| 71 | + |
| 72 | +### 5. Update field values with `set()` |
| 73 | + |
| 74 | +You can programmatically update a field's value using the `value.set()` method. This updates both the field and the underlying model signal: |
| 75 | + |
| 76 | +```ts |
| 77 | +// Update the value programmatically |
| 78 | +loginForm.email().value.set('alice@wonderland.com'); |
| 79 | +``` |
| 80 | + |
| 81 | +As a result, both the field value and the model signal are updated automatically: |
| 82 | + |
| 83 | +```ts |
| 84 | +// The model signal is also updated |
| 85 | +console.log(loginModel().email); // 'alice@wonderland.com' |
| 86 | +``` |
| 87 | + |
| 88 | +Here's a complete example: |
| 89 | + |
| 90 | +<docs-code-multifile preview path="adev/src/content/examples/signal-forms/src/login-simple/app/app.ts"> |
| 91 | + <docs-code header="app.ts" path="adev/src/content/examples/signal-forms/src/login-simple/app/app.ts"/> |
| 92 | + <docs-code header="app.html" path="adev/src/content/examples/signal-forms/src/login-simple/app/app.html"/> |
| 93 | + <docs-code header="app.css" path="adev/src/content/examples/signal-forms/src/login-simple/app/app.css"/> |
| 94 | +</docs-code-multifile> |
| 95 | + |
| 96 | +## Basic usage |
| 97 | + |
| 98 | +The `[field]` directive works with all standard HTML input types. Here are the most common patterns: |
| 99 | + |
| 100 | +### Text inputs |
| 101 | + |
| 102 | +Text inputs work with various `type` attributes and textareas: |
| 103 | + |
| 104 | +```html |
| 105 | +<!-- Text and email --> |
| 106 | +<input type="text" [field]="form.name" /> |
| 107 | +<input type="email" [field]="form.email" /> |
| 108 | +``` |
| 109 | + |
| 110 | +#### Numbers |
| 111 | + |
| 112 | +Number inputs automatically convert between strings and numbers: |
| 113 | + |
| 114 | +```html |
| 115 | +<!-- Number - automatically converts to number type --> |
| 116 | +<input type="number" [field]="form.age" /> |
| 117 | +``` |
| 118 | + |
| 119 | +#### Date and time |
| 120 | + |
| 121 | +Date inputs store values as `YYYY-MM-DD` strings, and time inputs use `HH:mm` format: |
| 122 | + |
| 123 | +```html |
| 124 | +<!-- Date and time - stores as ISO format strings --> |
| 125 | +<input type="date" [field]="form.eventDate" /> |
| 126 | +<input type="time" [field]="form.eventTime" /> |
| 127 | +``` |
| 128 | + |
| 129 | +If you need to convert date strings to Date objects, you can do so by passing the field value into `Date()`: |
| 130 | + |
| 131 | +```ts |
| 132 | +const dateObject = new Date(form.eventDate().value()); |
| 133 | +``` |
| 134 | + |
| 135 | +#### Multiline text |
| 136 | + |
| 137 | +Textareas work the same way as text inputs: |
| 138 | + |
| 139 | +```html |
| 140 | +<!-- Textarea --> |
| 141 | +<textarea [field]="form.message" rows="4"></textarea> |
| 142 | +``` |
| 143 | + |
| 144 | +### Checkboxes |
| 145 | + |
| 146 | +Checkboxes bind to boolean values: |
| 147 | + |
| 148 | +```html |
| 149 | +<!-- Single checkbox --> |
| 150 | +<label> |
| 151 | + <input type="checkbox" [field]="form.agreeToTerms" /> |
| 152 | + I agree to the terms |
| 153 | +</label> |
| 154 | +``` |
| 155 | + |
| 156 | +#### Multiple checkboxes |
| 157 | + |
| 158 | +For multiple options, create a separate boolean `field` for each: |
| 159 | + |
| 160 | +```html |
| 161 | +<label> |
| 162 | + <input type="checkbox" [field]="form.emailNotifications" /> |
| 163 | + Email notifications |
| 164 | +</label> |
| 165 | +<label> |
| 166 | + <input type="checkbox" [field]="form.smsNotifications" /> |
| 167 | + SMS notifications |
| 168 | +</label> |
| 169 | +``` |
| 170 | + |
| 171 | +### Radio buttons |
| 172 | + |
| 173 | +Radio buttons work similarly to checkboxes. As long as the radio buttons use the same `[field]` value, Signal Forms will automatically bind the same `name` attribute to all of them: |
| 174 | + |
| 175 | +```html |
| 176 | +<label> |
| 177 | + <input type="radio" value="free" [field]="form.plan" /> |
| 178 | + Free |
| 179 | +</label> |
| 180 | +<label> |
| 181 | + <input type="radio" value="premium" [field]="form.plan" /> |
| 182 | + Premium |
| 183 | +</label> |
| 184 | +``` |
| 185 | + |
| 186 | +When a user selects a radio button, the form `field` stores the value from that radio button's `value` attribute. For example, selecting "Premium" sets `form.plan().value()` to `"premium"`. |
| 187 | + |
| 188 | +### Select dropdowns |
| 189 | + |
| 190 | +Select elements work with both static and dynamic options: |
| 191 | + |
| 192 | +```html |
| 193 | +<!-- Static options --> |
| 194 | +<select [field]="form.country"> |
| 195 | + <option value="">Select a country</option> |
| 196 | + <option value="us">United States</option> |
| 197 | + <option value="ca">Canada</option> |
| 198 | +</select> |
| 199 | + |
| 200 | +<!-- Dynamic options with @for --> |
| 201 | +<select [field]="form.productId"> |
| 202 | + <option value="">Select a product</option> |
| 203 | + @for (product of products; track product.id) { |
| 204 | + <option [value]="product.id">{{ product.name }}</option> |
| 205 | + } |
| 206 | +</select> |
| 207 | +``` |
| 208 | + |
| 209 | +NOTE: Multiple select (`<select multiple>`) is not supported by the `[field]` directive at this time. |
| 210 | + |
| 211 | +## Validation and state |
| 212 | + |
| 213 | +Signal Forms provides built-in validators that you can apply to your form fields. To add validation, pass a schema function as the second argument to `form()`: |
| 214 | + |
| 215 | +```ts |
| 216 | +const loginForm = form(loginModel, (schemaPath) => { |
| 217 | + debounce(schemaPath.email, 500); |
| 218 | + required(schemaPath.email); |
| 219 | + email(schemaPath.email); |
| 220 | +}); |
| 221 | +``` |
| 222 | + |
| 223 | +The schema function receives a **schema path** parameter that provides paths to your fields for configuring validation rules. |
| 224 | + |
| 225 | +Common validators include: |
| 226 | + |
| 227 | +- **`required()`** - Ensures the field has a value |
| 228 | +- **`email()`** - Validates email format |
| 229 | +- **`min()`** / **`max()`** - Validates number ranges |
| 230 | +- **`minLength()`** / **`maxLength()`** - Validates string or collection length |
| 231 | +- **`pattern()`** - Validates against a regex pattern |
| 232 | + |
| 233 | +You can also customize error messages by passing an options object as the second argument to the validator: |
| 234 | + |
| 235 | +```ts |
| 236 | +required(schemaPath.email, { message: 'Email is required' }); |
| 237 | +email(schemaPath.email, { message: 'Please enter a valid email address' }); |
| 238 | +``` |
| 239 | + |
| 240 | +Each form field exposes its validation state through signals. For example, you can check `field().valid()` to see if validation passes, `field().touched()` to see if the user has interacted with it, and `field().errors()` to get the list of validation errors. |
| 241 | + |
| 242 | +Here's a complete example: |
| 243 | + |
| 244 | +<docs-code-multifile preview path="adev/src/content/examples/signal-forms/src/login-validation/app/app.ts"> |
| 245 | + <docs-code header="app.ts" path="adev/src/content/examples/signal-forms/src/login-validation/app/app.ts"/> |
| 246 | + <docs-code header="app.html" path="adev/src/content/examples/signal-forms/src/login-validation/app/app.html"/> |
| 247 | + <docs-code header="app.css" path="adev/src/content/examples/signal-forms/src/login-validation/app/app.css"/> |
| 248 | +</docs-code-multifile> |
| 249 | + |
| 250 | +### Field State Signals |
| 251 | + |
| 252 | +Every `field()` provides these state signals: |
| 253 | + |
| 254 | +| State | Description | |
| 255 | +| ------------ | -------------------------------------------------------------------------- | |
| 256 | +| `valid()` | Returns `true` if the field passes all validation rules | |
| 257 | +| `touched()` | Returns `true` if the user has focused and blurred the field | |
| 258 | +| `dirty()` | Returns `true` if the user has changed the value | |
| 259 | +| `disabled()` | Returns `true` if the field is disabled | |
| 260 | +| `readonly()` | Returns `true` if the field is readonly | |
| 261 | +| `pending()` | Returns `true` if async validation is in progress | |
| 262 | +| `errors()` | Returns an array of validation errors with `kind` and `message` properties | |
| 263 | + |
| 264 | +## Next steps |
| 265 | + |
| 266 | +To learn more about Signal Forms and how it works, check out the in-depth guides: |
| 267 | + |
| 268 | +- [Overview](guide/forms/signals/overview) - Introduction to Signal Forms and when to use them |
| 269 | +- [Form models](guide/forms/signals/models) - Creating and managing form data with signals |
| 270 | +- [Field state management](guide/forms/signals/field-state-management) - Working with validation state, interaction tracking, and field visibility |
| 271 | +- [Validation](guide/forms/signals/validation) - Built-in validators, custom validation rules, and async validation |
0 commit comments