113 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
Shuaib Hasan Akib 6da6d7af65 refactor(core): use native Promise.withResolvers() in remaining tests
Replaces the remaining hand-rolled deferred promise implementations
with the native `Promise.withResolvers()` API and removes the now
unused helper and import.

Follow-up to #69739.

(cherry picked from commit 1bc7e3c2c3)
2026-09-09 16:17:09 +02:00
SkyZeroZx 3bc03fa6a9 test(forms): remove redundant change detection configuration
OnPush is now the default change detection strategy, and tests run zoneless by default, so the explicit configuration is no longer needed.

(cherry picked from commit c235ecef8a)
2026-09-09 16:13:14 +02: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
SkyZeroZx 72e766456e refactor(forms): modernize Signal Forms tests
replace change detection calls with whenStable for async tests

(cherry picked from commit 4631a6e479)
2026-08-07 23:23:02 +00:00
SkyZeroZx c73dd603a4 refactor(forms): modernize signal forms tests to rely on whenStable
Rely on zoneless test scheduling instead of manually triggering change detection. Keep Signals Forms tests aligned with the async-first testing pattern.

(cherry picked from commit 59f6ef690b)
2026-07-29 09:57:42 -07:00
Matthieu Riegler 3497c9b943 fix(forms): ensure pending status propagates to the root form in signal forms
Previously, the `pending()` status on a field's `ValidationState` only checked if the field itself or its immediate children had a pending asynchronous validator by directly inspecting `asyncErrors()`. This meant that a pending asynchronous validator deep within a nested form (e.g. on a grand-child) would not correctly bubble the `pending` state up to the root form.

fixes #69840
2026-07-21 13:48:32 +02: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
volkanfilazi b542302a23 fix(forms/signals): make extractValue reactive for compat AbstractControl values 2026-07-08 11:23:00 -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
cexbrayat 45e8fb5d6c refactor(forms): type built-in getError results
Add overloads for built-in validation error keys so callers get precise error payload types from getError.

This enables signal forms template patterns like:

```html
@if (login.getError('minLength'); as minLengthError) {
  <div>Login should be {{ minLengthError.minLength }} characters</div>
}
```
2026-05-29 14:58:40 +02:00
Douglas Parker 1963a0eb18 refactor(forms): add provideExperimentalWebMcpForms
This enables the use of the `experimentalWebMcpTool` option on signal forms and implicitly declares a WebMCP tool based on the form data model. This is an experiment inspirted by the WebMCP declarative forms API to see if Angular's framework-level knowledge of the form's declarative data model can produce higher quality WebMCP tools than the web standard can on its own with less effort from the developer.

Example:

```typescript
// main.ts

import {bootstrapApplication} from '@angular/platform-browser';
import {provideExperimentalWebMcpForms} from '@angular/forms';
import {MyComp} from './form';

bootstrapApplication(MyComp, {
  providers: [
    // Activate the feature.
    provideExperimentalWebMcpForms(),
  ],
});
```

```typescript
// form.ts

import {Component, signal} from '@angular/core';
import {form} from '@angular/forms';

@Component({ /* ... */ })
export class MyComp {
  private readonly f = form(signal({
    firstName: '',
    lastName: '',
  }), {
    // Implicitly creates a WebMCP tool named `createUser` which accepts a `firstName` and `lastName` as parameters.
    experimentalWebMcpTool: {
      name: 'createUser',
      description: 'Creates a user with the given name.',
    },

    // Invokes the submit action when the agent calls the WebMCP tool.
    submission: {
      action: () => {
        console.log('User clicked submit, or agent called the tool!');
      },
    },
  });

  // ...
}
```
2026-05-15 11:35:22 -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
Suraj Yadav 68c3abbe09 fix(forms): synchronize controls with the model on reset
Synchronize `controlValue` with the model `value` following `reset()`. This
ensures the UI will reflect the form model in cases where a control had a
pending change–delayed by debouncing–at the time it was reset.
2026-05-06 10:45:40 -07:00
Leon Senft 708631f2c4 fix(forms): prohibit concurrent submits in signal forms
Prohibit concurrent submits in signal forms to prevent duplicate actions and side effects when a submission is already in progress.

If `submit()` is called while a prior submit is in progress for the same field or any of its parents, it returns `false` immediately without running the action again.

This commit also updates the documentation in `form-submission.md` to reflect this behavior.

Fixes #68317
2026-05-05 11:14:03 -07:00
Alex Rickabaugh 9b9769479b perf(forms): shortcut deepSignal writes if value is unchanged
Avoid deep write path traversal and triggering source signal updates
when calling `deepSignal.set(value)` with a new value that is
identical to the current value (`Object.is`).

This shortcuts the entire write path and unnecessary array/object
copying early on. This approach relies on the guarantee that `source`'s
value is non-nullable in the context where `deepSignal` is created and
used.

TAG=agy
CONV=9e5bd277-0d0a-466c-be36-5e3a8e6910be
2026-05-05 09:28:00 -07:00
Alex Rickabaugh 0ea50ffe5a fix(forms): ensure debounced async validators produce pending status during debounce
When using a debounced async validator, the pending status from the internal
debounced resource was not flowing through to the resource created by the
factory. Replicate the 'chain' logic using the new privately exported ɵchain
function to propagate the loading status correctly.

Fixes #68105
2026-05-04 13:03:07 -07:00
Alex Rickabaugh e0536091f5 perf(forms): optimize reactivity by using shallow array equality
Add `shallowArrayEquals` to computed signals returning arrays of errors or reasons in Signal Forms. This prevents unnecessary downstream invalidations when the content of the arrays remains unchanged.
2026-04-30 15:41:45 -07:00
Matthieu Riegler b2083a7fd2 build: cleanup workspace deps
Some of the deps are move down to the only targets that uses them.
2026-04-23 11:38:26 -07:00
Alex Rickabaugh 24e52d450d feat(forms): add debounce option to validateAsync and validateHttp
This adds support for a `debounce` option to the `validateAsync` and `validateHttp` functions.
This allows developers to debounce the triggering of async validators to improve performance.

A `DebounceTimer` type was also added to `@angular/core` to represent the wait condition parameters uniformly.
2026-03-25 14:17:52 -07:00
Alex Rickabaugh 709f5a390c feat(forms): add FieldState.getError()
Added a `getError(kind: string)` method to `FieldState` that returns the first validation error of a given kind, or `undefined` if no such error exists. This method is reactive and will re-evaluate when errors change.

Fixes #63905

Also updated public API goldens and added unit tests.
2026-03-24 15:07:55 -07:00
Alex Rickabaugh ee8d2098cb fix(forms): change FieldState optional properties to non-optional | undefined
This improves compatibility with TypeScript's exactOptionalPropertyTypes.

Fixes #67246
2026-03-24 14:51:31 -07:00
Leon Senft 0eeb1b5f03 fix(forms): allow FormRoot to be used without submission options (#67727)
The `[formRoot]` directive will no longer call `submit()` if the bound
form doesn't define its own submission options. This allows the
directive to be used solely for the default behavior it provides:
setting `novalidate` on the `<form>` and calling `preventDefault()` on
the `submit` event.

Fix #67367

PR Close #67727
2026-03-23 15:41:19 -07:00
Leon Senft f4a5b42ebe refactor(forms): rename directive files for consistency (#67727)
- **FormField**:  `form_field_directive.ts` -> `form_field.ts`
- **FormRoot**:   `ng_signal_form.ts`       -> `form_root.ts`

PR Close #67727
2026-03-23 15:41:19 -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
Alex Rickabaugh 74f76d8075 feat(forms): add reloadValidation to Signal Forms to manually trigger async validation
This commit introduces a formal mechanism to manually re-trigger
asynchronous validations in Signal Forms, addressing #66994.

It exposes a `reloadValidation` method on the `FieldState` interface
that recursively cascades down the form tree and invokes the underlying
`ResourceRef`'s `reload()` method for any metadata keys tagged with the
internal `IS_ASYNC_VALIDATION_RESOURCE` symbol.

Fixes #66994
2026-03-19 15:22:13 -07:00
Alex Rickabaugh 83032e3605 fix(forms): support generic unions in signal form schemas
This commit resolves an issue where using an uninstantiated generic type
parameter in a signal form model caused TypeScript compilation failures due to
distributive conditional types (#66596). The previous attempt to fix this issue
by tuple-wrapping everything caused another bug (#65535) that prevented property
access on generic unions.

This commit balances the need to resolve nested generic property access while
handling infinitely recursive generic structures without depth errors.

What changed and why:
- Base State Wrappers: Tuple wrappers (`[TModel] extends [AbstractControl]`) are
  applied to `FieldTreeBase` to safely defer generic evaluation. This prevents
  primitive unions (like `boolean`) from incorrectly evaluating to `never`.
- Naked Map Over Children: Object subfield checks (`TModel extends Record`) are
  re-evaluated as purely naked conditionals. Eager distribution over generics
  allows users to directly access shared properties of unresolved union types.
- Array Interface Deflection: `ReadonlyArrayLike<T>` generic abstraction is
  redefined as an explicit `interface` instead of a mapped `Pick` type alias.
  This optimally intercepts TypeScript from eagerly evaluating infinitely
  recursive array structures (e.g. `RecursiveType = (number | RecursiveType)[]`).
- Overloaded Context Methods: `FieldNodeContext.stateOf` and `fieldTreeOf` are
  defined as explicitly overloaded class methods and lexically bound (`this`) in
  the constructor. These changes are required to safely align the runtime bindings
  with the tautological conditionals implemented in the `RootFieldContext`
  interface structure.

Fixes #65535
2026-03-17 17:59:36 -06:00
Kristiyan Kostadinov b18592a1a8 build: fix failing test in forms
Fixes a test that started failing after a couple of related changes landed at the same time.
2026-03-17 15:31:35 +01:00
Leon Senft eeba51c50b refactor(forms): make markAsTouched() touch all descendants by default
`markAsTouched()` now marks all descendants as touched. In general this
method is called when controls update the model. Most controls update
leaf nodes, in which case this change has no effect.

Marking all descendants allows triggering validation for subsections of
a form, independently from having to call `submit()` on the entire form.

`markAsTouched()` now accepts a `MarkAsTouchedOptions` parameter, which
includes a `skipDescendants` property. This can be used mark only the
receiving field as touched: `node.markAsTouched({skipDescendants: true})`.
2026-03-16 16:03:36 -06:00
kirjs a94958b59b refactor(forms): Address more feedback
Minor test and type cleanup
2026-03-16 15:24:20 -06:00
kirjs b061495134 refactor(forms): Add more tests
thoroughly tests both propagation directions
2026-03-16 15:24:20 -06:00
kirjs 0d92708b33 refactor(forms): adress feedback
Drop unnecessary casts + cleanup
2026-03-16 15:24:20 -06:00
kirjs 6703e6c803 refactor(forms): add structured extract filter
It's a helper function that takes a form, and extrasts it's value unwrapping compat values and also allowing to filter them.
2026-03-16 15:24:20 -06:00
Leon Senft 57ba621c81 test(forms): read only context prevents writing to field value
Test that the read only context prevents writing to a field value:

* In validation rules
* In a provided configuration
2026-03-10 15:07:42 -07:00
Leon Senft a1a6c5282e refactor(forms): restrict reactive logic to a readonly API
Reactive logic in forms is not intended to mutate state, but this was
poorly communicated by the permissive and highly mutable field context
provided to all logic functions. This change splits all of the
state-related API into writable and readonly interfaces.

* Top-level functions that produce a `FieldTree` (e.g. `form()`) expose
  writable signals (e.g. `value: WritableSignal<T>`) and mutating
  methods (e.g. `markAsDirty()`).

* Reactive logic expose readonly signals (e.g. `value: Signal<T>`) and
  omit mutating methods.
2026-03-10 15:07:42 -07:00
Sonu Kapoor 71b8159b37 test(forms): cover transformedValue without FormField context
Adds a test verifying that `transformedValue` exposes parse errors via
the returned signal's `parseErrors()` property when no FormField
context is present.

This ensures that:
- parse errors are still observable without DI-based field propagation
- the model is not updated when `parse` omits `value`
- valid input clears parse errors and updates the model

This test protects the documented contract that DI-based error
propagation is expected for FormValueControl usage, while standalone
usage relies on explicit consumption of `parseErrors()`.
2026-03-09 16:41:48 -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
Miles Malerba 23fd8fa586 fix(forms): use consistent error format returned from parse
Aligns the errors returned from the `parse` function in
`transformedValue` to use the same convention as the rest of signal
forms (a property called `error` that can contain a single error or list
of errors)
2026-02-23 09:11:51 -08:00
Leon Senft 1a19d61e19 refactor(forms): clean up
* Remove unused `TValue` type parameter from `FormUiControl`
* Remove unused imports
* Remove unnecessary cast
2026-02-23 09:09:55 -08:00
cexbrayat fe25c57a5c fix(forms): preserve parse errors when parse returns value
Fixes #67170 by keeping the errors even a value is returned from the parse function.
2026-02-20 10:28:54 -08:00
Miles Malerba 27397b3f4f fix(forms): clear parse errors when model updates (#66917)
Changes `parsedErrors` to a `linkedSignal` based on the model value.
This ensures that the parse errors are reset if the model changes from
outside the control.

PR Close #66917
2026-02-13 12:11:06 -08:00
Miles Malerba ba009b6031 feat(forms): add form directive
Adds a `formRoot` directive to manage submitting the form in signal
forms.
2026-02-10 14:34:48 -08:00
Miles Malerba f56bb07d83 feat(forms): add field param to submit action and onInvalid
The `action` and `onInvalid` handlers now recevie two pieces of
information:
1. The form that is being submitted
2. The specific field that the submit was triggered on

Remove the `submit()` method on field state - supporting this is complex
from a typing perspective, since the `FieldState` only knows its
`TValue` type, not the `TModel` type of its owning `FieldTree`. Rather
than try to pack additional generics on to `FieldState`, we'll just
leave the `submit` function as a standalone importable function.
2026-02-09 14:49:43 -08:00