Angular has two form modules: Reactive and Template-driven. this is a common point of confusion and contention amongst developers. why have two rather than only one? the docs do try to make clear the reason for this and the distinctions between the two modules, but there is always some room for improvement, which is the purpose of this article
to answer the first question: the two modules are not, in fact, entirely distinct from each other. the Reactive module provides the core functionality for handling forms in Angular, whilst the Template-driven module is an abstraction built on top of it for the purpose of convenience
the Angular docs list some of the differences between the two modules in the following table:
Reactive | Template-driven | |
---|---|---|
Setup of form model | Explicit, created in component class | Implicit, created by directives |
Data model | Structured and immutable | Unstructured and mutable |
Data flow | Synchronous | Asynchronous |
Form validation | Functions | Directives |
some of the terms in this table require an explanation. they are terms that we probably understand in a general sense, but what their meaning is in an Angular forms context is a little less clear, I would argue
explicit versus implicit
Reactive forms are explicit in the sense that we work directly with the form model, creating it with classes such as FormControl
. the form model reflects the actual state of the form, containing such information as the form's data and validity status. Template-driven forms are implicit in the sense that we do not deal directly with the form model. the form model is created by Angular when we apply the ng-model
directive
structured and immutable versus unstructured and mutable
I think these terms might be the most confusing. obviously, everything has some structure, so it's not entirely clear what is meant here. with Reactive forms, the form model is immutable since when you update the form, you don't change the form model, you create a completely new form model. in Template-driven forms, you bind directly to values of the data-model and these are changed directly when you update the form
synchronous versus asynchronous
the difference between these probably isn't very significant, and it really only becomes an issue in unit testing. Template-driven forms are described as asynchronous, but, in fact, this is only for model-to-view updates. view-to-model updates remain synchronous. what happens in the code is that the model updates and change-detection has completed, a call to update the form model is made asynchronously. this is the actual code in ngModel
//https://github.com/angular/angular/blob/15.1.5/packages/forms/src/directives/ng_model.ts
private _updateValue(value: any): void {
resolvedPromise.then(() => {
this.control.setValue(value, {emitViewToModelChange: false});
this._changeDetectorRef?.markForCheck();
});
}
the reason for doing this is somewhat obscure, but is given in a comment within the code
ngModel
can export itself on the element and then be used in the template. Normally, this would result in expressions before theinput
that use the exported directive to have an old value as they have been dirty checked before. As this is a very common case forngModel
, we added this second change detection run.
I explored this in an experiment. we have two separate form elements. the second form allows us to carry out a model-to-view update on the first form. on the first form, we export the ngModel
on the firstName input field to the myName
template reference variable, allowing us to display it in the template. we interpolate it into the template both before and after the input field
<pre>
{{myName.value | json}}
</pre>
<form #myForm="ngForm">
<input name=“firstName" [(ngModel)]=“firstName" #myName="ngModel" />
</form>
<pre>
{{myName.value | json}}
</pre>
<h2>other form</h2>
<form>
<input name="name2" [(ngModel)]="name" />
</form>
we then edit the code for ngModel so that the code to update the form model runs synchronously:
private _updateValue(value: any): void {
//resolvedPromise.then(() => {
this.control.setValue(value, {emitViewToModelChange: false});
this._changeDetectorRef?.markForCheck();
//});
}
now, when we change some value in the second form, we see two things: firstly, the first interpolation of myName.value
has not updated, whilst the second interpolation has. secondly, we see a ExpressionChangedAfterItHasBeenCheckedError
error in the console. the error disappears if we delete the first interpolation
how inputs are updated in Reactive and Template-driven forms
whilst we talk about forms, it is instructive to see how updates occur at the level of individual input fields. as Reactive forms are the foundation for Template-driven forms, I will deal with them first
Reactive inputs
the main building blocks of a Reactive form control are: the DOM element (usually the input field), a ControlValueAccessor, and a FormControl
. the purpose of the ControlValueAccessor
is to form a bridge between the DOM and the FormControl
. when the user types something into an input field, the ControlValueAccessor
updates the FormControl
. when the FormControl
is updated externally, the ControlValueAccessor
updates the input field with the new value.
for this to work, the user has to have created a FormControl
instance in the component and referenced this in the template with a formControl directive. on encountering the formControl
directive, Angular instantiates FormControl and ControlValueAccessor objects in the injector of this element. the FormControl
is able to inject the ControlValueAccessor
to get a reference to it
some set up is done where a listener is registered on the ControlValueAccessor
that listens to user input changes and updates the FormControl; and a listener is registered on the FormControl
which calls the ControlValueAccessor
to update the input field
Template-driven inputs
Template-driven inputs are, by their nature, a little bit more complicated. the main difference is that to the building blocks of a Reactive input, we add NgModel
. an important thing to understand about NgModel
is that it follows Angular's two-way data binding protocol, so lets explain that first
the main idea behind two-way data binding is that property binding []
and event binding ()
are combined [()]
(sometimes called the 'bananas in boxes' syntax). the following:
<my-component [(foo)]="blah"></my-component>
will be de-sugared into:
<my-component [foo]="blah" (fooChange)="blah=$event"></my-component>
thus, two-way data binding will work as long as a component's @Output
property takes the form <name-of @input property>Change
ngModel
takes advantage of this protocol. the following:
<my-component [(ngModel)]=“foo”></my-component>
gets de-sugared into:
<my-component [ngModel]=“foo” (ngModelChange)=“foo=$event”></my-component>
when Angular encounters the ngModel
directive, it instantiates NgModel
and ControlValueAccessor
objects in the injector of this element. NgModel
is able to inject the ControlValueAccessor
to get a reference to it. NgModel
creates its own internal instance of FormControl
some set up is done where a listener is registered on the FormControl
for model-to-view updates, and a listener is registered on the ControlValueAccessor
for view-to-model updates
when the user types some input into the field, the ControlValueAccessor
will update the FormControl
with the new value, and it will inform NgModel
of the update. NgModel
will emit an ngModelChange
event with the new value. two-way data binding will kick in to update the data model
when the data model changes, ngOnChanges
on NgModel
will run as its data-bound inputs have changed. this asynchronously sets the value of the FormControl
. the previously registered listener of the FormControl
will run, calling writeValue
on the ControlValueAccessor
which updates the new value in the input field