Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions adev/src/content/guide/forms/signals/field-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ Calling `required(path.username)` contributes a value to the `REQUIRED` metadata

Several built-in constraint validators follow this pattern:

| Validator | Metadata key | Type | `FieldState` getter |
| ------------- | ------------ | --------------------- | ------------------- |
| `required()` | `REQUIRED` | `boolean` | `required` |
| `min()` | `MIN` | `number \| undefined` | `min` |
| `max()` | `MAX` | `number \| undefined` | `max` |
| `minLength()` | `MIN_LENGTH` | `number \| undefined` | `minLength` |
| `maxLength()` | `MAX_LENGTH` | `number \| undefined` | `maxLength` |
| `pattern()` | `PATTERN` | `RegExp[]` | `pattern` |
| Validator | Metadata key | Type | `FieldState` getter |
| ------------- | -------------------------- | --------------------- | ------------------- |
| `required()` | `REQUIRED` | `boolean` | `required` |
| `min()` | `MIN` selects `MIN_NUMBER` | `number \| undefined` | `min` |
| `max()` | `MAX` selects `MAX_NUMBER` | `number \| undefined` | `max` |
| `minDate()` | `MIN` selects `MIN_DATE` | `Date \| undefined` | `min` |
| `maxDate()` | `MAX` selects `MAX_DATE` | `Date \| undefined` | `max` |
| `minLength()` | `MIN_LENGTH` | `number \| undefined` | `minLength` |
| `maxLength()` | `MAX_LENGTH` | `number \| undefined` | `maxLength` |
| `pattern()` | `PATTERN` | `RegExp[]` | `pattern` |

Non-constraint validators like `email()` and `validate()` do not contribute to metadata. They run their check and surface a validation error, but they do not publish a reactive value for templates to read.

Expand Down Expand Up @@ -217,16 +219,20 @@ While `MetadataReducer.min()` and `MetadataReducer.max()` are reducers, you may

The built-in constraint keys pick their reducers based on what "strictest" means for the constraint, which is often the opposite of what the key's name suggests:

| Key | Reducer | Reasoning |
| ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `REQUIRED` | `or()` | If any `required()` rule evaluates to `true`, the field is required. |
| `MIN` | `max()` | A minimum-value constraint is strictest when largest. If one rule requires `>= 5` and another `>= 10`, the effective minimum is `10`. |
| `MAX` | `min()` | A maximum-value constraint is strictest when smallest. If one rule caps at `100` and another at `50`, the effective maximum is `50`. |
| `MIN_LENGTH` | `max()` | Same logic as `MIN`: the longest required length wins. |
| `MAX_LENGTH` | `min()` | Same logic as `MAX`: the shortest allowed length wins. |
| `PATTERN` | `list<RegExp>()` | Each `pattern()` call contributes a regex; the value must match all of them. |
| Key | Reducer | Reasoning |
| ------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `REQUIRED` | `or()` | If any `required()` rule evaluates to `true`, the field is required. |
| `MIN_NUMBER` | `max()` | A minimum-number constraint is strictest when largest. If one rule requires `>= 5` and another `>= 10`, the effective minimum is `10`. |
| `MIN_DATE` | `max()` | Same logic as `MIN_NUMBER`: the latest required date wins. |
| `MAX_NUMBER` | `min()` | A maximum-number constraint is strictest when smallest. If one rule caps at `100` and another at `50`, the effective maximum is `50`. |
| `MAX_DATE` | `min()` | Same logic as `MAX_NUMBER`: the earliest allowed date wins. |
| `MIN_LENGTH` | `max()` | Same logic as `MIN_NUMBER`: the longest required length wins. |
| `MAX_LENGTH` | `min()` | Same logic as `MAX_NUMBER`: the shortest allowed length wins. |
| `PATTERN` | `list<RegExp>()` | Each `pattern()` call contributes a regex; the value must match all of them. |

This pairing of "strictest wins" is why calling `min(path.age, 18)` and `min(path.age, 21)` in two composed schemas works correctly. Each call registers its own validator that enforces its specific bound (so a value below either bound fails validation). Separately, each call contributes to the public `MIN` key, and `state.metadata(MIN)!()` reports the aggregate (`21`) so UI and custom controls can read the effective minimum.
`MIN` and `MAX` are selection keys. They point to the concrete key that matches the field's value type, such as `MIN_NUMBER` for `min()` and `MIN_DATE` for `minDate()`. This is why `field().min()` and `field().max()` work for both numeric and date fields.

This pairing of "strictest wins" is why calling `min(path.age, 18)` and `min(path.age, 21)` in two composed schemas works correctly. Each call registers its own validator that enforces its specific bound (so a value below either bound fails validation). Separately, each call contributes to the `MIN_NUMBER` key, and `state.min!()` reports the aggregate (`21`) so UI and custom controls can read the effective minimum.

### Writing a custom reducer

Expand Down
20 changes: 10 additions & 10 deletions adev/src/content/guide/forms/signals/form-logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,16 +458,16 @@ Don't use debouncing if:

Metadata attaches reactive data to a field. Validation rules use this system internally, and you can publish your own keys for application-specific information like help text, configuration, or computed display values.

Signal Forms provides six pre-defined metadata keys that built-in validators populate automatically:

| Key | Populated by | Read via |
| ------------ | ------------- | --------------------- |
| `REQUIRED` | `required()` | `field().required()` |
| `MIN` | `min()` | `field().min()` |
| `MAX` | `max()` | `field().max()` |
| `MIN_LENGTH` | `minLength()` | `field().minLength()` |
| `MAX_LENGTH` | `maxLength()` | `field().maxLength()` |
| `PATTERN` | `pattern()` | `field().pattern()` |
Signal Forms provides pre-defined metadata keys that built-in validators populate automatically:

| Key | Populated by | Read via |
| ------------ | -------------------- | --------------------- |
| `REQUIRED` | `required()` | `field().required()` |
| `MIN` | `min()`, `minDate()` | `field().min()` |
| `MAX` | `max()`, `maxDate()` | `field().max()` |
| `MIN_LENGTH` | `minLength()` | `field().minLength()` |
| `MAX_LENGTH` | `maxLength()` | `field().maxLength()` |
| `PATTERN` | `pattern()` | `field().pattern()` |

The `[formField]` directive automatically binds five of these (`REQUIRED`, `MIN`, `MAX`, `MIN_LENGTH`, and `MAX_LENGTH`) to the corresponding HTML attribute on a native form control. `PATTERN` is the exception, because Signal Forms supports multiple patterns per field but the HTML `pattern` attribute accepts only a single regular expression.

Expand Down
Loading