Commit graph

71 commits

Author SHA1 Message Date
Miles Malerba
5880fbc73c 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

(cherry picked from commit ebc5c2b083)
2025-12-09 09:21:45 -08:00
cexbrayat
55fc677cef fix(forms): add signals for dirty, hidden, and pending states in custom controls
FormUiControl states that hidden, pending and dirty will be bind in custom controls, but this is currently not the case.

Fixes #65575

(cherry picked from commit 9fe9566581)
2025-12-08 10:30:48 -08:00
Kirill Cherkashin
7de8558219 refactor(forms): Break logic.ts into separate files
This would make it easier to navigate

(cherry picked from commit 5be33048cc)
2025-12-04 11:33:26 -08:00
Alex Rickabaugh
bf1c12cd93 fix(forms): memoize reads of child fields in signal forms (#65802)
Previously, navigating a `FieldTree` in signal forms involved reactive reads
of the value of the parent field(s), both directly and via `.childrenMap()`.

This meant that on _any_ change to the value of a field, reactive
notifications would trigger updates of computeds, reruns of effects, etc.
So for example, this effect would run on every change to the form:

```ts
const f = form(signal({data: 'abc', unrelated: 0}));
effect(() => {
  // accessing f.data incurs a dependency on f().value() which changes
  // on every change in the whole form
  console.log(f.data().value());
});
```

This is deeply counterintuitive and troublesome when attempting to write
effect logic, and also results in `computed`s unnecessarily updating.

This change introduces the concept of a "reader" computed, which memoizes
the access of a field at a given key via the reactive graph. With this, the
same `f.data` access above now depends on the `data` reader in `f` only,
which is effectively a constant computed. As a result, the effect only
reruns on changes to `data`'s value, as intended.

PR Close #65802
2025-12-03 12:52:43 -08:00
SkyZeroZx
6b4ab876e8 feat(forms): Allows transforms on FormUiControl signals
Extends the `FormUiControl` interface to allow `InputSignalWithTransform` in addition to `InputSignal` for its properties.

Fixes #65756

(cherry picked from commit 7d1e502345)
2025-12-03 15:13:06 +01:00
Leon Senft
a5dbd4b382 fix(forms): support dynamic [field] bindings (#65599)
Support binding a `Field` directive to a component created dynamically
with `createComponent()`.

Fix #64632

PR Close #65599
2025-12-03 15:10:49 +01:00
Matthieu Riegler
017672f742 refactor(compiler): Generate the controlCreate instruction after the native element has been created
This is necessary to exclude a race condition where the MutationObserver initialized by the instruction fired before the inputs are binded.

fixes #65678

(cherry picked from commit f35b2ef47c)
2025-12-02 12:59:53 +01:00
kirjs
e3f5f34732 refactor(forms): Make reset take value
Now you can do form.reset({name: 'cat', age: 4});

(cherry picked from commit dec222d4d7)
2025-11-25 10:51:38 -05:00
Miles Malerba
7d5c7cf99a feat(forms): add DI option for classes on Field directive
Adds a DI configuration option for signal forms that allows the
developer to specify CSS classes that should be automatically added
by the `Field` directive based on the field's status.

(cherry picked from commit c70e246c23)
2025-11-25 10:33:41 -05:00
Leon Senft
8acf5d2756 fix(forms): allow dynamic type bindings on signal form controls
The type checker will no longer prohibit binding the Signal Forms `[field]`
directive to an input with a dynamic `[attr.type]` or `[type]` binding.

(cherry picked from commit 3a1eb07c46)
2025-11-25 09:16:01 -05:00
Leon Senft
4845a33018 refactor(forms): support custom control directives
Support binding `[field]` to directives that implement
`FormValueControl` or `FormCheckboxControl`.

The `[field]` binds to whichever directive (or component) matches first in the
event there are multiple implementations. We are considering whether to make
this an error state, which could be reported during type checking.

Closes #63910, Closes #64992

(cherry picked from commit f97a1d4856)
2025-11-24 13:48:20 -05:00
Miles Malerba
de5fca94c5 fix(forms): run reset as untracked
Run the signal forms `reset()` as untracked so it does not trigger
`effect` to rerun when the model changes

Fixes https://github.com/angular/angular/issues/65322

(cherry picked from commit 7ddf4a6b07)
2025-11-19 22:27:55 +00:00
Matthieu Riegler
91d8d55a80 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

(cherry picked from commit b41a94bc85)
2025-11-17 17:41:57 +00:00
Leon Senft
e682f00e29 refactor(forms): reduce boilerplate needed to define custom controls
An early piece of feedback received regarding custom controls hosted on
native inputs was that they required a lot of boilerplate to bind
`FieldState` properties. Each property required an input to accept the
property, and a host binding to forward it to the native control.

(cherry picked from commit c727df5d38)
2025-11-17 17:41:02 +00:00
Leon Senft
d89e522a1f fix(forms): debounce updates from interop controls
* Apply any debounce rules to updates from interop controls (if configured).
* Add tests to ensure debouncing works for all control types (native, custom,
  and interop).

(cherry picked from commit b1037ec2f0)
2025-11-17 16:37:29 +00:00
Miles Malerba
e1a7c35370 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
ff4633dab8 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
Miles Malerba
966bfae337 build(forms): expose signal forms compat package
Hooks up @angular/forms/signals/compat to be released and have its docs
published

(cherry picked from commit fc1ef79ad4)
2025-11-14 17:23:39 +00:00
Leon Senft
8e988124ab test(forms): [field] inputs on components should just pass through
Test that a component with a bound `[field]` input is not treated as a
control, and that `fieldBinding` does not include the corresponding
`Field` instance.

(cherry picked from commit acb78eeb7a)
2025-11-14 16:37:45 +00:00
Leon Senft
37463525be 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.

(cherry picked from commit 98ce9a7b17)
2025-11-13 17:59:22 +00:00
Leon Senft
a278ee358c 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.

(cherry picked from commit d337cfb68f)
2025-11-11 12:00:13 -08:00
Kristiyan Kostadinov
cc25f96f02 test(forms): fix type checking errors in signal form tests
Fixes some type checking errors in the signal forms tests that accumulated while there wasn't any type checking.

(cherry picked from commit 10915f2f5f)
2025-11-07 11:57:53 -08:00
Miles Malerba
6d6fda8610 refactor(forms): allow FieldTree of recursive type
Adjusts our typings to work better with recursive types and avoid
infinite recursion in the type system.

(cherry picked from commit faccf03ee0)
2025-11-06 21:43:46 +00:00
Miles Malerba
9ea1c2216d refactor(forms): expose pathKeys as part of the API
Currently we maintain the pathKeys internally, but do not expose them
through the `FieldContext`, this PR updates the `FieldContext` to expose
this property.

(cherry picked from commit 2fd8dc9195)
2025-11-06 21:43:15 +00:00
Miles Malerba
8b4195fe7e refactor(forms): support dynamic object logic
extends `applyEach` to work on objects as well, conditionally applying
logic to each property of the object.

(cherry picked from commit c9058087ae)
2025-11-06 21:42:51 +00:00
kirjs
3bbcae6ce8 refactor(forms): add compatForm
This allows using reactive form controls in signal forms

(cherry picked from commit 60447945bc)
2025-11-06 18:51:31 +00:00
Miles Malerba
60ce31fb62 test(forms): re-enable some disabled tests
Adjusts test behavior to reflect current implementation and enables them

(cherry picked from commit fa02d96508)
2025-11-06 17:41:31 +00:00
Miles Malerba
f3e2252f4b refactor(forms): add better support for Object.keys and Symbol.iterator
Improves support for using `Object.keys` (and related methods) and
`Symbol.iterator` to work with a `FieldTree` of unknown shape.

(cherry picked from commit e87f423d90)
2025-11-06 15:51:31 +00:00
Miles Malerba
d1f4c8c18c refactor(forms): improved select support
When we set the `value` on a `<select>` element we're really just
setting the selected `<option>`. It treats the selected option as the
source of truth, rather than the actual value. This means that if
options are added or removed or their value changes, the `<select>` may
wind up with a different `value` than what's in our model.

This PR resolves this issue by adding a `MutationObserver` to the select
that is used to resync its value to the model whenever the options may
have changed.

(cherry picked from commit 194b41199b)
2025-11-06 15:49:09 +00:00
Miles Malerba
c3b4bea951 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
Leon Senft
507b3466ee perf(forms): implement change detection for field control bindings
For each field state property, check if it has changed since the last
time it was checked before writing it the corresponding form control
property.

The `pattern` and `required` properties of the field state now return a
default value rather than `undefined` if not defined by metadata.

(cherry picked from commit 41be02da2f)
2025-11-04 16:11:33 -08:00
Miles Malerba
60c8e2779c 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.

(cherry picked from commit fa13a167f1)
2025-10-29 10:40:09 +01:00
Miles Malerba
07e56596a5 refactor(forms): track arrays in a parent array by index
This commit changes arrays in a parent array to be tracked the same way
as primitive values like strings and numbers. This is necessary because
the tracking key symbol used to maintain identity for objects in an
array does not survive the array spread operation:

```
return {...oldValue} // tracking symbol preserved 
return [...oldValue] // tracking symbol lost 
```

(cherry picked from commit f80b51a738)
2025-10-28 20:52:54 +01:00
kirjs
d74fb923cc refactor(forms): Allow returning plain values from validators
This makes the API nicer to use

(cherry picked from commit c57bbaa87d)
2025-10-28 20:50:48 +01:00
kirjs
07b9be1d2c refactor(forms): Allow returning plain values from validators
This makes the API nicer to use

(cherry picked from commit 8562e29be4)
2025-10-28 20:50:48 +01:00
kirjs
71a6266dd3 refactor(forms): Allow returning plain values from validators
This makes the API nicer to use

(cherry picked from commit 28847c9a19)
2025-10-28 20:50:48 +01:00
kirjs
2c2ea2c0da refactor(forms): Allow returning plain values from validators
This makes the API nicer to use

(cherry picked from commit b5c29d0d0a)
2025-10-28 20:50:48 +01:00
csorrentino
bd780366e8 refactor(forms): add onError callback to validateHttp for HTTP errors
Adds onError callback inside validateHttp validator in signal forms

PR-Close: #63949
(cherry picked from commit a5678f6f2b)
2025-10-27 17:12:28 +01:00
Miles Malerba
22c012c985 refactor(forms): fix some typos
Fix some typos in signal forms code

(cherry picked from commit 01e1ad2b37)
2025-10-27 09:27:05 +01:00
Matthieu Riegler
a1ac9a6415 fix(forms): interop supports CVAs with signals (#64618)
The directive implemnetation might set CVA values during the template evaluation. Since the template is a reactive context we need to untrack when setting the CVA values to prevent writing to signals in a reactive context.

fixes #64614

PR Close #64618
2025-10-24 09:31:39 +02:00
Miles Malerba
0af83e6855 test(forms): test reactive iteration over array field (#64113)
Adds a test that verifies @for iteration over an array field is actually
reactive to new items being added to the array.

PR Close #64113
2025-10-24 09:29:29 +02:00
Miles Malerba
a6a5fd7b7d 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
Miles Malerba
f3c547b750 refactor(forms): rename logic functions related to metadata (#64603)
Renames logic functions related to metadata to align with the new
"metadata" name. Notably:
- `property(...)` => `metadata(...)`
- `aggregateProperty(...)` => `aggregateMetadata(...)`

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
Leon Senft
fd9af2afaf fix(forms): only propagate schema defined properties from field to control (#64446)
Prior to this change, `FieldState` defined a signal for each built-in
property. This unfortunately meant that the `Field` directive had no way
of knowing which property had actually been defined in the schema, and
would thus attempt to propagate them all to the bound form control. This
meant that the default values of these signals would override the
default or template defined values of these control properties.

Now these properties are `undefined` by default, and only initialized if
defined in the schema. Thus the `Field` directive will not attempt to
bind any properties that aren't explicitly managed by the schema.

PR Close #64446
2025-10-21 17:38:34 +00:00
Leon Senft
4261a8c583 refactor(forms): use FormValueControl instead of FormUiControl (#64446)
`FormUiControl` is the base type used for shared properties of
`FormValueControl` and `FormCheckboxControl`.

PR Close #64446
2025-10-21 17:38:34 +00:00
Leon Senft
6fca0e03ff test(forms): propagate pattern property to custom control input (#64446)
Note that unlike the other built-in properties, `pattern` is not
propagated to native controls so the lack of symmetry with other
property tests is intentional.

PR Close #64446
2025-10-21 17:38:34 +00:00
cexbrayat
9c5e969f51 fix(forms): bind invalid input in custom controls (#64526)
FormUiControl has an `invalid` input signal, but it was not bound by the control instructions.

PR Close #64526
2025-10-21 17:37:58 +00:00
Leon Senft
505bde1fed fix(forms): mark field as dirty when value is changed by ControlValueAccessor (#64471)
This corresponds with fixes made for native and custom signal form
controls: https://github.com/angular/angular/pull/64483.

PR Close #64471
2025-10-20 15:35:42 +00:00
Leon Senft
94b0afec00 fix(forms): implement interoperability between signal forms and reactive forms (#64471)
Add support for interoperability between signal forms and reactive forms that
commit effccffde0 had removed.

A signal forms field can once again be bound to any element or component with a
`ControlValueAccessor`.

PR Close #64471
2025-10-20 15:35:42 +00:00