Repository: angular
Issue: A proposal to improve ReactiveFormsModule
Labels: area: forms,needs: discussion

# 🚀 feature request

### Relevant Package

This Proposal is for @angular/forms

### Description

The ReactiveFormsModule is pretty good, but it has a number of problems.

1. The module is not strongly typed
   - See issues #13721 #27389 #27665 #25824 #20040 #17000 #16999 #16933 relating to controls
   - See issues #31801 #19340 #19329 relating to ControlValueAccessor
2. It’s relatively complicated to display error messages, given how fundamental this task is. See #25824 #24981 #22319 #21011 #2240 #9121 #18114
3. The methods for adding errors are inflexible. It is difficult to interface with async services to display errors (hence the need for different update strategies like on `blur` / on `submit`). In general, working with errors is more difficult than it should be.
   - See #31105 #29275 #26683 #23484 #20371 #17090 #13920 #9119 #6895 #19851 #18871 #10530 #6170.
4. Numerous annoyances with unfortunate API decisions.
   - You can't bind a single form control to multiple inputs without ControlValueAccessor #14451
   - Can't store arbitrary metadata on a control #19686
   - Calling `reset()` doesn't actually reset the control to its initial value #20214 #19747 #15741 #19251
   - Must call `markAsTouched()` / `markAsUntouched()` instead of simply `markTouched(boolean)`, which is more programmatically friendly #23414 #23336
   - Creating custom form components is relatively complex #12248
   - etc. #11447 #12715 #10468 #10195 #31133
5. In addition to all the issues dealing with errors (`#3` above), the API does not offer low level programmatic control and can be frustratingly not extensible.
   - See issues #3009 #20230 related to parsing/formatting user input
   - See issues #31046 #24444 #10887 #30610 relating to touched/dirty/etc flag changes
   - See issues #30486 #31070 #21823 relating to the lack of ng-submitted change tracking
   - Ability to remove FormGroup control without emitting event #29662
   - Ability to subscribe to FormGroup form control additions / removals #16756
   - Ability to mark ControlValueAccessor as untouched #27315
   - Provide ControlValueAccessors for libraries other than `@angular/forms` #27672

### Describe the solution you'd like

Fundamentally, the existing `AbstractControl` class does not offer the extensibility / ease of use that such an important object should have. This is a proposal to re-think the design of `AbstractControl` for inclusion in an eventual `ReactiveFormsModule2`. In general, it addresses points 1, 3, 4, and 5, above.

- Code for this proposal can be found in [this github repo](https://github.com/thefliik/reactive-forms-2-proposal).
- This proposal is demostrated in [this Stackblitz project](https://stackblitz.com/github/thefliik/reactive-forms-2-proposal).
  - The demo also contains an example compatibility directive, letting the new `AbstractControl` be used with existing angular forms components (such as Angular Material components).
- A prototype module of the proposal has been published at `reactive-forms-module2-proposal` *this is just suitable for experimentation!*
- The proposed interface is shown below.
- **The focus of this proposal is on the API of AbstractControl, not the specific implementation I've created.**

### [I wrote a blog post about this issue](https://medium.com/angular-in-depth/a-proposal-to-improve-angulars-reactiveformsmodule-323594fe2d74)

### Overview:

The new `AbstractControl` class has a `source: ControlSource<PartialControlEvent>` property which is the source of truth for all operations on the AbstractControl. The `ControlSource` is just a modified rxjs `Subject`. Internally, output from `source` is piped to the `events` observable, which performs any necessary actions to determine the new `AbstractControl` state before emitting a `ControlEvent` object. This means that subscribing to the `events` observable will get you all changes to the `AbstractControl`.

Below are a few somewhat advanced examples of the benefits / flexibility of this new API (there are [additional examples on stackblitz](https://stackblitz.com/github/thefliik/reactive-forms-2-proposal)). Because `AbstractControl` is abstract (and cannot be instantiated), these example use a simple `FormControl` object that looks like so:

```ts
class FormControl<Value = any, Data = any> implements AbstractControl<
  Value,
  Data
> {}
```

#### Example 0: the new API is similar to the old API

To begin, the new API should be very familiar to users of the old API.

```ts
const validatorFn: ValidatorFn =
  control => typeof control.value === 'string' ? null : {invalidValue: true};

const control = new FormControl('', {
  validators: validatorFn,
});

control.setValidators(null);

control.value; // ""

// get current value and also changes
control.observe('value').subscribe(value => {
  // do stuff ...
});

// just subscribe to changes
control.observeChanges('errors').subscribe(errors => {
  // do stuff ...
});

control.setValue('string');

control.touched; // false

control.markTouched(true);

// etc...
```

#### Example 1: linking one FormControl to another FormControl

Here, by subscribing the source of `controlB` to the events of `controlA`, `controlB` will reflect all changes to `controlA`.

```ts
const controlA = new FormControl();
const controlB = new FormControl();

controlA.events.subscribe(controlB.source);
```

Multiple form controls can also be linked to each other, meaning that all changes to one will be applied to the others. Because changes are keyed to source ids, this does not cause an infinite loop (as can be seen in the stackblitz example).

```ts
controlA.events.subscribe(controlB.source);
controlB.events.subscribe(controlA.source);
controlC.events.subscribe(controlA.source);
controlA.events.subscribe(controlC.source);
```

#### Example 2: subscribe to a nested property of a FormGroup

Here, we subscribe to validity changes of the `firstName` control of a nested form group. Everything is properly typed:

```ts
const formGroup = new FormGroup({
  userId: new FormControl(1),
  people: new FormArray([
    new FormGroup({
      id: new FormControl(1),
      firstName: new FormControl('John'),
    }),
  ]),
});

formGroup
  .observe('controls', 'people', 'controls', 0, 'controls', 'firstName', 'valid')
  .subscribe(valid => {
    // ... do stuff;
  })
```

Importantly, this subscription will also emit if the validity changes because a control is replaced or removed. For example, if the `FormArray` has the `FormGroup` at index `0` removed, then this subscription will emit `undefined`. If a new `FormGroup` is later added at index `0`, the subscription will emit to reflect the new `firstName` control's `valid` property.

This also allows us to subscribe to changes to the `controls `property of a form group, to be made aware of control additions / removals (or any other property of an AbstractControl).

#### Example 3: dynamically parse a control's text input

Here, a user is providing `string` date values and we want a control with javascript `Date` objects. We create two controls, one for holding the `string` values and the other for holding the `Date` values and we sync all changes between them. However, value changes from one to the other are transformed to be in the appropriate format.

As with the other examples, [demoing this on stackblitz](https://stackblitz.com/github/thefliik/reactive-forms-2-proposal?file=src%2Fapp%2Fexamples%2F03-example-three%2Fexample-three.component.ts) might be helpful.

```ts
declare const stringDateValidator: ValidatorFn;
declare const stringToDate: (value: string) => Date | null;
declare const dateToString: (value: Date | null) => string;

class ExampleThreeComponent implements OnInit {
  inputControl = new FormControl('', {
    validators: stringDateValidator,
  });

  dateControl = new FormControl<Date | null>(null);

  ngOnInit() {
    this.inputControl.events
      .pipe(
        map(event => {
          if (event.type === 'StateChange' && event.changes.has('value')) {
            const changes = new Map(event.changes);

            changes.set('value', stringToDate(changes.get('value'));

            return {
              ...event,
              changes,
            };
          }

          return event;
        }),
      )
      .subscribe(this.dateControl.source);

    this.dateControl.events
      .pipe(
        map(event => {
          if (event.type === 'StateChange' && event.changes.has('value')) {
            const changes = new Map(event.changes);

            changes.set('value', dateToString(changes.get('value'));

            return {
              ...event,
              changes,
            };
          }

          return event;
        }),
      )
      .subscribe(this.inputControl.source);
  }
}
```

To make things easier, the `FormControlDirective` / `FormControlNameDirective` / etc directives allow users to inject a `ValueMapper` object. This value mapper has `toControl` and `toAccessor` transform functions which will transform the control and input values, respectively. Optionally, you can also provide a `accessorValidator` function which validates the input values before they are transformed.

Usage is like:

```html
      <input
        [formControl]="controlA"
        [formControlValueMapper]="{
          toControl: stringToDate,
          toAccessor: dateToString,
          accessorValidator: dateValidatorFn
        }"
      />
```

This hypothetical example transforms input string values into javascript `Date` objects (this can also be seen on stackblitz in `example-three`).

#### Example 4: validating the value of an AbstractControl via a service

Here, a `usernameControl` is receiving text value from a user and we want to validate that with an external service (e.g. "does the username already exist?").

```ts
const usernameControl = new FormControl();

// here we want to always receive value updates, even if they were made with "noEmit"
usernameControl.validationEvents
  .pipe(
    filter(event => event.label === "End"),
    tap(() => this.usernameControl.markPending(true, { source: 'userService'})),
    debounceTime(500),
    switchMap(event => this.userService.doesNameExist(event.controlValue)),
    tap(() => this.usernameControl.markPending(false, { source: 'userService'})),
  )
  .subscribe(response => {
        const errors = response.payload ? { userNameExists: true } : null;

        this.usernameControl.setErrors(errors, {
          source: 'userService',
        });
      });
```

Some things to note in this example:

1. The API allows users to associate a call to `markPending()` with a specific key (in this case "userService"). This way, calling `markPending(false)` elsewhere (e.g. a different service validation call) will not prematurely mark _this_ service call as "no longer pending". The AbstractControl is pending so long as any `key` is true.
2. Internally, errors are stored associated with a source. In this case, the source is `'userService'`. If this service adds an error, but another service later says there are no errors, that service will not accidentally overwrite this service's error.
   1. Importantly, the `errors` property combines all errors into one object.

#### Example 5: using dependency injection to dynamically add new validator functions to a control

In the _existing_ `ReactiveFormsModule`, when you pass a control to a `FormControlDirective` via `[formControl]`, that directive may dynamically add validator functions to the control. It does this by creating a new validator function which combines the control's existing validator function(s) with any additional validator functions the `FormControlDirective` has had injected. It then replaces the control's existing validator function with the new one. This process is complex and can lead to bugs. For example, after this process is complete there isn't any way to determine which validator functions were added by the user vs which ones were added dynamically.

Here, validators are internally stored keyed to a source id (similar to errors). If a FormControl is passed to a directive which dynamically injects additional validator functions, those functions will be stored separately from the FormControl's other functions (and are deleted separately). This leads to more consistent, predictable behavior that an unknowledgeable user cannot mess with.

```ts
@Directive({
  selector: 'myControlDirective',
})
class MyControlDirective {
  static id = 0;

  @Input('myControlDirective') control: AbstractControl;

  private id = Symbol(`myControlDirective ${MyControlDirective.id}`);

  constructor(
    @Optional()
    @Self()
    @Inject(NG_VALIDATORS_2)
    private validators: ValidatorFn[] | null,
  ) {
    MyControlDirective.id++;
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.control.previousValue) {
      // clear injected validators from the old control
      const oldControl = changes.control.previousValue;

      oldControl.setValidators(null, {
        source: this.id,
      });
    }

    // add injected validators to the new control
    this.control.setValidators(this.validators, {
      source: this.id,
    });
  }
}
```

### The interface

```ts
interface AbstractControl<Value = any, Data = any> {
  /**
   * The ID is used to determine where StateChanges originated,
   * and to ensure that a given AbstractControl only processes
   * values one time.
   */
  readonly id: ControlId;

  data: Data;

  /**
   * **Warning!** Do not use this property unless you know what you are doing.
   *
   * A control's `source` is the source of truth for the control. Events emitted
   * by the source are used to update the control's values. By passing events to
   * this control's source, you can programmatically control every aspect of
   * of this control.
   *
   * Never subscribe to the source directly. If you want to receive events for
   * this control, subscribe to the `events` observable.
   */
  source: ControlSource<PartialControlEvent>;

  /** An observable of all events for this AbstractControl */
  events: Observable<ControlEvent & { [key: string]: any }>;

  readonly value: DeepReadonly<Value>;

  readonly errors: ValidationErrors | null;

  /**
   * A map of validation errors keyed to the source which added them.
   */
  readonly errorsStore: ReadonlyMap<ControlId, ValidationErrors>;

  readonly disabled: boolean;
  readonly enabled: boolean;
  readonly valid: boolean;
  readonly invalid: boolean;
  readonly pending: boolean;

  /**
   * A map of pending states keyed to the source which added them.
   * So long as there are any `true` boolean values, this control's
   * `pending` property will be `true`.
   */
  readonly pendingStore: ReadonlyMap<ControlId, true>;

  readonly status: 'DISABLED' | 'PENDING' | 'VALID' | 'INVALID';

  /**
   * focusChanges allows consumers to be notified when this
   * form control should be focused or blurred.
   */
  focusChanges: Observable<boolean>;

  /**
   * These are special, internal events which signal when this control is
   * starting or finishing validation.
   *
   * These events are not emitted from the `events` observable.
   */
  validationEvents: Observable<ValidationEvent>;

  readonly readonly: boolean;
  readonly submitted: boolean;
  readonly touched: boolean;
  readonly changed: boolean;
  readonly dirty: boolean;

  /**
   * A map of ValidatorFn keyed to the source which added them.
   *
   * In general, users won't need to access this. But it is exposed for
   * advanced usage.
   */
  readonly validatorStore: ReadonlyMap<ControlId, ValidatorFn>;

  /**
   * ***Advanced API***
   *
   * The "atomic" map is used by controls + parent ControlContainers to ensure
   * that parent/child state changes happen atomically before any events are
   * emitted.
   */
  readonly atomic: Map<ControlId, (event: ControlEvent) => (() => void) | null>;

  [AbstractControl.ABSTRACT_CONTROL_INTERFACE](): this;

  observeChanges<T = any>(
    props: string[],
    options?: { ignoreNoEmit?: boolean },
  ): Observable<T>;
  observeChanges<A extends keyof this>(
    a: A,
    options?: { ignoreNoEmit?: boolean },
  ): Observable<this[A]>;
  observeChanges<A extends keyof this, B extends keyof this[A]>(
    a: A,
    b: B,
    options?: { ignoreNoEmit?: boolean },
  ): Observable<this[A][B] | undefined>;


  observe<T = any>(
    props: string[],
    options?: { ignoreNoEmit?: boolean },
  ): Observable<T>;
  observe<A extends keyof this>(
    a: A,
    options?: { ignoreNoEmit?: boolean },
  ): Observable<this[A]>;
  observe<A extends keyof this, B extends keyof this[A]>(
    a: A,
    b: B,
    options?: { ignoreNoEmit?: boolean },
  ): Observable<this[A][B] | undefined>;

  equalValue(value: Value): value is Value;

  setValue(value: Value, options?: ControlEventOptions): void;

  patchValue(value: any, options?: ControlEventOptions): void;

  /**
   * If provided a `ValidationErrors` object or `null`, replaces the errors
   * associated with the source ID.
   *
   * If provided a `Map` object containing `ValidationErrors` keyed to source IDs,
   * uses it to replace the `errorsStore` associated with this control.
   */
  setErrors(
    value: ValidationErrors | null | ReadonlyMap<ControlId, ValidationErrors>,
    options?: ControlEventOptions,
  ): void;

  /**
   * If provided a `ValidationErrors` object, that object is merged with the
   * existing errors associated with the source ID. If the error object has
   * properties containing `null`, errors associated with those keys are deleted
   * from the `errorsStore`.
   *
   * If provided a `Map` object containing `ValidationErrors` keyed to source IDs,
   * that object is merged with the existing `errorsStore`.
   */
  patchErrors(
    value: ValidationErrors | ReadonlyMap<ControlId, ValidationErrors>,
    options?: ControlEventOptions,
  ): void;

  markTouched(value: boolean, options?: ControlEventOptions): void;

  markChanged(value: boolean, options?: ControlEventOptions): void;

  markReadonly(value: boolean, options?: ControlEventOptions): void;

  markSubmitted(value: boolean, options?: ControlEventOptions): void;

  markPending(
    value: boolean | ReadonlyMap<ControlId, true>,
    options?: ControlEventOptions,
  ): void;

  markDisabled(value: boolean, options?: ControlEventOptions): void;

  focus(value?: boolean, options?: ControlEventOptions): void;

  setValidators(
    value:
      | ValidatorFn
      | ValidatorFn[]
      | null
      | ReadonlyMap<ControlId, ValidatorFn>,
    options?: ControlEventOptions,
  ): void;

  /**
   * Returns an observable of this control's state in the form of
   * StateChange objects which can be used to make another control
   * identical to this one. This observable will complete upon
   * replaying the necessary state changes.
   */
  replayState(options?: ControlEventOptions): Observable<ControlEvent>;

  /**
   * A convenience method for emitting an arbitrary control event.
   */
  emitEvent<
    T extends PartialControlEvent = PartialControlEvent & { [key: string]: any }
  >(
    event: Partial<
      Pick<T, 'id' | 'meta' | 'source' | 'processed' | 'noEmit' | 'meta'>
    > &
      Omit<T, 'id' | 'meta' | 'source' | 'processed' | 'noEmit' | 'meta'> & {
        type: string;
      },
  ): void;
}

interface PartialControlEvent {
  id?: string;
  source: ControlId;
  readonly processed: ControlId[];
  type: string;
  meta?: { [key: string]: any };
  noEmit?: boolean;
}

interface ControlEvent extends PartialControlEvent {
  id: string;
  meta: { [key: string]: any };
}
```

### Wrapping up

There's a lot packed in to this API update. For a full overview, you should [check out the repo](https://github.com/thefliik/reactive-forms-2-proposal).

Two other details to note:

1. When you pass the `noEmit` option to a function, that squelches emissions from any `observe` and `observeChanges` observables, but it does not effect the `events` observable. This is a good thing. It means that library authors can hook into the pure stream of control events on an AbstractControl and choose to honor or ignore `noEmit` as appropriate (via an observable operator like `filter()`).
2. All methods that will emit offer a `meta` option that accepts an arbitrary metadata object that will be included in the control event object. This greatly increases customizability / extensibility, as you can attach custom information to any action and access that custom information on the ControlEvent objects.

### Things not included in this proposal

#### Validation

A lot of the issues with the current FormControl API are ultimately issues with the current `ValidatorFn` / `ValidationErrors` API.

Examples include:

1. If a control is required, a `[required]` attribute is not automatically added to the appropriate element in the DOM.
   1. Similarly, other validators should also include DOM changes (e.g. a maxLength validator should add a `[maxlength]` attribute for accessibility, there are ARIA attributes which should be added for accessibility, etc).
   2. If you validate to make sure an input is a `number`, it's appropriate to add a `type="number"` attribute on the underlying `<input>`.
2. Generating and displaying error messages is much harder than it should be, for such a fundamental part a Forms API.

Ultimately, I see these as failings of the current `ValidatorFn` / `ValidationErrors` API, and should be addressed in a fix to that API. Any such fix should be included in any `ReactiveFormsModule2`, but they should be discussed in a separate issue.

#### ControlValueAccessor

This proposal does not touch ControlValueAccessor and this proposal works with the existing ControlValueAccessor API. This decision was again made to focus discussion on AbstractControl.

This being said, this API allows for the `ControlValueAccessor` interface to be changed to simply:

```ts
interface ControlAccessor<T extends AbstractControl = AbstractControl> {
  control: T;
}
```

_You can see an example of this [in the repo](https://github.com/thefliik/reactive-forms-2-proposal)._

**I mention this possible change to ControlValueAccessor mainly as a way of highlighting how flexible/powerful this new AbstractControl API is.** With this update to ControlValueAccessor, the `control` property of a directive contains an `AbstractControl` representing the form state of the directive (as a reminder, components are directives).

Broadly speaking, this ControlAccessor API has several advantages over the current ControlValueAccessor API:

1. Easier to implement
    - When the form is touched, mark the control as touched.
    - When the form value is updated, `setValue` on the control.
    - etc
2. Easier to conceptualize (admittedly subjective)
3. Allows a `ControlValueAccessor` to represent a `FormGroup` / `FormArray` / etc, rather than just a `FormControl`.
   - A ControlValueAccessor can represent an `address` using a FormGroup.
   - A ControlValueAccessor can represent  `people` using a FormArray.
    - etc
4. Very flexible
   - You can pass metadata tied to changes to the ControlValueAccessor via the `meta` option found on the new `AbstractControl`.
   - You can create custom `ControlEvent` events for a `ControlValueAccessor`.
   - If appropriate, you can access the current form state of a `ControlValueAccessor` via a standard interface (and you can use the `replayState()` method to apply that state to another AbstractControl)
   - If appropriate, a `ControlValueAccessor` could make use of a custom object extending `AbstractControl`.

In terms of specifics, this `ControlValueAccessor` change is made possible because this new API allows you to make two form controls identical (via `replayState()`) and to link two form controls so they maintain identical states. This means that, in cases where a directive/component is receiving an AbstractControl as some sort of input, you can easily create a readonly reference control which represents the current input control's state.

For example, where before in ngOnChanges you might have this (and consumers of `MyComponent` would need to deal with changes to `control`):

```ts
export class MyComponent {
  @Input('providedControl') control: FormControl;
}
```

With the new API you can have:

```ts
export class MyComponent {
  @Input() providedControl: FormControl;

  readonly control = new FormControl();

  ngOnChanges() {
    this.subscriptions.forEach(s => s.unsubscribe());
    this.subscriptions = [];

    this.subscriptions.push(
      concat(
        this.providedControl.replayState(),
        this.providedControl.events,
      ).subscribe(this.control.source),
      this.control.events.subscribe(this.providedControl.source);
    )
  }
}
```

Notice that this version insulates consumers of your directive/component from changes to the `providedControl`, allowing them to interface with a static (readonly) reference control.

While this might seem like a small difference, the ability to have static references to controls ends up being a pretty big deal in terms of user-friendliness. For example, if you inject `NgFormControlDirective` into your component, you don't need to deal with changes to the `formControl` property of the `NgFormControlDirective`. You can simply subscribe to `NgFormControlDirective#control` and know that that control will always have the same state as `NgFormControlDirective#formControl`.

There is a [blog post](https://medium.com/angular-in-depth/a-proposal-to-improve-angulars-reactiveformsmodule-323594fe2d74) which goes into greater detail showing how this updated `ControlAccessor` would work. 

### Describe alternatives you've considered

While fixing the existing `ReactiveFormsModule` is a possibility, it would involve many breaking changes. As `Renderer` -> `Renderer2` has shown, a more user friendly solution is to create a new `ReactiveFormsModule2` module, depricate the old module, and provide a compatibility layer to allow usage of the two side-by-side.