Repository: angular
Issue: Enhancements to forms for future signal forms
Labels: area: forms

### Which @angular/* package(s) are relevant/related to the feature request?

forms

### Description

Template and reactive forms in Angular are great, yet using them through the years has revealed a few areas of improvement. This issue aims to provide feedback on some problems, hoping they can be addressed in the future re-vamped signal forms. I'd be happy to elaborate and provide further examples of any of the points below.

## Problem 1: Control Value Accessor is not strongly typed

CVA provide a nice separation of concerns and layer of abstraction between the form control and the DOM.

Nevertheless, form controls, either the reactive or template flavors, access the CVA through DI. This means that the binding is not strictly typed. A CVA can be designed to handle numbers, yet be used in a model that has the string value.

Additionally, the CVA interface builds on top of `any`, which offers a bad DX.

**Example:**
```ts
class NumberValueAccessor implements ControlValueAccessor {
  writeValue(value: number): void { /* ... */ }
  registerOnChange(fn: (value: number) => void): void { /* ... */ }
  // ...
}

// Usage in a form control that expects a string
this.form = new FormGroup({
  age: new FormControl('25') // This should be a number
});
```

## Problem 2: Control Value Accessor can cause issues with the timing of its binding

The timing when `writeValue` and `registerOnChange` are called is somehow part of its public API, following Hyrum's Law. It can cause some issues, for example, if the CVA supports multiple types and does a parsing on `writeValue`. The first time it get's called, `registerOnChange` hasn't been called yet, so the parsed value can't be immediately propagated.

Additionally, this API feels verbose and not declarative when compared with more recent framework features.

**Example:**
```ts
class MultiTypeValueAccessor implements ControlValueAccessor {
  writeValue(value: string | number): void {
    const parsedValue = typeof value === 'string' ? parseInt(value, 10) : value;
    // registerOnChange hasn't been called yet, so parsedValue can't be propagated
  }
  registerOnChange(fn: (value: number) => void): void { /* ... */ }
  // ...
}
```

## Problem 3: Strict typing of form controls and groups

When typed forms were introduced, they had to be retro-compatible with existing implementations, which we appreciated. Nevertheless, it is painful to work with the `undefined` type of controls just because they can be disabled. I'd argue that this decision has caused typing issues in many cases, since the form value when is valid still has `undefined` all around. True, the raw value can be used, but feels unnecessary.

Additionally, there is a common case where a field uses the required validator, but it's underlying type also supports `null`. If the form is valid, the control would have a non-null value, yet the type still includes `null`.

**Example:**
```ts
const form = new FormGroup({
  name: new FormControl<string | null>(null, Validators.required)
});

// When the form is valid, name should not be null, but the type still includes null
const name: string | null = form.get('name')?.value;
```

Finally, there are some controls that do parsing, such as supporting both `Date | string` as inputs, but always exposing a `Date`. Again, it can't be expressed with current typing, there is no transformation function or type narrowing.

**Example:**
```ts
const dateControl = new FormControl<Date | string>('2025-01-01');

// The control should always expose a Date, but it can accept a string
const date: Date = dateControl.value; // This can't be done
```

## Problem 4: Error fields does not have strict typing

Regardless of the return type of a validator, the `errors` property of controls is typed as `any`. For example, the max length validator exposes an error value similar to `{requiredLength: 5, actualLength: 7}`, but it can be consumed with type safety in the template.

**Example:**
```ts
const control = new FormControl('test', Validators.maxLength(5));
const errors = control.errors; // Typed as any, not safe to access properties
if (errors?.maxlength) {
  console.log(errors.maxlength.requiredLength); // No type safety
}
```

## Problem 5: Accessibility with reactive form validators

Reactive form validators provide a great tooling to build dynamic validators. However, they don't add HTML attributes such as `required` or `maxlength`, lacking a11n support. Only template driven validators do this. For those who embrace the virtues of reactive forms, validators have to be duplicated in the control and template to ensure good HTML attributes.

**Example:**
```ts
const control = new FormControl('', [Validators.required, Validators.maxLength(5)]);
// HTML does not reflect required or maxlength attributes
```

## Problem 6: Async validators are still pending when the form is submitted

One of the most common scenarios when using forms is acting when they are submitted. Currently, when the submit event is fired, a form with async validators is in pending state. It is not trivial to wait until they complete so as to decide whether the form action should be executed.

**Example:**
```ts
this.formGroup = new FormGroup({
  asyncControl: new FormControl('', null, asyncValidator)
});

onSubmit() {
  if (this.formGroup.pending) {
    // Form is still pending, can't proceed with submission
    console.log('Form is pending, please wait...');
  } else if (this.formGroup.valid) {
    // Form is valid, proceed with submission
    console.log('Form submitted successfully:', this.formGroup.value);
  } else {
    // Form is invalid, handle errors
    console.log('Form is invalid:', this.formGroup.errors);
  }
}
```

## Problem 7: Complex typing structures

Typed form's typing is complex. For example, a form group can't be typed with just the type of its value, it needs to know whether each field is a control, group, or array.

### Proposed solution

## Proposed solution 1: Bind CVA through inputs to ensure strict typing

Ideally, a CVA could expose a `model` consumed through two-way data binding rather than through DI. Since inputs are the only binding that enforces type safety, it would ensure that the CVA and its model value have the same type.

**Proposal:**

```ts
export interface ControlValueAccessor<T> {
  readonly model: `ModelSignal<T>`;
}
```

## Proposed solution 2: Use `model` for binding of the CVA

Having two methods for the data binding of a CVA could be better replaced with the new `model` that solves this specific problem and avoids timing issues.

## Proposed solution 3: Support type narrowing

Reconsider the decision that  disabled controls should have `undefined` value and avoid the need of exposing `value` vs `rawValue`. If a control was undefined, it would be an application decision whether the value should be `undefined`. Another alternative to solve the typing issue would be only support disable on controls that include the type `undefined`.

If type narrowing could be applied to form controls, we could benefit, for example, from:

* Required validator would ensure that, if successful, the value is not null and reflect that in the type.
* A control that does parsing would support multiple values when "setting", but a narrowed type when "getting".

**Proposal:**
```ts
export function RequiredValidator<T>(control: AbstractControl<T | undefined>): control is AbstractControl<T>;
```

## Proposed solution 4: Support generic type for errors

If a control is defined with validators, make them part of the generic type so that `errors` has a strict type.

## Proposed solution 5: Enhance API of programmatic validators

A programmatic validator would have an API similar to `host` so that it can modify the HTML and add a11y attributes as needed.

## Proposed solution 6: Provide `success` and `error` events on forms

Rather than relying on submit, provide `success` (or `validSubmit`) or `error` (or `invalidSubmit`) events that get triggered when, after the form is submitted, async validators are resolved, and the state is no longer pending, depending on whether it is `valid` or `invalid`.

The success event could also expose a narrowed type with the final types when validators are successful, following up on proposed solution 3.

## Proposed solution 7: Simplify typing for form groups/arrays

A form group/array would only have child abstract controls, regardless of their specific type. This way they can be typed with the value's type. If it had a child object-shaped field, it could later be bound to a control with an object value, or a child group dynamically.

### Alternatives considered

Creating my own forms library, but I really like Angular's 😉