Skip to content

Commit ca4e34e

Browse files
committed
docs(forms): fix broken snippets and wrong statements in the signal forms guides
- async-operations: return headers and timeout from the `validateHttp` `request` function (they are `HttpResourceRequest` properties, not `httpResource` options), and fix the invalid object literal in the `parse` example. - validation: remove a stray `|` from a component snippet, fix the "multiple errors" example (`a@b` is a valid email address for the `email()` validator, `a@` isn't), rename `field` to `fieldTree` in the `FieldContext` table, add the missing `signal` import, use one zod import style, and clarify that dynamic schemas are passed as a function. - custom-controls: `DisabledReason` exposes `fieldTree`, not `field`. - migration: use `extractValue()` to get the whole form value instead of building it manually, and fix the expected output. - form-logic: refer to the `FormField` directive by name, release the timer when the `debounce` example is aborted, and add the missing `form` import. - comparison example: submit through `FormRoot` and a `submission` action, so the page no longer reloads on submit.
1 parent 76492d0 commit ca4e34e

6 files changed

Lines changed: 54 additions & 42 deletions

File tree

‎adev/src/content/examples/signal-forms/src/comparison/app/signal-forms.ts‎

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import {Component, signal} from '@angular/core';
2-
import {email, form, FormField, minLength, required} from '@angular/forms/signals';
2+
import {email, form, FormField, FormRoot, minLength, required} from '@angular/forms/signals';
33

44
@Component({
55
selector: 'app-login',
6-
imports: [FormField],
6+
imports: [FormField, FormRoot],
77
template: `
8-
<form (submit)="onSubmit()">
8+
<form [formRoot]="loginForm">
99
<div>
1010
<label>
1111
Email
@@ -30,7 +30,7 @@ import {email, form, FormField, minLength, required} from '@angular/forms/signal
3030
}
3131
</div>
3232
33-
<button type="submit" [disabled]="loginForm().invalid()">Sign In</button>
33+
<button type="submit">Sign In</button>
3434
</form>
3535
`,
3636
})
@@ -40,18 +40,22 @@ export class LoginComponent {
4040
password: '',
4141
});
4242

43-
loginForm = form(this.loginModel, (fieldPath) => {
44-
required(fieldPath.email, {message: 'Email is required'});
45-
email(fieldPath.email, {message: 'Enter a valid email address'});
43+
loginForm = form(
44+
this.loginModel,
45+
(fieldPath) => {
46+
required(fieldPath.email, {message: 'Email is required'});
47+
email(fieldPath.email, {message: 'Enter a valid email address'});
4648

47-
required(fieldPath.password, {message: 'Password is required'});
48-
minLength(fieldPath.password, 8, {message: 'Password must be at least 8 characters'});
49-
});
50-
51-
onSubmit() {
52-
if (this.loginForm().valid()) {
53-
const credentials = this.loginModel();
54-
console.log('Submitting:', credentials);
55-
}
56-
}
49+
required(fieldPath.password, {message: 'Password is required'});
50+
minLength(fieldPath.password, 8, {message: 'Password must be at least 8 characters'});
51+
},
52+
{
53+
submission: {
54+
// Only runs when the form is valid.
55+
action: async (field) => {
56+
console.log('Submitting:', field().value());
57+
},
58+
},
59+
},
60+
);
5761
}

‎adev/src/content/guide/forms/signals/async-operations.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ onSuccess: (response: { usernameTaken: boolean; profanity: boolean }) => {
180180
} // prettier-ignore
181181
```
182182

183-
The type for `onSuccess` can be specified either directly in the parameter, or with the `parse` property of `validateHttp`'s `options`
183+
The type for `onSuccess` can be specified either directly in the parameter, or with the `parse` property of `validateHttp`'s `options`.
184184

185185
```ts
186186
onSuccess: (response: { usernameTaken: boolean; profanity: boolean }) => {
@@ -190,8 +190,8 @@ onSuccess: (response: { usernameTaken: boolean; profanity: boolean }) => {
190190
// or
191191

192192
options: {
193-
parse: (response) => response as {usernameTaken: boolean; profanity: boolean};
194-
}
193+
parse: (response) => response as {usernameTaken: boolean; profanity: boolean},
194+
},
195195
onSuccess: (response) => {
196196
// ...
197197
} // prettier-ignore
@@ -211,19 +211,19 @@ onError: (error) => {
211211

212212
### HTTP options
213213

214-
Customize the HTTP request with the `options` parameter:
214+
Customize the HTTP request by returning an `HttpResourceRequest` object from `request` instead of a URL:
215215

216216
```ts
217217
import {HttpHeaders} from '@angular/common/http';
218218

219219
validateHttp(schemaPath.field, {
220-
request: ({value}) => `/api/validate?value=${value()}`,
221-
options: {
220+
request: ({value}) => ({
221+
url: `/api/validate?value=${value()}`,
222222
headers: new HttpHeaders({
223223
Authorization: 'Bearer token',
224224
}),
225225
timeout: 5000,
226-
},
226+
}),
227227
onSuccess: (response: {valid: boolean}) =>
228228
response.valid
229229
? null
@@ -238,7 +238,7 @@ validateHttp(schemaPath.field, {
238238
});
239239
```
240240

241-
TIP: See the [httpResource API documentation](api/common/http/httpResource) for all available options.
241+
TIP: See the [`HttpResourceRequest` API documentation](api/common/http/HttpResourceRequest) for all available request properties, and the [httpResource API documentation](api/common/http/httpResource) for the `options` parameter.
242242

243243
## Custom async validation with validateAsync()
244244

‎adev/src/content/guide/forms/signals/custom-controls.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Control whether users can interact with your field:
175175
| `readonly` | Whether the field is readonly (visible but not editable) |
176176
| `hidden` | Whether the field is hidden from view |
177177

178-
NOTE: `disabledReasons` is an array of `DisabledReason` objects. Each object has a `field` property (reference to the field tree) and an optional `message` property. Access the message via `reason.message`.
178+
NOTE: `disabledReasons` is an array of `DisabledReason` objects. Each object has a `fieldTree` property (reference to the field tree) and an optional `message` property. Access the message via `reason.message`.
179179

180180
#### Validation constraints
181181

‎adev/src/content/guide/forms/signals/form-logic.md‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export class Profile {
262262

263263
## Display uneditable fields with `readonly()`
264264

265-
The `readonly()` rule prevents users from updating a field. The `[FormField]` directive automatically binds this state to the HTML `readonly` attribute, which prevents editing while still allowing users to focus and select text.
265+
The `readonly()` rule prevents users from updating a field. The `FormField` directive automatically binds this state to the HTML `readonly` attribute, which prevents editing while still allowing users to focus and select text.
266266

267267
NOTE: Readonly fields skip [validation](guide/forms/signals/validation).
268268

@@ -301,7 +301,7 @@ export class Account {
301301
}
302302
```
303303

304-
The `[FormField]` directive automatically binds the `readonly` attribute based on the field's state.
304+
The `FormField` directive automatically binds the `readonly` attribute based on the field's state.
305305

306306
### Conditional readonly
307307

@@ -457,10 +457,18 @@ export class Search {
457457
});
458458
459459
searchForm = form(this.searchModel, (schemaPath) => {
460-
debounce(schemaPath.query, () => {
461-
// Return a promise that resolves after 500ms
460+
debounce(schemaPath.query, (context, abortSignal) => {
461+
// Return a promise that resolves after 500ms, or right away when the debounce is aborted
462462
return new Promise<void>((resolve) => {
463-
setTimeout(() => resolve(), 500);
463+
const timeoutId = setTimeout(resolve, 500);
464+
abortSignal.addEventListener(
465+
'abort',
466+
() => {
467+
clearTimeout(timeoutId);
468+
resolve();
469+
},
470+
{once: true},
471+
);
464472
});
465473
});
466474
});
@@ -695,7 +703,7 @@ The conditional rules only run when the condition is true. This is useful for co
695703
Extract common rule configurations into reusable functions:
696704

697705
```ts
698-
import {SchemaPath, debounce, metadata, maxLength} from '@angular/forms/signals';
706+
import {SchemaPath, debounce, form, metadata, maxLength} from '@angular/forms/signals';
699707
import {PLACEHOLDER} from './metadata-keys';
700708

701709
function emailFieldConfig(path: SchemaPath<string>) {

‎adev/src/content/guide/forms/signals/migration.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,12 @@ form.password().value(); // 'password'
196196
form().value(); // { email: '', password: FormControl}
197197
```
198198

199-
If you need the whole form value, you'd have to build it manually:
199+
If you need the whole form value with the controls unwrapped, use `extractValue` from `@angular/forms/signals/compat`:
200200

201201
```typescript
202-
const formValue = computed(() => ({
203-
email: form.email().value(),
204-
password: form.password().value(),
205-
})); // {email: '', password: ''}
202+
import {extractValue} from '@angular/forms/signals/compat';
203+
204+
const formValue = computed(() => extractValue(form)); // {email: '', password: 'password'}
206205
```
207206

208207
## Bottom-up migration

‎adev/src/content/guide/forms/signals/validation.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ signupForm = form(this.signupModel, (schemaPath) => {
435435
});
436436
```
437437

438-
If the email field is empty, only the `required()` error appears. If the user types "a@b", both `email()` and `minLength()` errors appear. All validation rules run - validation doesn't stop after the first failure.
438+
If the email field is empty, only the `required()` error appears. If the user types "a@", both `email()` and `minLength()` errors appear. All validation rules run - validation doesn't stop after the first failure.
439439

440440
TIP: Use the `touched() && invalid()` pattern in your templates to prevent errors from appearing before users have interacted with a field. For comprehensive guidance on displaying validation errors, see the [Field State Management guide](guide/forms/signals/field-state-management#conditional-error-display).
441441

@@ -492,7 +492,7 @@ The validator function receives a `FieldContext` object with:
492492
| --------------- | ---------- | ------------------------------------------- |
493493
| `value` | Signal | Signal containing the current field value |
494494
| `state` | FieldState | The field state reference |
495-
| `field` | FieldTree | The field tree reference |
495+
| `fieldTree` | FieldTree | The field tree reference |
496496
| `valueOf()` | Method | Get the value of another field by path |
497497
| `stateOf()` | Method | Get the state of another field by path |
498498
| `fieldTreeOf()` | Method | Get the field tree of another field by path |
@@ -668,7 +668,7 @@ import {Component, signal} from '@angular/core';
668668
import {form, FormField, required, validateHttp} from '@angular/forms/signals';
669669
670670
@Component({
671-
selector: 'app-username-form',|
671+
selector: 'app-username-form',
672672
imports: [FormField],
673673
template: `
674674
<form novalidate>
@@ -734,6 +734,7 @@ The `valid()` signal returns `false` while validation is pending, even if there
734734
Signal Forms have built-in support for libraries that conform to [Standard Schema](https://standardschema.dev/) like [Zod](https://zod.dev/) or [Valibot](https://valibot.dev/). The integration is provided via the `validateStandardSchema` function. This allows you to use existing schemas while maintaining Signal Forms' reactive validation benefits.
735735

736736
```ts
737+
import {signal} from '@angular/core';
737738
import {form, validateStandardSchema} from '@angular/forms/signals';
738739
import * as z from 'zod';
739740

@@ -751,12 +752,12 @@ const userForm = form(signal({email: '', password: ''}), (schemaPath) => {
751752

752753
### Dynamic schemas
753754

754-
You can pass a signal instead of a static schema so the validation schema updates automatically when its dependencies change.
755+
Instead of a static schema, you can pass a function that returns the schema. The validation then updates automatically when the signals read by the function change.
755756

756757
```angular-ts
757758
import {Component, computed, signal} from '@angular/core';
758759
import {form, FormField, validateStandardSchema} from '@angular/forms/signals';
759-
import z from 'zod';
760+
import * as z from 'zod';
760761
761762
@Component({/* ... */})
762763
export class DynamicSchema {

0 commit comments

Comments
 (0)