Repository: angular
Issue: Signals for Reactive and Template-based Forms
Labels: area: forms,cross-cutting: signals

### Which @angular/* package(s) are relevant/related to the feature request?

forms

### Description

While the current forms module already supports reactivity using `valueChanges` and `statusChanges` and we can use RxJS / Signal interoperability helpers to work with signals in our reactive forms, there's still quite a gap when it comes to form handling with the new signal API.

While I believe that a complete rewrite of the Forms Module that is more aligned to the simplistic API design of signals, inject helper and signal-based component helper and is also purely based on signals for any form state, I also think that this requires a lot of time to rethink and build. I like the proposal of @ShacharHarshuv https://github.com/angular/angular/issues/51786, and it provides some good ideas about a simplistic API aligned to signals. However, there are still many things to consider (custom control values that are non-primitives and can cause confusion with groups (TValue first vs TControl first discussion in Typed forms discussion).

In the meantime, it would be great to have some updates to the existing form modules that would allow us to work with signal-based state when handling forms. Also, it would be great to expose any form-based state as reactive values contrary to the sometimes limited `valueChanges` and `statusChanges`. This would allow the combination of form state using `computed` and the execution of side-effects using `effect` to fulfil many form handling use cases.

### Proposed solution

The proposed solution is to include signals for all relevant form states within `AbstractControl` and update them whenever the local state of the form controls gets updated. This way, developers can benefit from the new reactive form state in both reactive and template-driven forms since NgForm / NgModel uses `AbstractControl` under the hood. This is a feasible and non-intrusive alternative before a possible rewrite of the Form Module.

Within this proposal, the signals of `AbstractControl` that represent the form controls state are grouped within a new `signals` object inside `AbstractControl`. This allows the regular form state and signals to coexist without causing name clashes or overcomplicated lengthy prefixes. This also encourages the extraction of specific signals using destructuring `const {value, valid} = myControl.signals;`.

For template-driven forms using `NgForm` and `NgModel`, the current idea is to provide a way to inject your signals from your component into the controls using `ngFormOptions` and `ngModelOptions`. Since the developer does not have control over the form control creation in template-driven forms, that's the simplest way to connect forms created within the template to some signals within your component. An alternative idea would be to obtain the `NgModel` directive within the component using `ViewChild` and then receive the signals from the exposed control. However, that would require you to wait for `AfterViewInit` or even `AfterViewChecked`, and this would compromise the declarative approach, which is possible by passing manually created signals into the controls.

To provide the necessary convenience to use external form control signals, I've added a helper function, `createFormSignals`. In the current version of the PR, I'm also experimenting with making the `value` signal a WritableSignal. This way, in the most simple way to use this proposal in conjunction with NgModel / NgForm, the value signal can be used to update the form value programmatically from within the component. Currently, this is done via an effect inside abstract control, and a lot of testing needs to be done to make this robust enough and ensure a consistent state while not causing any loops.

```typescript
@Component({
  selector: 'app-capacity',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <p>Reservations: {{reservations()}}</p>
    <p>Capacity: <input type="number" [formControl]="capacity" /></p>
  `
})
export class CapacityComponent {
  capacity = new FormControl(5, {nonNullable: true});
  reservations = signal(0);
  interval = setInterval(() => this.reservations.update(count => count + 1), 1000);
  hasCapacity = computed(() => this.capacity.signals.value() > this.reservations());
  capacityEffect = effect(() => console.log(this.hasCapacity() ? 'We have some capacity!' : 'No capacity!'));
}
```

This simple example with reactive forms shows that even when `FormControl` does not natively use signals, we still benefit from the newly exposed signals object and can create larger reactive constructs quite declaratively.

With template-driven forms, we can achieve similar reactive handling by using the `ngFormOptions` and `ngModelOptions` directive inputs. Since NgModel or NgForm model binding is optional, the existing template-driven forms can become the default tool when creating signal-based forms with this proposal.

```typescript
@Component({
  selector: 'app-simple-template',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input type="text" reqired ngModel [ngModelOptions]="{signals: searchQuery}" />
    <div>Results: {{results()}}</div>
  `
})
export class SimpleTemplateComponent {
  searchService = inject(SearchService);
  searchQuery = createFormSignals('');
  results = signal([] as string[]);
  searchEffect = effect(async () => {
    if (this.searchQuery.value().length > 3) {
      this.results.set(await lastValueFrom(this.searchService.search(this.searchQuery.value())));
      // Use the writable value signal to update the form programmatically (currently experimenting with this)
      this.searchQuery.value.set('');
    }
  }, {allowSignalWrites: true});
}
```

Or a little more complex template-driven form:

```typescript
@Component({
  selector: 'app-signal-based-form',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <form #form="ngForm" [ngFormOptions]="{signals}">
      <label>
        First Name:
        <input type="text" required name="firstName" ngModel/>
      </label>
      <label>
        Last Name:
        <input type="text" name="lastName" ngModel/>
      </label>

      <p>Full name: {{ fullName() }}</p>
      @if (showErrors()) {
        <p>Form contains errors</p>
      }
      <button (click)="updateFormValue()">Update form</button>
    </form>
  `,
})
export class SignalBasedFormComponent {
  signals = createFormSignals({ firstName: '', lastName: '' });
  fullName = computed(
    () => `${this.signals.value().firstName} ${this.signals.value().lastName}`
  );
  showErrors = computed(
    () => this.signals.invalid() && this.signals.dirty()
  );
  logFullname = effect(() => {
    console.log(`Full name: ${this.fullName()}`);
  });
  updateFormValue() {
    this.signals.value.set({
      firstName: 'Bob',
      lastName: 'Smith'
    });
  }
}
```

I've created a draft PR with the changes required for this new feature. The draft PR does not contain any tests, no documentation and probably has a few bugs. https://github.com/angular/angular/pull/53481

I've also published the forms module containing those changes to a temporary NPM package and included it in a stackblitz for you to play around: https://stackblitz.com/edit/stackblitz-starters-uyjg2x?file=src%2Freactive-form.component.ts,src%2Ftemplate-driven-form-signals-option.component.ts,src%2Ftemplate-driven-form.component.ts,src%2Fsignal-based-forms.component.ts

I would love to hear your feedback, thoughts and ideas!

Cheers
Gion

### Alternatives considered

n/a