41 Commits

Author SHA1 Message Date
Shuaib Hasan Akib 376b71ea1c docs(forms): document what required() considers empty
The `required()` validator treats `null`, `undefined`, `''`, `false` and `NaN` as empty, but the API
docs never defined "empty" at all and the validation guide listed only `null` and `''`.

(cherry picked from commit 467b37b4b4)
2026-09-11 22:16:22 +00:00
SkyZeroZx cc05deab72 test(forms): Add utility functions and update test files to use them
Moves common helper functions into a shared test utility to reduce duplication.

(cherry picked from commit 318fefad30)
2026-09-04 07:48:16 -07:00
Shuaib Hasan Akib f3027367eb refactor(core): use native Promise.withResolvers() in tests
Replaces the temporary `promiseWithResolvers` polyfill with the
native `Promise.withResolvers()` API in test files and Updates the TypeScript configuration to include the `es2024.promise`.

(cherry picked from commit b5ce15c659)
2026-09-01 09:31:54 -07:00
Alex Rickabaugh c8eb7f0056 fix(forms): use dot-access for readonly rule configuration
The readonly rule previously used 'when' in configOrLogic to determine if a configuration object was provided. Under property renaming/minification, the string literal property lookup fails and causes the rule to fall back to being permanently readonly.

This change switches to the dot-access form (configOrLogic?.when), matching hidden() and ensuring property renaming works correctly.

(cherry picked from commit 1e35de536d)
2026-08-24 10:24:35 -07:00
Matthieu Riegler 5cb8c733a3 fix(forms): allow multiple async validators
When a parent form element defines an async validator, its resource's `params` function needs to evaluate `syncValid()`, which causes unvisited child form nodes to be lazily instantiated. If any of these lazily instantiated child nodes also define an async validator, their resource is initialized while the parent's `params` function is still evaluating. This incorrectly triggers Angular core's `NG0992` guard (`Cannot create a resource inside the params of another resource`).
This commit exports `ɵsetInParamsFunction` and `ɵisInParamsFunction` from `@angular/core` and uses them in `FieldMetadataState.runMetadataCreateLifecycle` to explicitly detach the lazy creation of form metadata from the parent's reactive `params` context.

fixes #69620
2026-07-09 09:55:29 -07:00
Matthieu Riegler 25b7afa515 Revert "fix(forms): allow multiple async validators"
This reverts commit 953cdfd66a.
2026-07-08 13:03:48 -07:00
Matthieu Riegler 953cdfd66a fix(forms): allow multiple async validators
When a parent form element defines an async validator, its resource's `params`
function needs to evaluate `syncValid()`, which causes unvisited child form
nodes to be lazily instantiated. If any of these lazily instantiated child
nodes also define an async validator, their resource is initialized while the
parent's `params` function is still evaluating. This incorrectly triggers
Angular core's `NG0992` guard (`Cannot create a resource inside the params
of another resource`).
This commit exports `ɵsetInParamsFunction` and `ɵisInParamsFunction` from
`@angular/core` and uses them in `FieldMetadataState.runMetadataCreateLifecycle`
to explicitly detach the lazy creation of form metadata from the parent's reactive
`params` context.
2026-07-06 14:05:15 -07:00
kirjs 0806b2f02b refactor(forms): use overloads and JSDoc for deprecations
This commit removes runtime console warnings and uses TypeScript overloads with JSDoc @deprecated annotations to handle backward compatibility for conditional rules.
2026-05-06 14:10:12 -07:00
kirjs 7d9862f0be refactor(forms): support deprecated signatures for backward compatibility
This commit restores support for passing functions directly to hidden, disabled, and readonly rules, marking them as deprecated.
2026-05-06 14:10:12 -07:00
kirjs df54e6a7b2 refactor(forms): use when consistently for conditional rules and validators
This commit updates the signal forms API to use a consistent 'when' parameter for conditional rules and validators, replacing direct function arguments.
2026-05-06 14:10:12 -07:00
Leon Senft 276c917b34 refactor(forms): add validation rules for date constraints (#68001)
- Added `minDate()` and `maxDate()` for validating constraints on `Date` inputs.
- `ReadonlyFieldState.min` and `.max` now return
  `Signal<NonNullable<TValue>`. This ensures that `min` and `max` inputs
  on custom controls can accept a reliable type (matching their value
  type).
- Made the `TWrite` type parameter of `MetadataKey` contravariant to
  properly indicate that it's writable.
- Added `LimitKey` as a convenience type for defining validation limit
  metadata (e.g. `MAX_NUMBER`, `MIN_DATE`).
- Added `LimitSelectionKey` which can be used to bind a `LimitKey` with
  value-specific aggregation logic, to a generic metadata key (e.g. use
  `MAX_NUMBER` to aggregate numbers for `MAX`).

PR Close #68001
2026-05-06 11:59:18 -07:00
Leon Senft 592a12d6c9 refactor(forms): remove string support from min and max validation rules (#68001)
The `min` and `max` validation rules previously handled `string` values
to accommodate numbers bound to text inputs. However, this is no longer
necessary as the control binding itself handles the conversion.

This change removes string support from these rules, simplifying the
types to `number | null`. The validation logic has been updated to use
concrete checks (`value === null || Number.isNaN(value)`) to ensure safe
TypeScript narrowing.

Associated tests have been updated to:
- Remove string-specific validation checks.
- Add coverage for text input bindings.
- Add coverage for empty input handling (standard behavior where empty
  sets model to null and skips validation).

BREAKING CHANGE: `min` and `max` validation rules no longer support
string values. Bound values must be numbers or null.

PR Close #68001
2026-05-06 11:59:18 -07:00
Alex Rickabaugh 98c5afdb02 perf(forms): lazily instantiate signal form fields
Currently, Signal Forms eagerly instantiates all nodes in the form tree because `childrenMap` iterates over the `value` and creates a `FieldNode` for every property. This ensures validation side-effects are run early, but creates pure overhead for fields without validation logic unless explicitly accessed.

This commit makes `childrenMap` lazy by default, skipping materialization for children without schema logic. This is achieved by introducing `hasLogicRules()` and `anyChildHasLogic()` across the `LogicNode` hierarchy. Fields are now only instantiated when a direct read occurs via `getChild()` (which calls the new `ensureChildrenMap()`) or if their subtree requires eager evaluation due to existing validation rules.

Fixes #67212
2026-03-20 15:09:26 -07:00
Leon Senft c767d678cf feat(forms): add 'blur' option to debounce rule
Expands the `debounce` rule configuration to accept `'blur'`. When this option
is provided, the rule will delay model synchronization until the field loses
focus (is touched). This introduces a debouncer that defers resolution
until the framework automatically aborts pending debounces upon touch events.
2026-03-05 09:55:14 -08:00
SkyZeroZx 24c0c5a180 feat(forms): support signal-based schemas in validateStandardSchema
Allow `validateStandardSchema()` to consume a computed schema so
validation rules stay in sync when the schema changes over time.

This supports schemas stored in computed signals (e.g. zod schemas that
depend on input signals) and ensures the effective schema updates after
initialization instead of being captured once.

Fixes #66867
2026-02-06 07:40:46 -08:00
Leon Senft 26d12158e1 refactor(forms): convert FieldState.controlValue to a WritableSignal
Remove `setControlValue()` from `FieldState` and convert `controlValue` to a
`WritableSignal` whose setter implements the debounced syncing behavior
of `setControlValue()`.
2026-01-30 09:14:14 -08:00
cexbrayat 7fd076bdd3 test(forms): improve test coverage for multiple pattern validators
Add comprehensive test cases to validate behavior when multiple
pattern validators are applied to the same field.

These tests clarify that multiple patterns operate with AND logic,
where each pattern is validated independently and produces its own
error when it doesn't match.
2026-01-12 13:48:16 -08:00
Leon Senft 46dbd18566 refactor(forms): remove customError()
Remove the `customError` function and `CustomValidationError` type.

These were made obsolete by support for returning plain object literals
as custom errors.

This also catches few `field` properties that were missed in the
renaming to `fieldTree`.
2026-01-07 15:07:30 -05:00
SkyZeroZx e7d99f02cb fix(forms): clean up abort listener after timeout
Removes the abort event listener once the debounce timeout completes.

This avoids lingering listeners, prevents potential memory leaks, and ensures
the abort logic runs at most once.
2026-01-07 14:07:25 -05:00
Miles Malerba ae0c59028a refactor(forms): rename field to fieldTree in FieldContext and ValidationError
BREAKING CHANGE:
2025-12-16 10:26:22 -08:00
Miles Malerba ebc5c2b083 feat(forms): redo the signal forms metadata API
This PR makes a number of changes to the metadata API to address design
flaws in the previous API. Some of the changes include:

- Replaces the previous `MetadataKey` and `AggregateMetadataKey` with a
  single unified `MetadataKey` that is used for all metadata.
- The new `MetadataKey` is only defined for fields that explicitly set
  it in their schema logic
- All metadata now has reducer / aggregate behavior
- The new `MetadataKey` has an option to create a managed key which
  wraps the result of its computed aggregate into some other structure
  such as a `Resource` or `linkedSignal`
- There are now two APIs to create metadata keys
  - `createMetadataKey` for pure computed metadata
  - `createManagedMetadataKey` for metadata that manages its computation
    internally
2025-12-09 09:21:41 -08:00
Kirill Cherkashin 5be33048cc refactor(forms): Break logic.ts into separate files
This would make it easier to navigate
2025-12-04 11:33:20 -08:00
Matthieu Riegler b41a94bc85 fix(forms): Set error message of a schema error.
Use the error message of the issue as the error message of the error itself.

fixes #65247
2025-11-17 09:41:54 -08:00
Miles Malerba 722292f215 refactor(forms): improve typing on min & max (#65212)
If we're calling `min` on a path that's guaranteed to be `number` we
don't want to make the users validator function handle the `null` or
`string` cases.

This uncovered an issue in the `SchemaTreePath` type which needed to be
fixed by preventing the model type from being distributed over.

PR Close #65212
2025-11-14 21:56:58 +00:00
Miles Malerba 4f8ab4fa3c refactor(forms): allow passing number|string|null paths to min & max (#65212)
Relaxes the constraints on which paths can be used with the `min` &
`max` validation rules, since people may want to validate a
potentially-null number, or a numeric value represented as a string

PR Close #65212
2025-11-14 21:56:58 +00:00
Leon Senft 98ce9a7b17 refactor(forms): use AbortSignal to cancel debounced updates
Add an `AbortSignal` parameter to `Debouncer`. Implementations may
choose to accept this parameter to be informed when a debounced
operation is aborted. This may be useful for canceling pending timers or
avoiding unnecessary work.
2025-11-13 09:59:19 -08:00
Leon Senft d337cfb68f feat(forms): add debounce() rule for signal forms
The `debounce()` rule allows developers to control when changes to a
form control are synchronized to the form model.

This feature necessitated some changes to `FieldState`:

  * `controlValue` is a new signal property that represents the current
    value of a form field as it appears in its corresponding control.

  * `value` conceptually remains unchanged; however, its value may lag
    behind that of `controlValue` if a `debounce()` rule is applied.

The `debounce()` rule essentially manages when changes to `controlValue` are
synchronized to `value`. The intent is that an expensive or slow
validation rule can react to the debounced `value`, rather than a more
frequently changing `controlValue`.

Directly updating `value` immediately updates `controlValue`, and cancels any
pending debounced updates.

When multiple `debounce()` rules are applied to the same field, the last
currently active rule is used to debounce an update. These rules are
applied to child fields as well, unless they override them with their
own rule.
2025-11-11 12:00:09 -08:00
Miles Malerba c9058087ae refactor(forms): support dynamic object logic
extends `applyEach` to work on objects as well, conditionally applying
logic to each property of the object.
2025-11-06 13:42:48 -08:00
Miles Malerba fa02d96508 test(forms): re-enable some disabled tests
Adjusts test behavior to reflect current implementation and enables them
2025-11-06 09:41:28 -08:00
Miles Malerba 96388b6d8a refactor(forms): improve discoverability of ValidationError flavors (#64747)
Improves discoverability by putting the WithField, WithoutField, etc as
subtypes of the main ValidationError type

PR Close #64747
2025-11-05 22:42:59 +00:00
Miles Malerba fa13a167f1 refactor(forms): preserve logic order when apply is used on root path
In some cases the logic order was not preserved properly when using `apply`. In particular this occurs when some logic is registered on a child of the root, followed by an apply to the root, followed by further logic registered on a child. In this case the final registered logic wound up running before the applied logic.

This happened because `FieldPathNode` for a child path was caching its `LogicNodeBuilder` at creation time. This meant that if the parent's `LogicNodeBuilder` changed (e.g., due to an `apply` call), the child would still be using the old one.

This commit fixes the issue by dynamically resolving the `LogicNodeBuilder` for a child path whenever it is accessed.
2025-10-29 10:40:07 +01:00
kirjs c57bbaa87d refactor(forms): Allow returning plain values from validators
This makes the API nicer to use
2025-10-28 20:50:45 +01:00
kirjs 8562e29be4 refactor(forms): Allow returning plain values from validators
This makes the API nicer to use
2025-10-28 20:50:45 +01:00
kirjs 28847c9a19 refactor(forms): Allow returning plain values from validators
This makes the API nicer to use
2025-10-28 20:50:45 +01:00
kirjs b5c29d0d0a refactor(forms): Allow returning plain values from validators
This makes the API nicer to use
2025-10-28 20:50:45 +01:00
Miles Malerba 01e1ad2b37 refactor(forms): fix some typos
Fix some typos in signal forms code
2025-10-27 09:27:02 +01:00
Miles Malerba 884765be56 refactor(forms): rename field state related to metdata (#64603)
Renames the field state related to metadata to reflect the new
"metadata" name. In particular:
- `property(...)` is renamed to `metadata(...)`
- `hasProperty(...)` is renamed to `hasMetadata(...)`

PR Close #64603
2025-10-23 18:13:16 +02:00
kirjs a3e91b2e96 refactor(forms): Add a test better documenting behavior of the error function in built-in validators (#63993)
We will need to find a better way to name it.

PR Close #63993
2025-10-22 18:27:37 +00:00
Miles Malerba 00a1806eaa refactor(forms): add validateStandardSchema to public api (#63616)
Add `validateStandardSchema` which was accidentally omitted from the
public api.

PR Close #63616
2025-09-08 13:58:55 -07:00
Miles Malerba 10ef96adb3 fix(forms): consistent treatment of empty (#63456)
Removes custom handling of emptiness in several of the validators and
replaces it with a common `isEmpty` check. The common empty check
considered the following values to be empty: `null`, `undefined`, `''`,
`false`, `NaN`

Generally most validators should treat an empty value as valid. This
aligns with both the behavior or native HTML validators and reactive
forms validators.

As an example, consider an optional email field. If the email validator
considered empty string to be an invalid email, there would be no way
for the user to not enter it.

There are several exceptions to this rule:
- `required` whose entire purpose is to ensure that the field is *not*
  empty
- `validateStandardSchema` which should subject all values including
  empty ones to the specified standard schema. It is up to the schema to
  decide whether an empty value is valid or not
- `validate`/`validateAsync` which leaves it up to the user's custom
  validation logic to decide if an empty value is valid.

PR Close #63456
2025-08-29 14:43:03 -07:00
Miles Malerba b8314bd340 feat(forms): add experimental signal-based forms (#63408)
This commit introduces an experimental version of a new signal-based forms API for Angular. This new API aims to explore how signals can be leveraged to create a more declarative, intuitive, and reactive way of handling forms.

The primary goals of this new signal-based approach are:

*   **Signal-centric Design:** Place signals at the core of the forms experience, enabling a truly reactive programming model for form state and logic.
*   **Declarative Logic:** Allow developers to define form behavior, such as validation and conditional fields, declaratively using TypeScript. This moves logic out of the template and into a typed, testable schema.
*   **Developer-Owned Data Model:** The library does not maintain a copy of data in a form model, but instead read and write it via a developer-provided `WritableSignal`, eliminating the need for applications to synchronize their data with the form system.
*   **Interoperability:** A key design goal is seamless interoperability with existing reactive forms, allowing for incremental adoption.
*   **Bridging Template and Reactive Forms:** This exploration hopes to close the gap between template and reactive forms, offering a unified and more powerful approach that combines the best aspects of both.

This initial version of the experimental API includes the core building blocks, such as the `form()` function, `Field` and `FieldState` objects, and a `[control]` directive for binding to UI elements. It also introduces a schema-based system for defining validation, conditional logic, and other form behaviors.

Note: This is an early, experimental API. It is not yet complete and is subject to change based on feedback and further exploration.

Co-authored-by: Kirill Cherkashin <kirts@google.com>
Co-authored-by: Alex Rickabaugh <alxhub@users.noreply.github.com>
Co-authored-by: Leon Senft <leonsenft@users.noreply.github.com>
Co-authored-by: Dylan Hunn <dylhunn@gmail.com>
Co-authored-by: Michael Small <michael-small@users.noreply.github.com>

PR Close #63408
2025-08-28 09:02:43 -07:00