Try   HackMD

Design Doc - Angular Reactive Forms

author: Angular Community
created: 2020-05-08
status: [draft | in review | approved | implemented | obsolete]
doc url: https://hackmd.io/HHodz9FQR6mGCZxRmEimsg?both
discord discussion: https://discord.gg/gCEj4zC in #forms channel
feedback url:
design doc template version: 1.0.0

Objective

This document outlines requirements, and goals for building a new reactive forms API for Angular. A few key goals are:

  • Type-safe

Angular is built on top of TypeScript to provide type-safe APIs. We want to encourage usage of type-safety when consuming these forms, and provide that type-safety throughout the API.

  • Composeable

Simple use cases for forms will be covered, but we also want to cover complex use cases, such as:

  • Nested validation
  • Nested forms
  • Complex async validation

Minimal but extensible

Relying on built-in Web APIs would be preferred where possible.

Compatible with native Angular form controls and validators (NG_VALUE_ACCESSOR and NG_VALIDATORS).

Property-renaming-safe - does not need to access controls by string (e.g. formControlName='prop').

This design doc serves to gather feedback, knowledge about existing APIs, and guidance for the proposed new APIs.

Approvals

Expected User/Developer Experience

Background

[Zack DeRose] I've spoken in the past about my initial experience learning RxJS, which revolved around heavy use of BehaviorSubject's, and specifically avoiding operators by calling next() on a given BehaviorSubject within the subscription of some other BehaviorSubject.

This mixture of imperative and declarative styles lead down a dark path. Even without much complexity in the use case, using this BehaviorSubject approach is analygous to assembling a Rune-Goldberg machine. Making changes or fixes to such machine requires a heavy cognitive load on the developer, and simulatenously, the developer also loses the fine grain control over Observable streams that is normally available when using RxJS.

I see the majority of the issues with @angular/form's, is that in it's current state, a developer has no choice but to mix imperative (calling setValue() on a FormControl, implementing writeValue() in the ControlValueAccessor) with declarative when implementing reactive behavior. It's essentially the same as my above BehaviorSubject example, but with no alternative available.

I believe that rectifying this situation should be our paramount goal in @ngrx/forms.

Prior Art

Current Angular Forms libraries

Issues:

Libraries:

ngneat Forms Manager - https://github.com/ngneat/forms-manager
ngx-sub-form - https://github.com/cloudnc/ngx-sub-form
ngObservableForm - https://github.com/SanderElias/ngObservableForm
rxweb/types(Strongly Typed Reactive Form) - https://github.com/rxweb/rxweb/tree/master/client-side/angular/packages/types
Formly - https://formly.dev/
Forms Typed - https://github.com/gparlakov/forms-typed/blob/master/projects/forms/README.md

Prototype

Detailed Design

[[ I imagine this hardly qualifies as fully-detailed ]]

The core of my concept revolves around the concept of a private Observable, for now we'll call it _domValue$: Observable<T>. This property is private because due to the nature of forms, our developers don't have complete control of the domValue (as by intent, control is ceded to our users).

Rather, a developer would supply a valueOperator: Observable<T> => Observable<T>. This operator would allow the developer to define any 'developer controled' dynamic/reactive/programmatic adjustments to the value of the form, based on the _domValue$ of the form, as well as any other Observable in scope that should externally cause the form in question to adjust its value.

Additionally, a developer would supply a validationOperator: Observable<T> => Observable<ValidationErrors | null>, which would operate much like the value operator, but with Validity.

Publically, our @ngrx/forms API would then expose a readonly value$: Observable<T>, as well as a readonly errors$: Observable<ValidationErrors | null>. These would both be created by applying their respective developer-defined operators to the source _domValue$.

(Similar approaches could be used for readonly touched$: Observable<void>, readonly enabled$: Observable<'enabled' | 'disabled'>, readonly submit$: Observable<T>, and readonly blur$: Observable<void>)

Compatability with current reactive forms API

[Jan-Niklas Wortmann] I think creating an API that is compatabile with the current reactive forms is key to the success of this module. Otherwise other third party libraries (e.g. @angular/material), but also existing CustomControls will be pain to use. On top of that additional migration effort would prevent many projects to move this direction.

To establish an API that is compatible with the current reactive forms API I see two approaches.

  1. Extending the key entities by inheritance
  2. Wrap the key entities with a service
Extending the key entities by inheritance

Technically we could extend the key entities of the reactive forms API.

export class ReactiveFormControl<T> extends FormControl {}

This way we could support all the existing features in a compatible way. There could be some drawbacks about the strict template type checking.

Benefits
  • compatibility
  • new features will work more or less out of the box
  • fairly easy migration (could be provided with factory methods and schematics)
  • easy to determine boundaries
Drawbacks
  • tight coupling (might break in upcoming versions)

Wrap the key entities with a service

I think this is the approach the ngneat forms manager took. By creating a service that wraps the forms API we could extend type safety, but also generate formControls, etc. to establish a compatible way of working with existing API.

Benefits
  • Composition over inheritance (lose coupling)
  • easier to test (DI vs. classes and POJOs)
Drawbacks
  • no easy migration
  • sounds like more effort
  • features added to the reactive forms API might need to be added manually to the service too

Registration of Controls

Validation/Error Handling

See validityOperator and errors$ described above.

Testing

I envision the unit testing would look like controlling/defining the _domValue$ (to model a user's various potential behavior), as well as mocking or defining any other streams the developer would expect to have an affect, and using marble-diagrams/the test scheduler to assert that the public observables (value$/errors$/touched$/enabled$) exposed are as expected.

Caveats

Performance Considerations

Documentation Plan

Style Guide Impact

N/A

Developer Impact

Public API Surface Impact

Developer Experience Impact

Breaking Changes

N/A

Rollout Plan

  • Integration with existing APIs?
  • Angular Material integration?

Rollback Plan

N/A

Maintenance Plan

TBD