-
Notifications
You must be signed in to change notification settings - Fork 27k
Description
Which @angular/* package(s) are relevant/related to the feature request?
forms
Description
I would like to propose an enhancement to Angular Signal Forms that allows a model() used inside a FormValueControl to derive its value from a reactive computation, instead of acting purely as a mutable storage primitive.
Today, model() represents the value exposed by a custom control and is expected to be updated manually. However, in many real-world scenarios, that value is derived from multiple internal reactive fields (e.g. unit + amount, currency + precision, date + timezone).
This proposal suggests extending model() with a computed/linked variant that automatically recalculates and propagates its value whenever its reactive dependencies change, similarly to how linkedSignal works in @angular/core.
The goal is to enable more declarative, reactive, and maintainable FormValueControl implementations when using Signal Forms.
The Problem
When building custom FormValueControl components with Signal Forms, developers often manage multiple internal reactive fields whose combined state determines the value exposed to the parent form.
Currently, keeping the model() value in sync with this internal state requires imperative glue code:
- Manual calls to
this.value.set(...) - Synchronization tied to DOM events such as
(input)or(change) - Repeated logic scattered across event handlers
This approach introduces several drawbacks:
- Imperative over reactive: Updates depend on events rather than state changes
- Boilerplate-heavy: Each internal field change must be manually wired
- Error-prone: Easy to miss updates in complex or dynamic controls
- Poor scalability: The pattern does not scale well for rich inputs (currency, units, dates, formatted values)
When migrating from classic Reactive Forms to Signal Forms, this feels like a regression in declarativity, as derived values cannot be expressed directly through reactive computation.
A built-in way to define a derived model value would remove this friction and align Signal Forms more closely with Angular’s reactive design philosophy.
Example
@Component({
selector: 'app-new-weight-input',
standalone: true,
imports: [Field, ReactiveFormsModule],
template: `
<input
type="number"
[field]="form.val"
(input)="setValue()"
/>
<select
[field]="form.unit"
(change)="setValue()"
>
<option value="kg">KG</option>
<option value="lbs">LBS</option>
</select>
`,
})
export class NewWeightInputComponent implements FormValueControl {
readonly value = model(0);
weight = signal({ unit: 'kg', val: 0 });
form = form(this.weight);
setValue() {
this.value.set(
this.weight().unit === 'kg'
? this.weight().val
: this.weight().val * 0.453592
);
}
}In this example, the exposed form value is derived from multiple internal fields, yet it must be synchronized imperatively via DOM events. As the control grows in complexity, this pattern becomes increasingly difficult to maintain.
Proposed solution
Introduce a computed or linked variant of model() (for example model.linked()), allowing a FormValueControl to expose a value that is reactively derived from internal state.
The proposed API would:
- Accept a computation function instead of an initial static value
- Track any reactive dependencies accessed inside the computation
- Automatically update the model value when those dependencies change
- Propagate the updated value to the parent form without manual
set()calls
Hypothetical API
export class WeightInput implements FormValueControl {
internalForm = form({ unit: 'kg', amount: 0 });
readonly value = model.linked(() => {
const { unit, amount } = this.internalForm.value();
return unit === 'lbs'
? amount * 0.453592
: amount;
});
}This keeps the control fully declarative: the exposed value is a pure function of reactive state, rather than something that must be synchronized manually through event handlers.
Design Notes
- The API name is illustrative; alternatives such as
derivedModel,computedModel, or extendingmodel()with an overload could also be considered - Write access could remain restricted or optional, depending on whether two-way updates are desired
- Internally, this could reuse the same dependency-tracking mechanism already used by
linkedSignal
This approach would significantly reduce boilerplate and improve the developer experience when building complex custom form controls with Signal Forms.
Alternatives considered
Manual synchronization via event handlers
The current recommended approach is to manually synchronize the model() value by calling this.value.set(...) inside DOM event handlers such as (input), (change), or (blur).
While functional, this approach:
- Relies on imperative glue code
- Couples reactive state updates to DOM events
- Becomes verbose and error-prone as control complexity grows
This pattern does not scale well for controls with multiple internal reactive fields.
Using computed() + effect() internally
Another possible workaround is to derive the value using computed() and then mirror it into model() via an effect():
const computedValue = computed(() => /* derive value */);
effect(() => this.value.set(computedValue()));Although reactive, this solution:
- Requires additional boilerplate
- Introduces an extra synchronization layer
- Feels indirect and non-idiomatic for a
FormValueControl
Exposing internal form instead of a derived value
In some cases, developers may choose to expose the internal form structure directly and let the parent handle derivation logic.
This shifts complexity to consumers, breaks encapsulation, and undermines the purpose of custom form controls.
Why a built-in solution is preferable
A first-class, computed-capable model() API would be:
- More declarative
- Less error-prone
- Consistent with Angular’s existing signal primitives
It would allow developers to express derived form values directly, without relying on indirect or imperative workarounds.
Metadata
Metadata
Assignees
Type
Projects
Status