Richard Hunter
  • Projects
  • Writings
  • Contact

making sense of Angular forms
created: Feb 22 2023
edited: Mar 05 2023

a deep dive into Angular's two form modules

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 the input that use the exported directive to have an old value as they have been dirty checked before. As this is a very common case for ngModel, 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 ExpressionChangedAfterItHasBeenCheckedErrorerror 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

Featured writings

creating a bar chart using D3

a tutorial demonstrating how to create a vertical bar chart ...

making sense of Angular forms

a deep dive into Angular's two form modules...

Angular Resolution Modifiers and scope

how resolution modifiers like @Host and @Self affect injecto...

Injectors in Angular Dependency Injection

a deep dive into the scope of Component and Directive inject...

commentary on Kara Erickson's talk on Angular Forms

commentary on a talk given by Angular Core developer Kara Er...

Building my first computer game

My first attempt at creating a computer game in Javascript...

Animated page transitions in a Single Page App

In a Single Page App, we can take control of the routing pro...

Problems with Redux and why Streams are better

A discussion on some of the drawbacks of using Redux within ...

Implementing Angular's Hero app using React

Implementing Angular documentation's Hero app using React. ...

Dependency Injection

A discussion on Dependency Injection and a library I have wr...

Ways of making CSS clip-path property work better

Ways of making the clip path better...

Carracci: a UML diagram editing tool

Carracci is a project that I have been working on recently i...
  • Home
  • Projects
  • Writings
  • Contact