diff --git a/adev/src/content/guide/forms/signals/form-logic.md b/adev/src/content/guide/forms/signals/form-logic.md
index 7b965f45f9c..f9e0c807c95 100644
--- a/adev/src/content/guide/forms/signals/form-logic.md
+++ b/adev/src/content/guide/forms/signals/form-logic.md
@@ -34,7 +34,7 @@ For complete details on `FieldContext` properties and methods, see the [Validati
The `disabled()` rule configures a field's disabled state.
-It works with the `[field]` directive to automatically bind the `disabled` attribute based on the field's state, so you don't need to manually add `[disabled]="yourForm.fieldName().disabled()"` to your template.
+It works with the `[formField]` directive to automatically bind the `disabled` attribute based on the field's state, so you don't need to manually add `[disabled]="yourForm.fieldName().disabled()"` to your template.
NOTE: Disabled fields skip validation - they don't participate in form validation checks. The field's value is preserved but not validated. For details on validation behavior, see the [Validation guide](guide/forms/signals/validation).
@@ -43,28 +43,28 @@ NOTE: Disabled fields skip validation - they don't participate in form validatio
To disable a field permanently, call `disabled()` with just the field path:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, disabled } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-settings',
- imports: [Field],
+ imports: [FormField],
template: `
- `
+ `,
})
export class Settings {
settingsModel = signal({
systemId: 'SYS-12345',
- userName: ''
- })
+ userName: '',
+ });
settingsForm = form(this.settingsModel, (schemaPath) => {
- disabled(schemaPath.systemId)
- })
+ disabled(schemaPath.systemId);
+ });
}
```
@@ -73,33 +73,33 @@ export class Settings {
To disable a field based on conditions, provide a reactive logic function that returns `true` (disabled) or `false` (enabled):
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, disabled } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-order',
- imports: [Field],
+ imports: [FormField],
template: `
- `
+ `,
})
export class Order {
orderModel = signal({
total: 25,
- couponCode: ''
- })
+ couponCode: '',
+ });
orderForm = form(this.orderModel, (schemaPath) => {
- disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50)
- })
+ disabled(schemaPath.couponCode, ({valueOf}) => valueOf(schemaPath.total) < 50);
+ });
}
```
@@ -110,21 +110,21 @@ In this example, when the order total is less than $50, the coupon code field is
When you disable a field, provide user-facing explanations by returning a string instead of `true`:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, disabled } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, disabled} from '@angular/forms/signals';
@Component({
selector: 'app-order',
- imports: [Field],
+ imports: [FormField],
template: `
@if (orderForm.couponCode().disabled()) {
@@ -134,19 +134,19 @@ import { form, Field, disabled } from '@angular/forms/signals'
}
}
- `
+ `,
})
export class Order {
orderModel = signal({
total: 25,
- couponCode: ''
- })
+ couponCode: '',
+ });
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.couponCode, ({valueOf}) =>
- valueOf(schemaPath.total) < 50 ? 'Order must be $50 or more to use a coupon' : false
- )
- })
+ valueOf(schemaPath.total) < 50 ? 'Order must be $50 or more to use a coupon' : false,
+ );
+ });
}
```
@@ -164,12 +164,12 @@ You can also call `disabled()` multiple times on the same field, and all of the
```angular-ts
orderForm = form(this.orderModel, (schemaPath) => {
disabled(schemaPath.promoCode, ({valueOf}) =>
- !valueOf(schemaPath.hasAccount) ? 'You must have an account to use promo codes' : false
- )
+ !valueOf(schemaPath.hasAccount) ? 'You must have an account to use promo codes' : false,
+ );
disabled(schemaPath.promoCode, ({valueOf}) =>
- valueOf(schemaPath.total) < 25 ? 'Order must be at least $25' : false
- )
-})
+ valueOf(schemaPath.total) < 25 ? 'Order must be at least $25' : false,
+ );
+});
```
If both conditions are true, the field shows both disabled reasons. This pattern is useful for complex availability rules that you want to keep separate.
@@ -178,7 +178,7 @@ If both conditions are true, the field shows both disabled reasons. This pattern
The `hidden()` rule configures a field's hidden state. However, this only sets a programmatic state. **You control whether the field appears in the UI**.
-IMPORTANT: Unlike `disabled` and `readonly`, there is no native DOM property for `hidden` state. The `[field]` directive does not apply a `hidden` attribute to elements. You must use `@if` or CSS in your template to conditionally render fields based on the `hidden()` state.
+IMPORTANT: Unlike `disabled` and `readonly`, there is no native DOM property for `hidden` state. The `[formField]` directive does not apply a `hidden` attribute to elements. You must use `@if` or CSS in your template to conditionally render fields based on the `hidden()` state.
NOTE: Like disabled fields, hidden fields also skip validation. See the [Validation guide](guide/forms/signals/validation) for details.
@@ -187,41 +187,41 @@ NOTE: Like disabled fields, hidden fields also skip validation. See the [Validat
Use `hidden()` with a reactive logic function that returns `true` (hidden) or `false` (visible):
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, hidden } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, hidden} from '@angular/forms/signals';
@Component({
selector: 'app-profile',
- imports: [Field],
+ imports: [FormField],
template: `
@if (!profileForm.publicUrl().hidden()) {
}
- `
+ `,
})
export class Profile {
profileModel = signal({
isPublic: false,
- publicUrl: ''
- })
+ publicUrl: '',
+ });
profileForm = form(this.profileModel, (schemaPath) => {
- hidden(schemaPath.publicUrl, ({valueOf}) => !valueOf(schemaPath.isPublic))
- })
+ hidden(schemaPath.publicUrl, ({valueOf}) => !valueOf(schemaPath.isPublic));
+ });
}
```
## Display uneditable fields with `readonly()`
-The `readonly()` rule prevents users from updating a field. The `[field]` directive automatically binds this state to the HTML `readonly` attribute, which prevents editing while still allowing users to focus and select text.
+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.
NOTE: Readonly fields skip [validation](guide/forms/signals/validation).
@@ -230,70 +230,70 @@ NOTE: Readonly fields skip [validation](guide/forms/signals/validation).
To make a field permanently readonly, call `readonly()` with just the field path:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, readonly } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, readonly} from '@angular/forms/signals';
@Component({
selector: 'app-account',
- imports: [Field],
+ imports: [FormField],
template: `
- `
+ `,
})
export class Account {
accountModel = signal({
username: 'johndoe',
- email: 'john@example.com'
- })
+ email: 'john@example.com',
+ });
accountForm = form(this.accountModel, (schemaPath) => {
- readonly(schemaPath.username)
- })
+ readonly(schemaPath.username);
+ });
}
```
-The `[field]` directive automatically binds the `readonly` attribute based on the field's state.
+The `[FormField]` directive automatically binds the `readonly` attribute based on the field's state.
### Conditional readonly
To make a field readonly based on conditions, provide a reactive logic function:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, readonly } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, readonly} from '@angular/forms/signals';
@Component({
selector: 'app-document',
- imports: [Field],
+ imports: [FormField],
template: `
- `
+ `,
})
export class Document {
documentModel = signal({
isLocked: false,
- title: 'Untitled'
- })
+ title: 'Untitled',
+ });
documentForm = form(this.documentModel, (schemaPath) => {
- readonly(schemaPath.title, ({valueOf}) => valueOf(schemaPath.isLocked))
- })
+ readonly(schemaPath.title, ({valueOf}) => valueOf(schemaPath.isLocked));
+ });
}
```
@@ -350,29 +350,29 @@ Debouncing delays these updates and reduces unnecessary work.
You can debounce a field by specifying a delay in milliseconds:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, debounce } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, debounce} from '@angular/forms/signals';
@Component({
selector: 'app-search',
- imports: [Field],
+ imports: [FormField],
template: `
Searching for: {{ searchForm.query().value() }}
- `
+ `,
})
export class Search {
searchModel = signal({
- query: ''
- })
+ query: '',
+ });
searchForm = form(this.searchModel, (schemaPath) => {
- debounce(schemaPath.query, 300)
- })
+ debounce(schemaPath.query, 300);
+ });
}
```
@@ -397,32 +397,32 @@ This means users can type quickly, tab away, or submit the form without waiting
For advanced control, provide a debouncer function that controls when to synchronize the value. This function is called every time the control value is updated and can return either `undefined` to synchronize immediately, or a Promise that prevents synchronization until it resolves:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, debounce } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, debounce} from '@angular/forms/signals';
@Component({
selector: 'app-search',
- imports: [Field],
+ imports: [FormField],
template: `
- `
+ `,
})
export class Search {
searchModel = signal({
- query: ''
- })
+ query: '',
+ });
searchForm = form(this.searchModel, (schemaPath) => {
debounce(schemaPath.query, () => {
// Return a promise that resolves after 500ms
return new Promise((resolve) => {
- setTimeout(() => resolve(), 500)
- })
- })
- })
+ setTimeout(() => resolve(), 500);
+ });
+ });
+ });
}
```
@@ -471,77 +471,72 @@ When you use validation rules like `required()` or `min()`, they automatically s
### Reading pre-defined metadata
-The `[field]` directive automatically binds built-in metadata to HTML attributes. You can also read metadata directly using the built-in accessors on field state:
+The `[FormField]` directive automatically binds built-in metadata to HTML attributes. You can also read metadata directly using the built-in accessors on field state:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, required, min, max } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, required, min, max} from '@angular/forms/signals';
@Component({
selector: 'app-age',
- imports: [Field],
+ imports: [FormField],
template: `
@if (ageForm.age().required()) {
*
}
- `
+ `,
})
export class Age {
ageModel = signal({
- age: 0
- })
+ age: 0,
+ });
ageForm = form(this.ageModel, (schemaPath) => {
- required(schemaPath.age)
- min(schemaPath.age, 18)
- max(schemaPath.age, 120)
- })
+ required(schemaPath.age);
+ min(schemaPath.age, 18);
+ max(schemaPath.age, 120);
+ });
}
```
-The `[field]` directive automatically binds `required`, `min`, and `max` attributes to the input. You can read these values using `field().required()`, `field().min()`, and `field().max()` for display or logic purposes.
+The `[formField]` directive automatically binds `required`, `min`, and `max` attributes to the input. You can read these values using `field().required()`, `field().min()`, and `field().max()` for display or logic purposes.
### Setting metadata manually
Use the `metadata()` function to set metadata values when validation rules don't automatically set them. For built-in metadata like `MIN` and `MAX`, prefer using the validation rules:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, min, max, validate } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, min, max, validate} from '@angular/forms/signals';
@Component({
selector: 'app-custom',
- imports: [Field],
- template: `
-
- `
+ imports: [formField],
+ template: ` `,
})
export class Custom {
- customModel = signal({ score: 0 })
+ customModel = signal({score: 0});
customForm = form(this.customModel, (schemaPath) => {
// Use built-in validation rules - they automatically set metadata
- min(schemaPath.score, 0)
- max(schemaPath.score, 100)
+ min(schemaPath.score, 0);
+ max(schemaPath.score, 100);
// Add custom validation logic if needed
validate(schemaPath.score, ({value}) => {
- const score = value()
+ const score = value();
// Custom validation beyond min/max (e.g., must be multiple of 5)
if (score % 5 !== 0) {
- return {kind: 'increment', message: 'Score must be a multiple of 5'}
+ return {kind: 'increment', message: 'Score must be a multiple of 5'};
}
- return null
- })
- })
+ return null;
+ });
+ });
}
```
@@ -550,21 +545,21 @@ export class Custom {
Create your own metadata keys for application-specific information:
```angular-ts
-import { createMetadataKey, metadata } from '@angular/forms/signals'
+import {createMetadataKey, metadata} from '@angular/forms/signals';
// Define at module level (not inside components)
-export const PLACEHOLDER = createMetadataKey()
-export const HELP_TEXT = createMetadataKey()
+export const PLACEHOLDER = createMetadataKey();
+export const HELP_TEXT = createMetadataKey();
// Use in schema
form(model, (schemaPath) => {
- metadata(schemaPath.email, PLACEHOLDER, () => 'user@example.com')
- metadata(schemaPath.email, HELP_TEXT, () => 'We will never share your email')
-})
+ metadata(schemaPath.email, PLACEHOLDER, () => 'user@example.com');
+ metadata(schemaPath.email, HELP_TEXT, () => 'We will never share your email');
+});
// Read in component
-const placeholderText = myForm.email().metadata(PLACEHOLDER)
-const helpText = myForm.email().metadata(HELP_TEXT)
+const placeholderText = myForm.email().metadata(PLACEHOLDER);
+const helpText = myForm.email().metadata(HELP_TEXT);
```
By default, custom metadata keys use a "last write wins" strategy - if you call `metadata()` multiple times with the same key, only the last value is kept.
@@ -576,20 +571,20 @@ By default, custom metadata keys use a "last write wins" strategy - if you call
By default, calling `metadata()` multiple times with the same key uses "last write wins" - only the final value is kept. To accumulate values instead, pass a reducer to `createMetadataKey()`:
```angular-ts
-import { createMetadataKey, metadata, MetadataReducer } from '@angular/forms/signals'
+import {createMetadataKey, metadata, MetadataReducer} from '@angular/forms/signals';
// Create a key that accumulates values into an array
-export const HINTS = createMetadataKey(MetadataReducer.list())
+export const HINTS = createMetadataKey(MetadataReducer.list());
// Multiple calls accumulate values
form(model, (schemaPath) => {
- metadata(schemaPath.password, HINTS, () => 'At least 8 characters')
- metadata(schemaPath.password, HINTS, () => 'Include a number')
- metadata(schemaPath.password, HINTS, () => 'Include a special character')
-})
+ metadata(schemaPath.password, HINTS, () => 'At least 8 characters');
+ metadata(schemaPath.password, HINTS, () => 'Include a number');
+ metadata(schemaPath.password, HINTS, () => 'Include a special character');
+});
// Result: Signal containing the accumulated array
-const passwordHints = passwordForm.password().metadata(HINTS)()
+const passwordHints = passwordForm.password().metadata(HINTS)();
// ['At least 8 characters', 'Include a number', 'Include a special character']
```
@@ -606,29 +601,30 @@ Angular provides built-in reducers through `MetadataReducer`:
Use `createManagedMetadataKey()` when you need to compute a new value from the accumulated result. The transform function receives a signal of the reduced value and returns the computed result:
```angular-ts
-import { createManagedMetadataKey, metadata, MetadataReducer } from '@angular/forms/signals'
+import {createManagedMetadataKey, metadata, MetadataReducer} from '@angular/forms/signals';
// Accumulate hints and compute additional data from the result
export const HINTS = createManagedMetadataKey(
- (signal) => computed(() => {
- const hints = signal()
- return {
- messages: hints,
- count: hints?.length ?? 0
- }
- }),
- MetadataReducer.list()
-)
+ (signal) =>
+ computed(() => {
+ const hints = signal();
+ return {
+ messages: hints,
+ count: hints?.length ?? 0,
+ };
+ }),
+ MetadataReducer.list(),
+);
// Multiple calls accumulate values
form(model, (schemaPath) => {
- metadata(schemaPath.password, HINTS, () => 'At least 8 characters')
- metadata(schemaPath.password, HINTS, () => 'Include a number')
- metadata(schemaPath.password, HINTS, () => 'Include a special character')
-})
+ metadata(schemaPath.password, HINTS, () => 'At least 8 characters');
+ metadata(schemaPath.password, HINTS, () => 'Include a number');
+ metadata(schemaPath.password, HINTS, () => 'Include a special character');
+});
// Result: Signal with transformed value
-const passwordHints = passwordForm.password().metadata(HINTS)()
+const passwordHints = passwordForm.password().metadata(HINTS)();
// { messages: ['At least 8 characters', 'Include a number', 'Include a special character'], count: 3 }
```
@@ -642,16 +638,16 @@ The managed metadata key takes two arguments:
Make metadata reactive to other field values:
```angular-ts
-import { Component, signal } from '@angular/core'
-import { form, Field, max } from '@angular/forms/signals'
+import {Component, signal} from '@angular/core';
+import {form, FormField, max} from '@angular/forms/signals';
@Component({
selector: 'app-inventory',
- imports: [Field],
+ imports: [formField],
template: `