Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 190a3a0

Browse files
committed
feat(@angular/cli): add initial set of signal form examples
This commit introduces a comprehensive set of 18 examples for the new signal-based forms functionality in Angular. These examples are designed to be used with the `find_examples` tool (MCP) to provide developers with actionable, real-world code snippets covering a wide range of use cases. The `find_examples` tool is currently experimental and must be enabled using the `-E find_examples` option when running the `ng mcp` command. The examples cover the following key concepts: - Core concepts and basic setup - Built-in and cross-field validation - Dynamic and conditional form structures - Custom form controls and reusable validators - Asynchronous operations and third-party integrations (Zod) - Best practices for testing, accessibility, and styling
1 parent 6552dcf commit 190a3a0

18 files changed

+3081
-0
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
title: Basic Signal Form
3+
summary: Creates a reactive link between a WritableSignal data model and native HTML inputs using the `form()` function and `[control]` directive.
4+
keywords:
5+
- signal forms
6+
- form
7+
- control
8+
- basic example
9+
- submit
10+
required_packages:
11+
- '@angular/forms'
12+
related_concepts:
13+
- 'signals'
14+
experimental: true
15+
---
16+
17+
## Purpose
18+
19+
The purpose of this pattern is to create a reactive, two-way link between a component's data model and its template. The problem it solves is keeping the state of the UI (the form inputs) perfectly synchronized with the application's state (the signal model) without manual state management, ensuring data consistency at all times.
20+
21+
## When to Use
22+
23+
Use this pattern as the starting point for any new form in an Angular application. It is the modern, preferred way to handle forms, superseding the traditional `FormsModule` (`NgModel`) and `ReactiveFormsModule` (`FormGroup`, `FormControl`) for most use cases by offering better type safety and a more reactive, signal-based architecture.
24+
25+
## Key Concepts
26+
27+
- **`form()`:** A function that creates a `Field` representing the form, taking a `WritableSignal` as the data model.
28+
- **`[control]` directive:** Binds a `Field` from your component to a native HTML input element, creating a two-way data binding.
29+
- **`submit()`:** An async helper function that manages the form's submission state and should be called from the submit event handler.
30+
31+
## Example Files
32+
33+
This example consists of a standalone component that defines and manages a user profile form.
34+
35+
### user-profile.component.ts
36+
37+
This file defines the component's logic, including the signal-based data model and the form structure.
38+
39+
```typescript
40+
import { Component, signal, output, ChangeDetectionStrategy } from '@angular/core';
41+
import { form, submit, Control } from '@angular/forms/signals';
42+
import { JsonPipe } from '@angular/common';
43+
44+
export interface UserProfile {
45+
firstName: string;
46+
lastName: string;
47+
email: string;
48+
}
49+
50+
@Component({
51+
selector: 'app-user-profile',
52+
imports: [JsonPipe, Control],
53+
templateUrl: './user-profile.component.html',
54+
changeDetection: ChangeDetectionStrategy.OnPush,
55+
})
56+
export class UserProfileComponent {
57+
readonly submitted = output<UserProfile>();
58+
59+
profileModel = signal<UserProfile>({
60+
firstName: '',
61+
lastName: '',
62+
email: '',
63+
});
64+
65+
profileForm = form(this.profileModel);
66+
67+
async handleSubmit() {
68+
// For this basic example, we assume the form is always valid for submission.
69+
// See the validation example for how to handle invalid forms.
70+
await submit(this.profileForm, async () => {
71+
this.submitted.emit(this.profileForm().value());
72+
});
73+
}
74+
}
75+
```
76+
77+
### user-profile.component.html
78+
79+
The template is wrapped in a `<form>` tag with a `(submit)` event and a submit button.
80+
81+
```html
82+
<form (submit)="handleSubmit(); $event.preventDefault()">
83+
<div>
84+
<label>
85+
First Name:
86+
<input type="text" [control]="profileForm.firstName" />
87+
</label>
88+
</div>
89+
<div>
90+
<label>
91+
Last Name:
92+
<input type="text" [control]="profileForm.lastName" />
93+
</label>
94+
</div>
95+
<div>
96+
<label>
97+
Email:
98+
<input type="email" [control]="profileForm.email" />
99+
</label>
100+
</div>
101+
102+
<button type="submit">Submit</button>
103+
</form>
104+
105+
<h3>Live Form Value:</h3>
106+
<pre>{{ profileForm().value() | json }}</pre>
107+
```
108+
109+
## Usage Notes
110+
111+
- The `[control]` directive automatically handles the two-way data binding between the input element and the corresponding `Field` in the `profileForm`.
112+
- The `profileForm()` signal gives you access to the `FieldState`, which includes the form's `value` as a signal.
113+
- The native `(submit)` event on the `<form>` element is bound to the `handleSubmit` method. It's important to call `$event.preventDefault()` to prevent a full page reload.
114+
- The `handleSubmit` method uses the `submit()` helper to manage the submission process.
115+
116+
## How to Use This Example
117+
118+
The parent component imports the `UserProfile` interface and listens for the `(submitted)` event to receive the strongly-typed form data.
119+
120+
```typescript
121+
// in app.component.ts
122+
import { Component } from '@angular/core';
123+
import { JsonPipe } from '@angular/common';
124+
import { UserProfileComponent, UserProfile } from './user-profile.component';
125+
126+
@Component({
127+
selector: 'app-root',
128+
imports: [UserProfileComponent, JsonPipe],
129+
template: `
130+
<h1>User Profile</h1>
131+
<app-user-profile (submitted)="onProfileSubmit($event)"></app-user-profile>
132+
133+
@if (submittedData) {
134+
<h2>Submitted Data:</h2>
135+
<pre>{{ submittedData | json }}</pre>
136+
}
137+
`,
138+
})
139+
export class AppComponent {
140+
submittedData: UserProfile | null = null;
141+
142+
onProfileSubmit(data: UserProfile) {
143+
this.submittedData = data;
144+
console.log('Profile data submitted:', data);
145+
}
146+
}
147+
```
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
---
2+
title: Signal Form with Built-in Validators
3+
summary: Demonstrates how to use the suite of built-in validators like `required`, `minLength`, `email`, and `pattern` within a signal form schema.
4+
keywords:
5+
- signal forms
6+
- validation
7+
- built-in validators
8+
- required
9+
- minLength
10+
- maxLength
11+
- min
12+
- max
13+
- email
14+
- pattern
15+
- schema
16+
required_packages:
17+
- '@angular/forms'
18+
related_concepts:
19+
- 'signals'
20+
experimental: true
21+
---
22+
23+
## Purpose
24+
25+
The purpose of this pattern is to apply common, standard validation rules to form fields with minimal boilerplate. It solves the problem of repeatedly implementing frequent checks (e.g., for required fields, email formats, or length constraints) by providing a pre-built, configurable, and optimized set of validators.
26+
27+
## When to Use
28+
29+
Use this pattern as the primary method for applying common validation logic in your signal forms. These built-in functions should be your first choice before writing custom validators, as they cover the vast majority of real-world validation scenarios. They are the modern, signal-based equivalent of the static methods on the `Validators` class from `@angular/forms`.
30+
31+
## Key Concepts
32+
33+
- **`required()`:** A built-in validator that ensures a field has a value.
34+
- **`minLength(number)`:** A built-in validator that ensures a string's length is at least the specified minimum.
35+
- **`email()`:** A built-in validator that checks if a string is in a valid email format.
36+
- **`submit()`:** An async helper function that manages the form's submission state and should be called from the submit event handler.
37+
38+
## Example Files
39+
40+
This example consists of a standalone component that defines and manages a user settings form with built-in validators.
41+
42+
### user-settings.component.ts
43+
44+
This file defines the component's logic, including the data model and a validation schema that uses multiple built-in validators.
45+
46+
```typescript
47+
import { Component, signal, output, ChangeDetectionStrategy } from '@angular/core';
48+
import { form, schema, submit, Control } from '@angular/forms/signals';
49+
import { required, minLength, maxLength, min, max, email, pattern } from '@angular/forms/signals';
50+
import { JsonPipe } from '@angular/common';
51+
52+
export interface UserSettings {
53+
username: string;
54+
age: number;
55+
email: string;
56+
website: string;
57+
}
58+
59+
const settingsSchema = schema<UserSettings>((form) => {
60+
required(form.username);
61+
minLength(form.username, 3);
62+
maxLength(form.username, 20);
63+
64+
required(form.age);
65+
min(form.age, 18);
66+
max(form.age, 100);
67+
68+
required(form.email);
69+
email(form.email);
70+
71+
pattern(form.website, /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/);
72+
});
73+
74+
@Component({
75+
selector: 'app-user-settings',
76+
imports: [JsonPipe, Control],
77+
templateUrl: './user-settings.component.html',
78+
changeDetection: ChangeDetectionStrategy.OnPush,
79+
})
80+
export class UserSettingsComponent {
81+
readonly submitted = output<UserSettings>();
82+
83+
settingsModel = signal<UserSettings>({
84+
username: '',
85+
age: 18,
86+
email: '',
87+
website: '',
88+
});
89+
90+
settingsForm = form(this.settingsModel, settingsSchema);
91+
92+
async handleSubmit() {
93+
await submit(this.settingsForm, async () => {
94+
this.submitted.emit(this.settingsForm().value());
95+
});
96+
}
97+
}
98+
```
99+
100+
### user-settings.component.html
101+
102+
This file provides the template for the form, displaying specific error messages for each potential validation failure.
103+
104+
```html
105+
<form (submit)="handleSubmit(); $event.preventDefault()">
106+
<div>
107+
<label>Username:</label>
108+
<input type="text" [control]="settingsForm.username" />
109+
@if (settingsForm.username().errors().length > 0) {
110+
<div class="errors">
111+
@for (error of settingsForm.username().errors(); track error) { @switch (error.kind) { @case
112+
('required') {
113+
<p>Username is required.</p>
114+
} @case ('minLength') {
115+
<p>Username must be at least 3 characters.</p>
116+
} @case ('maxLength') {
117+
<p>Username cannot exceed 20 characters.</p>
118+
} } }
119+
</div>
120+
}
121+
</div>
122+
123+
<div>
124+
<label>Age:</label>
125+
<input type="number" [control]="settingsForm.age" />
126+
@if (settingsForm.age().errors().length > 0) {
127+
<div class="errors">
128+
@for (error of settingsForm.age().errors(); track error) { @switch (error.kind) { @case
129+
('required') {
130+
<p>Age is required.</p>
131+
} @case ('min') {
132+
<p>You must be at least 18 years old.</p>
133+
} @case ('max') {
134+
<p>Age cannot be more than 100.</p>
135+
} } }
136+
</div>
137+
}
138+
</div>
139+
140+
<div>
141+
<label>Email:</label>
142+
<input type="email" [control]="settingsForm.email" />
143+
@if (settingsForm.email().errors().length > 0) {
144+
<div class="errors">
145+
@for (error of settingsForm.email().errors(); track error) { @switch (error.kind) { @case
146+
('required') {
147+
<p>Email is required.</p>
148+
} @case ('email') {
149+
<p>Please enter a valid email address.</p>
150+
} } }
151+
</div>
152+
}
153+
</div>
154+
155+
<div>
156+
<label>Website:</label>
157+
<input type="url" [control]="settingsForm.website" />
158+
@if (settingsForm.website().errors().length > 0) {
159+
<div class="errors">
160+
@for (error of settingsForm.website().errors(); track error) { @switch (error.kind) { @case
161+
('pattern') {
162+
<p>Please enter a valid URL.</p>
163+
} } }
164+
</div>
165+
}
166+
</div>
167+
168+
<button type="submit" [disabled]="!settingsForm().valid()">Save Settings</button>
169+
</form>
170+
```
171+
172+
## Usage Notes
173+
174+
- All built-in validators are imported directly from `@angular/forms`.
175+
- The native `(submit)` event on the `<form>` element is bound to the `handleSubmit` method. It's important to call `$event.preventDefault()` to prevent a full page reload.
176+
- The `handleSubmit` method uses the `submit()` helper to manage the submission process.
177+
178+
## How to Use This Example
179+
180+
The parent component listens for the `(submitted)` event to receive the validated form data.
181+
182+
```typescript
183+
// in app.component.ts
184+
import { Component } from '@angular/core';
185+
import { JsonPipe } from '@angular/common';
186+
import { UserSettingsComponent, UserSettings } from './user-settings.component';
187+
188+
@Component({
189+
selector: 'app-root',
190+
imports: [UserSettingsComponent, JsonPipe],
191+
template: `
192+
<h1>User Settings</h1>
193+
<app-user-settings (submitted)="onFormSubmit($event)"></app-user-settings>
194+
195+
@if (submittedData) {
196+
<h2>Submitted Data:</h2>
197+
<pre>{{ submittedData | json }}</pre>
198+
}
199+
`,
200+
})
201+
export class AppComponent {
202+
submittedData: UserSettings | null = null;
203+
204+
onFormSubmit(data: UserSettings) {
205+
this.submittedData = data;
206+
console.log('Settings data submitted:', data);
207+
}
208+
}
209+
```

0 commit comments

Comments
 (0)