Skip to content

Reactive model updates in FormValueControl via computation #66453

@vitNapastiuk98

Description

@vitNapastiuk98

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 extending model() 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

No type

Projects

Status

No status

Relationships

None yet

Development

No branches or pull requests

Issue actions