Getting Naked with Angular Reactive Forms

How do Reactive Forms work under the hood?

It is time for another deep dive into Angular dear reader. In this article we are going to look at Angular’s Reactive Forms library under the hood, to see how it models forms and GUI elements.

I thought I would take a slightly different approach and poke around in the Angular source code to try to understand how the form controls and directives work, as there are plenty of tutorials on Reactive forms already available online, including the excellent tutorial in the Angular docs.

Let’s start by looking at the @angular/forms library on github.com. Open it up now or you won’t be able to follow along and make the most of this guide.

What exactly are Reactive forms?

If you take a look in the Angular 8 source code, you will find a directory for @angular/forms in the packages/forms/src folder. In order to understand the awesomeness of Reactive forms we will have to put on our detective glasses and look for clues as to how the library works.

A Reactive Form is a fancy way for saying a model driven form, which has been designed using the principles of functional reactive programming. Angular’s Reactive Forms library provides a series of classes, components and directives that use Observables under the hood to provide a way of manipulating forms in a more Reactive style.

A more simple approach to forms is to use the Template driven forms. Template driven forms are designed for very simple use cases. Template driven forms are more lightweight and easy to use, but lack a lot of the power of Reactive Forms.

// Example Template driven form using ngForm and ngModel
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
<div class="form-group">
<input [(ngModel)]="currentHero.name">

You can add attributes like ngForm and ngModelto your html inputs and Angular’s magic directives will automatically utilise two-way binding to make the form data available to your code. Changes to currentHero.name in your code will be automatically reflected in the view, and your code will always have the latest value of the form component.

One-way or Two-way binding…

As a brief aside on the subject of binding, did you know that you can actually work out which type of binding Angular is using by looking at how the property attributes are structured?

// One-way binding from data source to view target
[target]="expression"
bind-target="expression"
// One-way binding from view target to data source (i.e. events)(target)="statement"
on-target="statement"
// Two-way binding
[(target)]="expression"
bindon-target="expression"

To see if something has two-way binding, look for a bannana in a box! e.g. [(ngModel)] Both have their uses. Two-way binding is simple and easy to use, but one-way binding tends to be easier to reason with in larger applications, has fewer side effects and scales more easily. Facebook’s React library doesn’t support two-way binding for a reason!

Template Driven vs Reactive…

Whilst at first glance template forms seem handy, they offer some issues when scaling up an application. Firstly, it requires some processor and memory resources to keep track of each control. If you have lots of controls in your app then this is going to end up slowing down your application.

The second issue is that the two-way bound data is mutable, which essentially means that you cannot guarantee that the data isn’t going to be changed whilst you are working with it. We will never be sure if what we can see is accurate or if it will be changed whilst we are working with the data due to the asynchronous nature of the two-way binding. Template driven forms also only support form validation via directives and not functions, making them less flexible.

Reactive forms on the other hand don’t have this problem, they are synchronous, immutable, support structured data models, validation via functions and low-level control of all form components.

In Angular there are three key building blocks for defining forms:

  • FormGroup
  • FormControl
  • FormArray

All of which are used by both Reactive and the Template driven forms.

AbstractControl

AbstractControl is defined in models.ts and is the base class for FormControl, FormGroup and FormArray. Angular docs provide some more information:

* It provides some of the shared behavior that all controls and groups of controls have, like running validators, calculating status, and resetting state. It also defines the properties that are shared between all sub-classes, like `value`, `valid`, and `dirty`. It shouldn’t be instantiated directly.

The AbstractControl class is quite large, so we will take a look through it to see the interesting bits. Here is the parent() method:

get parent(): FormGroup|FormArray { return this._parent; }

Cool, we now know that any AbstractControl potentially has a parent which is either a FormGroup or FormArray. This also means that both FormGroup’s and FormArray’s can be nested. FormControl’s however cannot, which makes sense.

Next up we have the status of the control, which is a simple string showing the validation status:

  • VALID: This control has passed all validation checks.
  • INVALID: This control has failed at least one validation check.
  • PENDING: This control is in the midst of conducting a validation check.
  • DISABLED: This control is exempt from validation checks.

This property allows any child classes of AbstractControl to easily check the validation status:

get valid(): boolean { return this.status === VALID; }
get invalid(): boolean { return this.status === INVALID; }

There is a pristine property too, which lets us know if the user has yet to change the value of the control. A control changed by a filthy human will also become dirty, which we can check with dirty().

public readonly pristine: boolean = true;
get dirty(): boolean { return !this.pristine; }

Similarly, we also have a touched property, which is if the user has triggered the blur event on it.

public readonly touched: boolean = false;

Now this is where it gets interesting, we have an Observable property called valueChanges that emits events every time the value of the control changes in the UI or programatically.

public readonly valueChanges : Observable<any>;

We also have another observable that emits statusChanges:

public readonly statusChanges : Observable<any>;

updateOn() returns the formHook that this control is currently set to update on. This allows the developer to specify when the control should update its parent FormArray or FormGroup that its value has been changed. By default, the control will update its parent form each time the control is changed, but we might prefer it to update on blur, when the user exits the control, or only when the final submit button is pressed. Possible values for _updateOn are: change (default) | blur | submit

get updateOn(): FormHooks {    
return this._updateOn ? this._updateOn : (this.parent ? this.parent.updateOn : 'change');
}

setValue() allows the developer to set the value of the control which seems straight forward and reset() resets the control. patchValue() however enables the developer to patch the value of the control. Huh?

abstract setValue(value: any, options?: Object): void;
abstract reset(value?: any, options?: Object): void;
abstract patchValue(value: any, options?: Object): void;

Well it turns out that both patchValue and setValue can be used to change the control’s value. Sami C. explains the difference simply in his medium article setValue vs patchValue, and Todd Motto dives deep into the API far better than I can in his tutorial, but the gist is this:

patchValue() allows us to selectively set the value of some controls in a FormGroup or FormArray. setValue requires that all of the controls are set.

Finally, updateValueAndValidity() can be used to recalculate the value and validation status. It will also update the value of its ancestors by default in the event that some higher up logic might dictate if the whole form is valid.

updateValueAndValidity(
opts: {
onlySelf?: boolean,
emitEvent?: boolean
} = {}): void {
this._setInitialStatus();
this._updateValue();
// If the control is enabled
if (this.enabled) {
// Calls unsubscribe on any asyncValidation Subscriptions
this._cancelExistingSubscription();

// runs the validator and assigns any errors to this.errors
(this as{errors: ValidationErrors | null}).errors = this._runValidator();
// Update's the control's current status (post validation check)
(this as{status: string}).status = this._calculateStatus();
// If the control is valid or still pending, it runs the asynchronous validation checks
if (this.status === VALID || this.status === PENDING) {
this._runAsyncValidator(opts.emitEvent); }
}

// Emit the new value and status as events
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
// If the control has a FormArray or FormGroup parent, call its validation function
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
}
}

We have learned so far that this AbstractControl class is quite important as it provides some of the default behaviour and properties for any FormControl, FormGroup or FormArray, all of which extend AbstractControl. We have skipped over validation for now, but we should get a sense that the same methods and properties are shared across these three important classes and whilst their implementations might differ, they all have this uniform interface thanks to AbstractControl.

AbstractControl also has an interface called AbstractControlOptions that provides more specific configuration data for the AbstractControl. This can be passed into any FormControl, FormGroup or FormArray constructor to define specific validators and updateOn parameters.

export interface AbstractControlOptions {  
// The list of validators applied to a control.
validators?: ValidatorFn|ValidatorFn[]|null;

// The list of async validators applied to control.
asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null;

// The event name for control to update upon.
updateOn?: 'change'|'blur'|'submit';
}

You might have also noticed that there is no reference to the DOM in any of the above. This is because AbstractControl, FormControl,FormGroup and FormArray are all models, or ‘virtualform components — hence why they are found in models.ts. By abstracting forms and controls and away from the DOM, we can start to add some useful functionality such as validators and provide more fine grained control over the form. Angular Directives will eventually take care of joining our models with our DOM components, as we will see later.

FormControl

FormControl is our virtual model of our Form component. Its main job is to track the validation status of an individual form control, and it gets most of its basic functionality by extending AbstractControl. We can use a FormControl in a few different ways by passing a value for the control, and optionally we can also pass an options configuration object for more fine grained control:

const control = new FormControl('some value'); console.log(control.value);     // 'some value'

const control = new FormControl({ value: 'n/a', disabled: true });
console.log(control.value); // 'n/a' console.log(control.status); // 'DISABLED'
const control = new FormControl('', {
updateOn: 'blur',
validators: Validators.required,
asyncValidators: myAsyncValidator
});

Let’s take a look at the constructor:

// FormControl constructor
onstructor(
// The form state or value
formState: any = null,
// A synchronous validator or Options object
validatorOrOpts?: ValidatorFn|ValidatorFn[]| AbstractControlOptions|null,

// An asynchronous validator
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null
)
{
// Call AbstractComponent constructor with validators
super(
coerceToValidator(validatorOrOpts),
coerceToAsyncValidator(asyncValidator, validatorOrOpts)
);
// Apply the value by calling _applyFormState(formState)
this._applyFormState(formState);

// Pass in the options configuration obj and set update strategy
this._setUpdateStrategy(validatorOrOpts);

// update the value and check the validity
this.updateValueAndValidity({onlySelf: true, emitEvent: false});

// Initialize our observables
this._initObservables();
}

formState Initializes the control with an initial value, or an object that defines the initial value and disabled state. The second parameter takes either a full AbstractControlOptions object or a synchronous validator, and the final property, asyncValidator, defines any asynchronous validators.

We can see that the formState parameter gets passed along to this._applyFormState(formState) , a private function of the FormControl.

_applyFormState() checks to see if formState is a simple or boxed value, i.e. an object like{value: 'test', disabled: 'false'} and then sets the this.value and this._pendingValue to the new formState.value.

Remember in AbstractControl we had a SetValue() function? Lets see how this is defined in the FormControl component. I have added comments to the lines below to explain what each is doing:

Cool, so it appears that we have an array of callback functions _onChange that we can notify when we update the form value. We also have an _onDisabledChangearray of callback functions for when we change the disabled state of the component. We can register callbacks with the registerOnChange() or registerOnDisabledChange() functions:

registerOnChange(fn: Function): void { this._onChange.push(fn); }
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void { this._onDisabledChange.push(fn); }

patchValue() just calls setValue() on the FormControl component and is there for completeness.

Finally, we have a _syncPendingControls() function, that will update the control with the latest _pendingDirty, _pendingTouched, and _pendingValue values only in the case that updateOn === ‘submit’.

/** @internal */ 
_syncPendingControls(): boolean {
if (this.updateOn === 'submit') {
if (this._pendingDirty) this.markAsDirty();
if (this._pendingTouched) this.markAsTouched();
if (this._pendingChange) {
this.setValue(this._pendingValue, {onlySelf: true,
emitModelToViewChange: false});
return true;
}
}
return false;
}

This allows the user to change the internal component values without officially updating its state. All such, values get temporarily assigned to a _pending variables and will only change the state of the component on demand when the _syncPendingControls() function is called, e.g. by a parent FormGroup or FormArray whose submit button has just been pressed.

FormGroup

FormGroup is our virtual model of our form and is defined in models.ts. The Angular docs explain what FormGroup does quite well:

A `FormGroup` aggregates the values of each child `FormControl` into one object, with each control name as the key. It calculates its status by reducing the status values of its children.

For example, if one of the controls in a group is invalid, the entire group becomes invalid. `FormGroup` is one of the three fundamental building blocks used to define forms in Angular, * along with `FormControl` and `FormArray`.

We can create a FormGroup instance as follows:

// Define a basic form group consisting of two controls
const form = new FormGroup({
first: new FormControl('Nancy', Validators.minLength(2)),
last: new FormControl('Drew'),
});
// Include a group-level validator
const form2 = new FormGroup(
{
password: new FormControl('', Validators.minLength(2)),
passwordConfirm: new FormControl('', Validators.minLength(2))
},
passwordMatchValidator);

The FormGroup class is defined in model.ts:

// Creates a new `FormGroup` instance.
export class FormGroup extends AbstractControl {
constructor(
// A collection of child controls.
public controls: {[key: string]: AbstractControl},

// A synchronous validator function, or an array of such func
validatorOrOpts?:
ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
// A single async validator or array of async validator functions
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null
){
super(
coerceToValidator(validatorOrOpts),
coerceToAsyncValidator(asyncValidator, validatorOrOpts));
this._initObservables(); this._setUpdateStrategy(validatorOrOpts);
this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}
// Other methods
}

The FormGroup constructor takes a collection of child controls via the controls parameter, together with a synchronous or aynchronous validator function (or array). It then calls its parent class with the synchronous and asynchronous validators. Finally, the constructor initialises the Observables, sets up the update strategy and controls and then updates itself. Did you spot that the controls and the parent class of FormGroup is the AbstractControl class?

Conclusion

Angular’s forms library and models help us to create virtual forms that we can manipulate and subscribe to. The models share common interfaces and behaviour which makes working with the different components easier. This enables the Angular Reactive Form directives take our DOM elements and link them with our models so that any changes get updated in our components automatically.

Hopefully this article has helped to shine a light on how Form Groups and FormControl components are modelled internally by angular.

Functional Programming for Humans

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store