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)
Two of the three `compatForm` overloads open a fenced code block in their
`@example` and never close it. The fence then runs to the end of the tag, so the
API renderer emits a plain `<pre><code>` block for those two instead of a
highlighted one.
The result is that on the compatForm API page the first overload's example is
syntax highlighted while the other two, which contain identical code, render as
flat unstyled text.
Rebuilding //adev/src/assets:content with and without this change alters 1 of
1565 pages. That page goes from 6 highlighted and 2 plain code blocks to 8
highlighted and none plain.
(cherry picked from commit b9fb456971)
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)
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)
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)
Fixes that signal forms were overridding the `name` input of the CVA with an auto-generated one. This can break directives like `mat-radio` that assign their own `name`.
Fixes#69677.
(cherry picked from commit 38861ac41b)
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)
We keep getting PRs that target single usages of `hasOwnProperty` and we have ~100 of them. These changes aim to address the issue centrally by swapping out all the instances and adding a lint rule against introducing new ones.
(cherry picked from commit 732e505018)
The "Value transformation" section taught readers to hand-roll
transformation with `linkedSignal()` and a manual parse method, even
though `@angular/forms/signals` ships `transformedValue()` for exactly
this case. Readers ended up with a weaker version of a feature the
framework already provides — notably, no parse error reporting.
Rewrite the section around `transformedValue()` and document the parts
the manual pattern could not cover: returning `{error}` from `parse` to
surface parse errors on the field's `errors()`, and `reset()` clearing
them. This also makes good on the cross-reference from the validation
guide, which pointed here for parse error details the section never
covered.
Fixes#70206
Co-authored-by: Matthieu Riegler <kyro38@gmail.com>
(cherry picked from commit 182c371d82)
Recompute the checked state when a reused radio input receives a new value while the form model remains unchanged.
Handle this case for both reactive forms and signal forms, and add regression tests covering reused radio elements.
(cherry picked from commit 9e37a58e14)
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)
Adds the `isFieldTree` utility that allows users to assert whether a value is a field tree. This is something that has come up on Material recently and will be useful for users as well.
Fixes#69984.
(cherry picked from commit 2a141847a5)
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
If a directive has an input declared as `dismissible = input<boolean>(true, {transform: booleanAttribute});` then the following templates were not compiling:
```
<div directiveName dismissible="true"></div>
<div directiveName dismissible></div>
```
This commit fixes the issue, without breaking contravariant consumers.
Native text `<input type="text">` controls do not support `null` values.
When a Signal Forms model bound to a text input is set to `null`, the value
is silently coerced to an empty string.
Use controlValue() instead of value() when synchronizing
ControlValueAccessor instances.
When debounce is active, value() can still contain the
previous model value while controlValue() reflects the
latest user-entered value. This prevents stale values
from being written back to the CVA before the debounce
is flushed.
Adds a regression test covering the debounce scenario.
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
Three small, behavior-neutral cleanups to maybeRemoveStaleArrayFields:
1. Avoid allocating an empty Set when prevData.byTrackingKey is
undefined. new Set(undefined) previously created an unused empty
Set on every call for parents with no tracking keys.
2. Guard the per-element tracking-key check on `oldTracking` being
defined, skipping the isObject/hasOwn check entirely when there's
nothing to track.
3. Replace childValue.hasOwnProperty(identitySymbol) with
Object.hasOwn(childValue, identitySymbol). hasOwnProperty throws
on null-prototype array elements (Object.create(null)), which
would crash computeChildrenMap. Object.hasOwn is null-prototype-safe
and preserves "own property" semantics (does not match inherited
identitySymbol values).
4. Replace `data.byTrackingKey?.delete(id)` with
`data.byTrackingKey!.delete(id)`. The optional chaining was dead:
if oldTracking.size > 0, prevData.byTrackingKey (and therefore
data.byTrackingKey, same Map reference via the spread) is always
defined. The `?.` masked this invariant; `!` documents it and
would surface a runtime error instead of a silent no-op if the
invariant is ever violated.
Verified via performance.mark/measure instrumented directly inside
the function (count=1 call for a single-field edit in both cases).
Total duration dropped from ~0.7ms to ~0.1ms, consistent with the
avoided Set allocation in (1) and (2).
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.
A custom FormValueControl only participates in debounce('blur') if it emits
the touch output on the native blur event. This was undocumented, and the
touch name reads like a focus event, so users wired it to (focus) and
blur-based debouncing silently did nothing.
Add a dedicated guide section with a working example, link the debounce API
reference to it, and clarify the touch JSDoc that it must fire on blur, not
focus.
Fixes#69370
After #68581 graduated the Signal Forms APIs to public API and #68654 removed
the experimental warnings from the Signal Forms documentation in adev, the
package READMEs still framed the API as experimental.
Update `packages/forms/signals/PACKAGE.md`: drop the experimental title and
intro, remove the now-shipped entries from "Not yet supported" (interop with
reactive/template forms and strongly-typed binding to UI controls), and remove
the remaining experimental and exploratory wording from the FAQ.
Update `packages/forms/PACKAGE.md`: it listed only two ways to build forms
(reactive and template-driven). Add signal forms as the third.
Fixes#68724
Reading the form model on init is unsafe as it could depend on inputs (eg a required input). We need to delay the read by a tick (after the inputs are set) to ensure that values can be safely read.
fixes#69262
The `watchValidity` method in `AnimationInputValidityMonitor` was registering
an anonymous arrow function via `addEventListener` with no corresponding
`removeEventListener` call.
In V8, each closure is represented as a `JSFunction` holding a strong pointer
to a heap-allocated `Context` object containing captured variables
(`VariableLocation::CONTEXT` slots, decided at parse time by
`Scope::MustAllocateInContext`). In Blink, DOM event listeners are stored in
the element's `EventTargetData::event_listener_map` as `JSEventListener`
wrappers backed by a `v8::Persistent<JSFunction>` handle — a strong cross-heap
reference that keeps the function alive as long as the element is alive.
Because the callback passed to `watchValidity` closes over the calling
component/directive (which itself holds a reference back to the element), this
produced a cross-heap reference cycle:
```
HTMLInputElement (Blink/Oilpan)
└── EventTargetData → JSEventListener → v8::Persistent<JSFunction>
└── Context → callback closure
└── component → HTMLInputElement ← cycle
```
Neither V8's nor Blink's GC could independently break this cycle because it
crosses the V8/Oilpan heap boundary. The element was therefore never collected
after being removed from the DOM.
The fix stores the listener in a named local variable and registers its removal
via `DestroyRef.onDestroy`, tying cleanup to the lifetime of the component that
owns the element. This ensures `removeEventListener` is called with the exact
same `JSFunction` reference, causing Blink to drop the `v8::Persistent` handle
and allowing both the function and the element to become GC-eligible.
This tells the agent that all input properties have been explicitly declared and that it should not attempt to specify additional arguments with unknown names. This provides a little more safety and gives the AI a little more information about the allowed set of inputs for this tool.
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>
}
```
The `errors` linkedSignal in `createParser` had no equality check, so
every reset or recomputation — even to an identical empty array — would
mark downstream dependents as dirty and trigger unnecessary re-renders.
Add `shallowArrayEquals` as the equality function so the signal only
notifies dependents when the error list actually changes.
`parseErrors` in `FormField` always produced a new array on every recomputation, even when nothing actually changed. The `?? []` fallback created a new empty array whenever `parseErrorsSource` was undefined, and `.map()` also returned new object references each time.
Since computed signals use reference equality by default, those new arrays were treated as changed values. That caused unnecessary updates to propagate through `validationState.parseErrors` and the combined errors chain, triggering extra recomputations during change detection.
Fix this by adding `{equal: shallowArrayEquals}` to the `parseErrors` computed, matching the existing `errors` computed and the validation computeds in `field/validation.ts`.
This prevents empty arrays from triggering updates while still correctly propagating real parse-error changes.
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!');
},
},
});
// ...
}
```
This commit removes runtime console warnings and uses TypeScript overloads with JSDoc @deprecated annotations to handle backward compatibility for conditional rules.
This commit updates the signal forms API to use a consistent 'when' parameter for conditional rules and validators, replacing direct function arguments.
Replaced `@experimental` tags with `@publicApi 22.0` across all Signal Forms APIs under `packages/forms/signals` to mark them as ready for general use in v22.
TAG=agy
CONV=0af6c644-225a-4212-a49a-5843d17ec638
* Test that `minDate`/`maxDate` binds to `min`/`max` on date and time inputs
* Test that `min`/`max` attribute can be set directly on date and time inputs
* Relax type checker to allow `min`/`max` bindings on date and time inputs
PR Close#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