mirror of https://github.com/abpframework/abp.git
Browse Source
Introduces a new community article detailing the experimental Signal-Based Forms API in Angular 21. The article covers motivations, type safety, two-way binding, validation, and practical usage examples, highlighting the advantages over traditional Reactive Forms. Includes a cover image for the article.pull/24198/head
2 changed files with 322 additions and 0 deletions
|
After Width: | Height: | Size: 1.6 MiB |
@ -0,0 +1,322 @@ |
|||||
|
# Signal-Based Forms in Angular 21: Why You’ll Never Miss Reactive Forms Again |
||||
|
|
||||
|
Angular 21 introduces one of the most exciting developments in the modern edition of Angular: **Signal-Based Forms**. Built directly on the reactive foundation of Angular signals, this new experimental API provides a cleaner, more intuitive, strongly typed, and ergonomic approach for managing form state—without the heavy boilerplate of Reactive Forms. |
||||
|
|
||||
|
> ⚠️ **Important:** Signal Forms are *experimental*. |
||||
|
> Their API can change. Avoid using them in critical production scenarios unless you understand the risks. |
||||
|
|
||||
|
Despite this, Signal Forms clearly represent Angular’s future direction. |
||||
|
--- |
||||
|
|
||||
|
## Why Signal Forms? |
||||
|
|
||||
|
Traditionally in Angular, building forms has involved several concerns: |
||||
|
|
||||
|
- Tracking values |
||||
|
- Managing UI interaction states (touched, dirty) |
||||
|
- Handling validation |
||||
|
- Keeping UI and model in sync |
||||
|
|
||||
|
Reactive Forms solved many challenges but introduced their own: |
||||
|
|
||||
|
- Verbosity FormBuilder API |
||||
|
- Required subscriptions (valueChanges) |
||||
|
- Manual cleaning |
||||
|
- Difficult nested forms |
||||
|
- Weak type-safety |
||||
|
|
||||
|
**Signal Forms solve these problems through:** |
||||
|
|
||||
|
1." Automatic synchronization |
||||
|
2." Full type safety |
||||
|
3." Schema-based validation |
||||
|
4." Fine-grained reactivity |
||||
|
5." Drastically reduced boilerplate |
||||
|
6." Natural integration with Angular Signals |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 1. Form Models — The Core of Signal Forms |
||||
|
|
||||
|
A **form model** is simply a writable signal holding the structure of your form data. |
||||
|
|
||||
|
```ts |
||||
|
import { Component, signal } from '@angular/core'; |
||||
|
import { form, Field } from '@angular/forms/signals'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-login', |
||||
|
imports: [Field], |
||||
|
template: ` |
||||
|
<input type="email" [field]="loginForm.email" /> |
||||
|
<input type="password" [field]="loginForm.password" /> |
||||
|
`, |
||||
|
}) |
||||
|
export class LoginComponent { |
||||
|
loginModel = signal({ |
||||
|
email: '', |
||||
|
password: '', |
||||
|
}); |
||||
|
|
||||
|
loginForm = form(this.loginModel); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Calling `form(model)` creates a **Field Tree** that maps directly to your model. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 2. Achieving Full Type Safety |
||||
|
|
||||
|
Although TypeScript can infer types from object literals, defining explicit interfaces provides maximum safety and better IDE support. |
||||
|
|
||||
|
```ts |
||||
|
interface LoginData { |
||||
|
email: string; |
||||
|
password: string; |
||||
|
} |
||||
|
|
||||
|
loginModel = signal<LoginData>({ |
||||
|
email: '', |
||||
|
password: '', |
||||
|
}); |
||||
|
|
||||
|
loginForm = form(loginModel); |
||||
|
``` |
||||
|
|
||||
|
Now: |
||||
|
|
||||
|
- `loginForm.email` → `FieldTree<string>` |
||||
|
- Accessing invalid fields like `loginForm.username` results in compile-time errors |
||||
|
|
||||
|
This level of type safety surpasses Reactive Forms. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 3. Reading Form Values |
||||
|
|
||||
|
#### Read from the model (entire form): |
||||
|
|
||||
|
```ts |
||||
|
onSubmit() { |
||||
|
const data = this.loginModel(); |
||||
|
console.log(data.email, data.password); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Read from an individual field: |
||||
|
|
||||
|
```html |
||||
|
<p>Current email: {{ loginForm.email().value() }}</p> |
||||
|
``` |
||||
|
|
||||
|
Each field exposes: |
||||
|
|
||||
|
- `value()` |
||||
|
- `valid()` |
||||
|
- `errors()` |
||||
|
- `dirty()` |
||||
|
- `touched()` |
||||
|
|
||||
|
All as signals. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 4. Updating Form Models Programmatically |
||||
|
|
||||
|
Signal Forms allow three update methods. |
||||
|
|
||||
|
#### 1. Replace the entire model |
||||
|
|
||||
|
```ts |
||||
|
this.userModel.set({ |
||||
|
name: 'Alice', |
||||
|
email: 'alice@example.com', |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
#### 2. Patch specific fields |
||||
|
|
||||
|
```ts |
||||
|
this.userModel.update(prev => ({ |
||||
|
...prev, |
||||
|
email: newEmail, |
||||
|
})); |
||||
|
``` |
||||
|
|
||||
|
#### 3. Update a single field |
||||
|
|
||||
|
```ts |
||||
|
this.userForm.email().value.set(''); |
||||
|
``` |
||||
|
|
||||
|
This eliminates the need for: |
||||
|
|
||||
|
- `patchValue()` |
||||
|
- `setValue()` |
||||
|
- `formGroup.get('field')` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 5. Automatic Two-Way Binding With `[field]` |
||||
|
|
||||
|
The `[field]` directive enables perfect two-way data binding: |
||||
|
|
||||
|
```html |
||||
|
<input [field]="userForm.name" /> |
||||
|
``` |
||||
|
|
||||
|
#### How it works: |
||||
|
|
||||
|
- **User input → Field state → Model** |
||||
|
- **Model updates → Field state → Input UI** |
||||
|
|
||||
|
No subscriptions. |
||||
|
No event handlers. |
||||
|
No boilerplate. |
||||
|
|
||||
|
Reactive Forms could never achieve this cleanly. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 6. Nested Models and Arrays |
||||
|
|
||||
|
Models can contain nested object structures: |
||||
|
|
||||
|
```ts |
||||
|
userModel = signal({ |
||||
|
name: '', |
||||
|
address: { |
||||
|
street: '', |
||||
|
city: '', |
||||
|
}, |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Access fields easily: |
||||
|
|
||||
|
```html |
||||
|
<input [field]="userForm.address.street" /> |
||||
|
``` |
||||
|
|
||||
|
Arrays are also supported: |
||||
|
|
||||
|
```ts |
||||
|
orderModel = signal({ |
||||
|
items: [ |
||||
|
{ product: '', quantity: 1, price: 0 } |
||||
|
] |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Field state persists even when array items move, thanks to identity tracking. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 7. Schema-Based Validation |
||||
|
|
||||
|
Validation is clean and centralized: |
||||
|
|
||||
|
```ts |
||||
|
import { required, email } from '@angular/forms/signals'; |
||||
|
|
||||
|
const model = signal({ email: '' }); |
||||
|
|
||||
|
const formRef = form(model, { |
||||
|
email: [required(), email()], |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
Field validation state is reactive: |
||||
|
|
||||
|
```ts |
||||
|
formRef.email().valid() |
||||
|
formRef.email().errors() |
||||
|
formRef.email().touched() |
||||
|
``` |
||||
|
|
||||
|
Validation no longer scatters across components. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 8. When Should You Use Signal Forms? |
||||
|
|
||||
|
#### New Angular 21+ apps |
||||
|
Signal-first architecture is the new standard. |
||||
|
|
||||
|
#### Teams wanting stronger type safety |
||||
|
Every field is exactly typed. |
||||
|
|
||||
|
#### Devs tired of Reactive Form boilerplate |
||||
|
Signal Forms drastically simplify code. |
||||
|
|
||||
|
#### Complex UI with computed reactive form state |
||||
|
Signals integrate perfectly. |
||||
|
|
||||
|
#### ❌ Avoid if: |
||||
|
- You need long-term stability |
||||
|
- You rely on mature Reactive Forms features |
||||
|
- Your app must avoid experimental APIs |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 9. Reactive Forms vs Signal Forms |
||||
|
|
||||
|
| Feature | Reactive Forms | Signal Forms | |
||||
|
|--------|----------------|--------------| |
||||
|
| Boilerplate | High | Very low | |
||||
|
| Type-safety | Weak | Strong | |
||||
|
| Two-way binding | Manual | Automatic | |
||||
|
| Validation | Scattered | Centralized schema | |
||||
|
| Nested forms | Verbose | Natural | |
||||
|
| Subscriptions | Required | None | |
||||
|
| Change detection | Zone-heavy | Fine-grained | |
||||
|
|
||||
|
Signal Forms feel like the "modern Angular mode," while Reactive Forms increasingly feel legacy. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 10. Full Example: Login Form |
||||
|
|
||||
|
```ts |
||||
|
@Component({ |
||||
|
selector: 'app-login', |
||||
|
imports: [Field], |
||||
|
template: ` |
||||
|
<form (ngSubmit)="submit()"> |
||||
|
<input type="email" [field]="form.email" /> |
||||
|
<input type="password" [field]="form.password" /> |
||||
|
<button>Login</button> |
||||
|
</form> |
||||
|
`, |
||||
|
}) |
||||
|
export class LoginComponent { |
||||
|
model = signal({ email: '', password: '' }); |
||||
|
form = form(this.model); |
||||
|
|
||||
|
submit() { |
||||
|
console.log(this.model()); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Minimal. Reactive. Completely type-safe. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## **Conclusion** |
||||
|
|
||||
|
Signal Forms in Angular 21 represent a big step forward: |
||||
|
|
||||
|
- Cleaner API |
||||
|
- Stronger type safety |
||||
|
- Automatic two-way binding |
||||
|
- Centralized validation |
||||
|
- Fine-grained reactivity |
||||
|
- Dramatically better developer experience |
||||
|
|
||||
|
|
||||
|
Although these are experimental, they clearly show the future of Angular's form ecosystem. |
||||
|
Once you get into using Signal Forms, you may never want to use Reactive Forms again. |
||||
|
|
||||
|
--- |
||||
Loading…
Reference in new issue