Skip to content

Commit 4eaf4a0

Browse files
committed
fixup! docs(forms): add initial signal forms examples
1 parent fc699e4 commit 4eaf4a0

File tree

6 files changed

+560
-70
lines changed

6 files changed

+560
-70
lines changed

packages/forms/signals/examples/03-cross-field-validation.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
title: Signal Form with Cross-Field Validation
3-
summary: Implements a cross-field validator using `validateTree` to ensure two fields in a signal form are consistent with each other.
3+
summary: Implements a cross-field validator by applying a validator to one field that reactively depends on the value of another field.
44
keywords:
55
- signal forms
66
- form
77
- control
88
- validation
99
- cross-field validation
10-
- validateTree
10+
- valueOf
1111
- schema
1212
- minLength
1313
- submit
@@ -23,25 +23,25 @@ The purpose of this pattern is to implement validation logic that depends on the
2323

2424
## When to Use
2525

26-
Use this pattern when the validity of one form field depends on the value of another. This is common in registration forms, password change forms, and any form where two fields must be consistent with each other. `validateTree` is the signal-based equivalent of a cross-field validator on a `FormGroup` in `ReactiveFormsModule`.
26+
Use this pattern when the validity of one form field depends on the value of another. This is common in registration forms, password change forms, and any form where two fields must be consistent with each other. The recommended approach is to apply a validator to the field where the error should be reported (e.g., `confirmPassword`) and use the `valueOf` function in the validation context to reactively get the value of the other field it depends on (e.g., `password`).
2727

2828
## Key Concepts
2929

30-
- **`validateTree()`:** A function used within a schema to add a validator to a form group or the entire form.
30+
- **`validate()`:** A function used within a schema to add a validator to a specific field.
31+
- **`valueOf()`:** A function available in the validation context that allows you to reactively access the value of any other field in the form.
3132
- **`valid` signal:** A signal on the `FieldState` that is `true` only when the field and all its descendants are valid.
32-
- **`(ngSubmit)`:** An event binding on a `<form>` element that triggers a method when the form is submitted.
3333

3434
## Example Files
3535

3636
This example consists of a standalone component that defines and manages a password change form.
3737

3838
### password-form.component.ts
3939

40-
This file defines the component's logic, including a schema with a `validateTree` function for cross-field validation.
40+
This file defines the component's logic. The schema applies a validator to `confirmPassword` that compares its value to the value of the `password` field.
4141

4242
```typescript
4343
import { Component, signal, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
44-
import { form, schema, validate, validateTree } from '@angular/forms/signals';
44+
import { form, schema, validate } from '@angular/forms/signals';
4545
import { JsonPipe } from '@angular/common';
4646

4747
export interface PasswordForm {
@@ -56,9 +56,9 @@ const passwordSchema = schema<PasswordForm>((passwordForm) => {
5656
return null;
5757
});
5858

59-
validateTree(passwordForm, ({value, fieldOf}) => {
60-
if (value.password !== value.confirmPassword) {
61-
return [{ field: fieldOf(passwordForm.confirmPassword), error: { mismatch: true } }];
59+
validate(passwordForm.confirmPassword, ({value, valueOf}) => {
60+
if (value !== valueOf(passwordForm.password)) {
61+
return { mismatch: true };
6262
}
6363
return null;
6464
});

packages/forms/signals/examples/04-conditional-validation.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
title: Signal Form with Conditional Validation
3-
summary: Applies a validator to a signal form field only when a specific condition is met by using the `validateWhen` function.
3+
summary: Applies a validator to a signal form field only when a specific condition is met by using the `applyWhen` function.
44
keywords:
55
- signal forms
66
- form
77
- control
88
- validation
99
- conditional validation
10-
- validateWhen
10+
- applyWhen
1111
- schema
1212
- submit
1313
required_packages:
@@ -26,7 +26,7 @@ Use this pattern when a validation rule for one field depends on the state of an
2626

2727
## Key Concepts
2828

29-
- **`validateWhen()`:** A function used within a schema to apply a validator conditionally.
29+
- **`applyWhen()`:** A function used within a schema to apply a validator conditionally.
3030
- **`valid` signal:** A signal on the `FieldState` that is `true` only when the field and all its descendants are valid.
3131
- **`(ngSubmit)`:** An event binding on a `<form>` element that triggers a method when the form is submitted.
3232

@@ -36,11 +36,11 @@ This example consists of a standalone component that defines and manages a conta
3636

3737
### contact-form.component.ts
3838

39-
This file defines the component's logic, including a schema that uses `validateWhen` to apply a validator conditionally.
39+
This file defines the component's logic, including a schema that uses `applyWhen` to apply a validator conditionally.
4040

4141
```typescript
4242
import { Component, signal, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
43-
import { form, schema, validateWhen } from '@angular/forms/signals';
43+
import { form, schema, applyWhen, required } from '@angular/forms/signals';
4444
import { JsonPipe } from '@angular/common';
4545

4646
export interface ContactForm {
@@ -49,10 +49,10 @@ export interface ContactForm {
4949
}
5050

5151
const contactSchema = schema<ContactForm>((contactForm) => {
52-
validateWhen(
53-
contactForm.email,
54-
({valueOf}) => valueOf(contactForm.subscribe),
55-
({value}) => (value === '' ? { required: true } : null)
52+
applyWhen(
53+
contactForm,
54+
({value}) => value().subscribe,
55+
schema((form) => required(form.email))
5656
);
5757
});
5858

packages/forms/signals/examples/09-async-validation.md

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
---
22
title: Signal Form with Asynchronous Validation
3-
summary: Implements an asynchronous validator on a signal form field using `validateAsync` to check for a unique username against a mock backend service.
3+
summary: Implements an asynchronous validator on a signal form field using `validateHttp` to check for a unique username against a mock backend endpoint.
44
keywords:
55
- signal forms
66
- form
77
- control
88
- validation
99
- asynchronous validation
1010
- async
11-
- validateAsync
11+
- validateHttp
1212
- schema
1313
required_packages:
1414
- '@angular/forms'
15+
- '@angular/common'
1516
related_concepts:
1617
- 'signals'
1718
---
@@ -22,68 +23,37 @@ The purpose of this pattern is to perform validation that requires an asynchrono
2223

2324
## When to Use
2425

25-
Use this pattern for validation that requires a network request, a debounce, or any other asynchronous operation. It allows the UI to remain responsive while the validation is in progress and provides feedback to the user once the validation is complete. This is the modern, signal-based equivalent of `AsyncValidator` in `ReactiveFormsModule`.
26+
Use this pattern for validation that requires a network request. The `validateHttp` function provides a convenient, built-in way to integrate backend validation. It allows the UI to remain responsive while the validation is in progress and provides feedback to the user once the validation is complete. This is the modern, signal-based equivalent of `AsyncValidator` in `ReactiveFormsModule`.
2627

2728
## Key Concepts
2829

29-
- **`validateAsync()`:** A function used within a schema to add an asynchronous validator to a field.
30-
- **Asynchronous Validator Function:** A function that returns a `Promise` which resolves to an error object or `null`.
30+
- **`validateHttp()`:** A function used within a schema to add an asynchronous validator to a field based on an HTTP request.
3131
- **`pending` signal:** A signal on the `FieldState` that is `true` while an asynchronous validator is running.
3232

3333
## Example Files
3434

35-
This example consists of a standalone component and a service that work together to perform asynchronous validation.
36-
37-
### username.service.ts
38-
39-
This file defines a mock service that simulates an asynchronous check for username uniqueness.
40-
41-
```typescript
42-
import { Injectable } from '@angular/core';
43-
import { of } from 'rxjs';
44-
import { delay } from 'rxjs/operators';
45-
46-
@Injectable({ providedIn: 'root' })
47-
export class UsernameService {
48-
private existingUsernames = ['admin', 'user', 'test'];
49-
50-
isUsernameTaken(username: string): Promise<boolean> {
51-
return of(this.existingUsernames.includes(username.toLowerCase()))
52-
.pipe(delay(500))
53-
.toPromise();
54-
}
55-
}
56-
```
35+
This example consists of a standalone component that performs asynchronous validation against a mock API endpoint.
5736

5837
### registration-form.component.ts
5938

60-
This file defines the component's logic, including a schema that uses `validateAsync` to call the username service.
39+
This file defines the component's logic, including a schema that uses `validateHttp` to call the username validation endpoint.
6140

6241
```typescript
63-
import { Component, inject, signal, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
64-
import { form, schema, validate, validateAsync } from '@angular/forms/signals';
42+
import { Component, signal, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
43+
import { form, schema, required, validateHttp } from '@angular/forms/signals';
6544
import { JsonPipe } from '@angular/common';
66-
import { UsernameService } from './username.service';
6745

6846
export interface RegistrationForm {
6947
username: string;
7048
}
7149

72-
const createRegistrationSchema = (usernameService: UsernameService) => {
73-
return schema<RegistrationForm>((registrationForm) => {
74-
validate(registrationForm.username, ({value}) => {
75-
if (value === '') {
76-
return { required: true };
77-
}
78-
return null;
79-
});
80-
validateAsync(registrationForm.username, async ({value}) => {
81-
if (value === '') return null;
82-
const isTaken = await usernameService.isUsernameTaken(value);
83-
return isTaken ? { unique: true } : null;
84-
});
50+
const registrationSchema = schema<RegistrationForm>((form) => {
51+
required(form.username);
52+
validateHttp(form.username, {
53+
request: ({value}) => `/api/username-check?username=${value}`,
54+
errors: (result: {isTaken: boolean}) => (result.isTaken ? {unique: true} : null),
8555
});
86-
};
56+
});
8757

8858
@Component({
8959
selector: 'app-registration-form',
@@ -95,13 +65,11 @@ const createRegistrationSchema = (usernameService: UsernameService) => {
9565
export class RegistrationFormComponent {
9666
@Output() submitted = new EventEmitter<RegistrationForm>();
9767

98-
private usernameService = inject(UsernameService);
99-
10068
registrationModel = signal<RegistrationForm>({
10169
username: '',
10270
});
10371

104-
registrationForm = form(this.registrationModel, createRegistrationSchema(this.usernameService));
72+
registrationForm = form(this.registrationModel, registrationSchema);
10573

10674
handleSubmit() {
10775
if (this.registrationForm().valid()) {
@@ -144,15 +112,47 @@ This file provides the template for the form, showing how to display feedback wh
144112

145113
## Usage Notes
146114

147-
- The **`validateAsync`** function takes a `FieldPath` and an async validator function.
148-
- The async validator function should return a `Promise` that resolves with the validation result.
115+
- The **`validateHttp`** function takes a `FieldPath` and an options object.
116+
- The `request` function returns the URL for the validation request.
117+
- The `errors` function maps the HTTP response to a validation error.
149118
- The **`pending`** signal on the field state (`registrationForm.username().pending()`) can be used to show a loading indicator to the user.
150119

151120
## How to Use This Example
152121

153-
The parent component listens for the `(submitted)` event to receive the strongly-typed form data.
122+
To run this example, you need to provide an `HttpClient` and a mock backend that can respond to the validation requests.
154123

155124
```typescript
125+
// in app.config.ts
126+
import { ApplicationConfig } from '@angular/core';
127+
import { provideHttpClient, withInterceptors } from '@angular/common/http';
128+
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
129+
130+
// A simple interceptor to mock the backend
131+
const mockApiInterceptor = (req, next) => {
132+
const controller = TestBed.inject(HttpTestingController);
133+
const existingUsernames = ['admin', 'user', 'test'];
134+
135+
if (req.url.startsWith('/api/username-check')) {
136+
const username = req.params.get('username');
137+
const isTaken = existingUsernames.includes(username?.toLowerCase() ?? '');
138+
139+
// Use a timeout to simulate network latency
140+
setTimeout(() => {
141+
const mockReq = controller.expectOne(req.urlWithParams);
142+
mockReq.flush({ isTaken });
143+
}, 500);
144+
}
145+
146+
return next(req);
147+
};
148+
149+
export const appConfig: ApplicationConfig = {
150+
providers: [
151+
provideHttpClient(withInterceptors([mockApiInterceptor])),
152+
provideHttpClientTesting(),
153+
],
154+
};
155+
156156
// in app.component.ts
157157
import { Component } from '@angular/core';
158158
import { JsonPipe } from '@angular/common';

0 commit comments

Comments
 (0)