Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: James Allen@CFJamesAllen )

My First - And Possibly Last - Look At Reactive Forms In Angular 7.2.13

By Ben Nadel on

CAUTION: The vast majority of forms that I create in Angular are very simple and require almost no validation (mostly just "empty" checks). As such, this entire post should be viewed from the perspective of someone who rarely needs complex validation rules or behaviors.

For the last 7-years or so, I've used template-driven forms, both in my Angular.js days and now in Angular 2 (through 7). And, to be honest, they've always "just worked." From very simple forms to fairly complex, dynamic forms, the template-driven approach has been perfectly capable. Even NgModel works easily with features like custom Control Value Accessors. In short, template-driven forms have never left me wanting. But, people in the Angular community rave about the power and ease-of-use with Reactive Forms. So, I wanted to at least take a look at how they work. At first blush, Reactive Forms do not "spark joy" in me. I found the API of Reactive Forms to require a more robust mental model; and, with no immediately obvious payoff in terms of data-processing or Template simplicity.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

In order to try and compare Apples to Apples - regarding Template-Driven and Reactive Forms in Angular - I wanted to create parallel examples in the same demo. So, I created a "TemplateFormComponent" and a "ReactiveFormComponent" that do the same thing. And, then I simply switched between the two Angular Components based on the user-selection.

As a context for this exploration, I borrowed from my previous demo on dynamic, template-driven forms. Only this time, instead of just creating N-number of Pets, I'm also allowing each Pet to have N-number of "nicknames". This "nested array" type data feels like a sufficiently complex context in which I can earnestly kick the tires of each Form-based methodology.

In each of these examples, I did try to leverage some degree of validation. Mostly, I just used the "required" attribute and then added a CSS class based on the derived "valid" property. It doesn't really work that well (the way I have it) - it's not something I have a lot of experience with (see CAUTION above); and, should not be viewed as a critical part of this thought-experiment. It was just a way for me to get a bit more familiar with each Form's API.

That said, let's start by looking at the Template-Driven Form in Angular. Remember, the form allows you to add and remove Pets; and, for each Pet, add and remove Nicknames. Here's the Class behind this component:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { NgForm } from "@angular/forms";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // NOTE: I am giving the form view-model entries an "id" property because it makes the
  • // values easier to consume in an NgModel context. These are not intended to be database
  • // identifiers - just unique identifiers within the form's view-model.
  •  
  • interface Pet {
  • id: number;
  • type: string;
  • name: string;
  • age: string;
  • isPastOn: boolean;
  • nicknames: Nickname[];
  • }
  •  
  • interface Nickname {
  • id: number;
  • value: string;
  • }
  •  
  • @Component({
  • selector: "my-template-form",
  • styleUrls: [ "./form.component.less" ],
  • templateUrl: "./template-form.component.htm"
  • })
  • export class TemplateFormComponent {
  •  
  • public form: {
  • pets: Pet[];
  • };
  •  
  • // I initialize the template-form component.
  • constructor() {
  •  
  • this.form = {
  • pets: []
  • };
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I add a new nickname record to the given pet form-model.
  • public addNickname( pet: Pet ) : void {
  •  
  • pet.nicknames.push({
  • id: Date.now(),
  • value: ""
  • });
  •  
  • }
  •  
  •  
  • // I add a new pet record to the form-model.
  • public addPet() : void {
  •  
  • this.form.pets.push({
  • id: Date.now(),
  • type: "Dog",
  • name: "",
  • age: "",
  • isPastOn: false,
  • nicknames: []
  • });
  •  
  • }
  •  
  •  
  • // I process the form-model.
  • public processForm( form: NgForm ) : void {
  •  
  • console.warn( "Handling form submission!" );
  •  
  • console.group( "Form Data" );
  • console.log( JSON.stringify( this.form.pets, null, 4 ) );
  • console.groupEnd();
  •  
  • console.group( "Form Model" );
  • console.log( form );
  • console.groupEnd();
  •  
  • }
  •  
  •  
  • // I remove the given nickname from the given pet.
  • public removeNickname( pet: Pet, nickname: Nickname ) : void {
  •  
  • this.removeFromCollection( pet.nicknames, nickname );
  •  
  • }
  •  
  •  
  • // I remove the given pet from the form-data.
  • public removePet( pet: Pet ) : void {
  •  
  • this.removeFromCollection( this.form.pets, pet );
  •  
  • }
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  • // I remove the given item from the given collection. Mutates the collection.
  • private removeFromCollection<T>( collection: T[], item: T ) : void {
  •  
  • collection.splice( collection.indexOf( item ), 1 );
  •  
  • }
  •  
  • }

What I love about Template-Driven forms in Angular is how simple and straightforward this code is. It's all vanilla data-types that are strongly-typed (something that is hard in Reactive-Forms) and can be easily manipulated with standard JavaScript techniques.

In fact, there's not really anything in this code, other than some of the property and method names that even indicates that this data is driving some sort of Form-based interaction. That might be a benefit or a drawback, depending on your perspective; but, I point it out only to showcase the fact that Template-Driven forms work naturally and seamlessly with your existing data strategies.

Now, let's look at the Template for this Template-Driven form. Because there's nothing in the data-structures that knows about the Form Controls, all of the association of the data to the various Form Controls has to be performed within the template itself. This is accomplished through several Angular directives provided by the FormsModule:

  • NgForm (implicitly applied)
  • NgModelGroup
  • NgModel

To be 100% transparent, I didn't even know about NgModelGroup directive before writing this demo. NgModelGroup creates Form Control hierarchies and helps create a local context that makes naming the various controls a bit more natural (requiring names to be unique within the "group", not the form). The fact that I never knew about this directive speaks clearly to the fact that the vast majority of my forms are very simple!

As far as I can tell - and I may very well be wrong about this - the NgModelGroup directive doesn't really provide a way to define a group as an "Array". As such, when I define my groups, I have to name them using attribute interpolation:

  • ngModelGroup="pet_{{ pet.id }}"

So, instead of ending up with an Array of Groups that represent Pets, I end up with a unique "key" for each Pet:

  • ngForm.controls.pet_123 = { ...control... }
  • ngForm.controls.pet_456 = { ...control... }
  • ngForm.controls.pet_789 = { ...control... }

Of course, since I don't really ever reach down into the Form Control structures explicitly (I generally drive my validation through the view-model, not the Form Controls), the structure makes little difference to me.

With that said, let's look at the actual template:

  • <form #petsForm="ngForm" (submit)="processForm( petsForm )">
  •  
  • <h2>
  • TemplateForm&lt;Pets&gt;
  • </h2>
  •  
  • <ng-template ngFor let-pet [ngForOf]="form.pets">
  •  
  • <!--
  • Since each ngModel control needs to have a unique name within its PARENT
  • control, we're going to use the NgModelGroup to create a unique parent per
  • ngFor iteration (both for Pets and Nicknames). This will allow our input
  • names to be a bit more sane (deferring the weird naming to the NgModelGroup).
  • --
  • NOTE: With template-driven forms, it doesn't seem possible to define the
  • group as an ARRAY. As such, we are using the various "id" values to calculate
  • a unique group-name per parent context.
  • -->
  • <div
  • #petModelGroup="ngModelGroup"
  • ngModelGroup="pet_{{ pet.id }}"
  • class="pet"
  • [class.pet--invalid]="( petModelGroup.touched && petModelGroup.invalid )">
  •  
  • <!--
  • NOTE: While our "name" attributes only have to be unique to the parent,
  • the "for" attributes still have to be globally-unique for the page. As
  • such, we still have to use complex interpolation for our LABELS.
  • -->
  •  
  • <div class="field">
  • <label for="type_{{ pet.id }}" class="field__label">
  • Type:
  • </label>
  • <div class="field__control">
  • <select id="type_{{ pet.id }}" name="type" [(ngModel)]="pet.type">
  • <option value="Dog">Dog</option>
  • <option value="Cat">Cat</option>
  • </select>
  • </div>
  • </div>
  •  
  • <div class="field">
  • <label for="name_{{ pet.id }}" class="field__label">
  • Name:
  • </label>
  • <div class="field__control">
  • <input
  • type="text"
  • id="name_{{ pet.id }}"
  • name="name"
  • [(ngModel)]="pet.name"
  • required
  • autofocus
  • size="20"
  • placeholder="Name..."
  • />
  •  
  • <div class="nicknames">
  •  
  • <ng-template ngFor let-nickname [ngForOf]="pet.nicknames">
  •  
  • <!-- Create a local group context for ngModel. -->
  • <div
  • ngModelGroup="nickname_{{ nickname.id }}"
  • class="nicknames__item">
  •  
  • <input
  • type="text"
  • name="nickname"
  • [(ngModel)]="nickname.value"
  • required
  • autofocus
  • size="20"
  • placeholder="Nickname..."
  • />
  •  
  • <a
  • (click)="removeNickname( pet, nickname )"
  • title="Remove Nickname"
  • class="nicknames__remove">
  • &times;
  • </a>
  •  
  • </div>
  •  
  • </ng-template>
  •  
  • <a (click)="addNickname( pet )" class="nicknames__add">
  • + nickname
  • </a>
  •  
  • </div>
  • </div>
  • </div>
  •  
  • <div class="field">
  • <label for="age_{{ pet.id }}" class="field__label">
  • Age:
  • </label>
  • <div class="field__control">
  • <input
  • type="text"
  • id="age_{{ pet.id }}"
  • name="age"
  • [(ngModel)]="pet.age"
  • size="10"
  • placeholder="Age..."
  • />
  • </div>
  • </div>
  •  
  • <div class="field">
  • <label for="isPastOn_{{ pet.id }}" class="field__label">
  • Passed:
  • </label>
  • <div class="field__control">
  • <input
  • type="checkbox"
  • id="isPastOn_{{ pet.id }}"
  • name="isPastOn"
  • [(ngModel)]="pet.isPastOn"
  • />
  • ( ie, is deceased )
  • </div>
  • </div>
  •  
  • <a (click)="removePet( pet )" title="Remove Pet" class="pet__remove">
  • &times;
  • </a>
  •  
  • </div>
  •  
  • </ng-template>
  •  
  • <p class="actions">
  • <a (click)="addPet()">Add Another Pet</a>
  • </p>
  •  
  • <!--
  • Since we are [implicitly] registering each form control with the parent NgForm
  • and NgModelGroup instances, the validity of the form will be an aggregation of
  • the individual control validity. As such, we can disable the form submission if
  • the form looks invalid as a whole.
  • -->
  • <button type="submit" [disabled]="( ! form.pets.length || ! petsForm.form.valid )">
  • Process Form
  • </button>
  •  
  • </form>

There's a lot of HTML here. But, hopefully it's not too hard to follow. Basically, it's just two nested NgFor loops that iterate over the vanilla data that we defined in the Class. The trickiest part of this template is understanding how the NgModelGroup directive defines a local context and exposes a "Control" reference (#petModelGroup) within each NgFor iteration that aggregates the state of the embedded NgModel controls.

Now, if we run this version of the demo and submit some data, we get the following output:


 
 
 

 
 Using template-driven forms in Angular 7.2.13. 
 
 
 

Yay, it worked - we have arbitrarily nested arrays of form data using template-driven forms.

Now, for those of you who are looking at this amount of HTML and thinking, "Yeah, that's exactly why you should be using Reactive-Forms", you will find that the following Reactive-Forms version leads to no reduction in HTML complexity. At least, not in the way that I was able to get it to work. Perhaps more experienced Reactive-Forms developers can point out where I could have cleaned things up.

So, with that, let's more to the Reactive-Forms version of this demo. The Reactive-Forms version attempts to provide the same exact functionality - define N-number Pets that contains N-number nicknames. Here is the ReactiveFormComponent:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  • import { FormArray } from "@angular/forms";
  • import { FormBuilder } from "@angular/forms";
  • import { FormControl } from "@angular/forms";
  • import { FormGroup } from "@angular/forms";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // Unlike the Template-Driven forms, which just use a simple View-Model, there is no way
  • // to define a "Type" / "Interface" for Reactive-Forms. At least, none that I could find.
  • // --
  • // Potential workaround by Daniele Morosinotto:
  • // https://github.com/angular/angular/issues/13721#issuecomment-468745950
  • // --
  • // interface Pet {
  • // id: number;
  • // type: string;
  • // name: string;
  • // age: string;
  • // isPastOn: boolean;
  • // nicknames: Nickname[];
  • // }
  • //
  • // interface Nickname {
  • // id: number;
  • // value: string;
  • // }
  •  
  • @Component({
  • selector: "my-reactive-form",
  • styleUrls: [ "./form.component.less" ],
  • templateUrl: "./reactive-form.component.htm"
  • })
  • export class ReactiveFormComponent {
  •  
  • public form: FormGroup;
  •  
  • private formBuilder: FormBuilder;
  •  
  • // I initialize the reactive-form component.
  • constructor( formBuilder: FormBuilder ) {
  •  
  • this.formBuilder = formBuilder;
  •  
  • this.form = this.formBuilder.group({
  • pets: this.formBuilder.array( [] )
  • });
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I add a new nickname record to the given pet form-model.
  • public addNickname( pet: FormControl ) : void {
  •  
  • var nicknames = this.getNicknames( pet ); // Critical: casts as FormArray.
  •  
  • nicknames.push(
  • this.formBuilder.group({
  • // NOTE: In order to facilitate the unique LABEL-For creation for each
  • // control, we have to hack-in a pseudo-control for "id", and then just
  • // consume it as a static value.
  • id: [ Date.now() ],
  • value: [ "" ]
  • })
  • );
  •  
  • }
  •  
  •  
  • // I add a new pet record to the form-model.
  • public addPet() : void {
  •  
  • var pets = this.getPets(); // Critical: casts as FormArray.
  •  
  • pets.push(
  • this.formBuilder.group({
  • // NOTE: In order to facilitate the unique LABEL-For creation for each
  • // control, we have to hack-in a pseudo-control for "id", and then just
  • // consume it as a static value.
  • id: [ Date.now() ],
  • type: [ "Dog" ],
  • name: [ "" ],
  • age: [ "" ],
  • isPastOn: [ false ],
  • nicknames: this.formBuilder.array( [] )
  • })
  • );
  •  
  • }
  •  
  •  
  • // I return the nicknames for the given pet in a format that can be consumed by the
  • // ngFor loop in the template.
  • public getNicknames( pet: FormControl ) : FormArray {
  •  
  • // NOTE: The "as FormArray" is the critical part since AbstractControl doesn't
  • // have a property, "controls", which is used by the NgFor loop.
  • return( pet.get( "nicknames" ) as FormArray );
  •  
  • }
  •  
  •  
  • // I return the pets in a format that can be consumed by the ngFor loop in the
  • // template.
  • public getPets() : FormArray {
  •  
  • // NOTE: The "as FormArray" is the critical part since AbstractControl doesn't
  • // have a property, "controls", which is used by the NgFor loop.
  • return( this.form.get( "pets" ) as FormArray );
  •  
  • }
  •  
  •  
  • // I process the form-model.
  • public processForm( form: FormGroup ) : void {
  •  
  • console.warn( "Handling form submission!" );
  •  
  • console.group( "Form Data" );
  • console.log( JSON.stringify( this.form.value, null, 4 ) );
  • console.groupEnd();
  •  
  • console.group( "Form Model" );
  • console.log( form );
  • console.groupEnd();
  •  
  • }
  •  
  •  
  • // I remove the given nickname from the given pet.
  • public removeNickname( pet: FormControl, nickname: FormControl ) : void {
  •  
  • // Critical: casts as FormArray.
  • this.removeFromCollection( this.getNicknames( pet ), nickname );
  •  
  • }
  •  
  •  
  • // I remove the given pet from the form-data.
  • public removePet( pet: FormControl ) : void {
  •  
  • // Critical: casts as FormArray.
  • this.removeFromCollection( this.getPets(), pet );
  •  
  • }
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  • // I remove the given control from the given array. Mutates the array.
  • private removeFromCollection( collection: FormArray, item: FormControl ) : void {
  •  
  • collection.removeAt( collection.controls.indexOf( item ) );
  •  
  • }
  •  
  • }

Coming from the template-driven context, the Class for this reactive-forms component is - at least to me - more complex. We're no longer working with vanilla data-structures. Instead, we're dealing with Form Control objects that have to be built using a Builder class.

NOTE: The FormBuilder class isn't strictly required, but it does simplify the construction of the Form Controls.

Since we are no longer dealing with vanilla data-structures, you can see that we lose the ability to attach TypeScript interfaces to our data. Now, when we pull data out of the Form Controls (not shown in the demo), we'll have to cast the data if we want any subsequent compile-time validation.

And, that's not to be confused with the ("as FormArray") casting that I have in my component. This casting is necessary in order for the two FormArray values to be consumed by the NgFor directives within the template. Since the FormArray extends the AbstractControl, which has no concept of a ".controls" property, we have to provide a proxy-method that gets the FormArray and casts it so that the template can safely consume the FormArray's ".controls" property.

The other weird thing here is that in order to provide a unique ID for each Pet and Nickname (to be used with label-for bindings), I had to define an id "Form Control". Unlike with the template-driven approach, where I'm binding to vanilla data, I couldn't find a "clean" way to associate static data (like the id) with each Form Control. Hopefully someone with more experience will see this and tell me how to accomplish in a more intelligent way.

And, now that we've seen the ReactiveFormComponent, let's take a look at the template. As with the template-driven approach, the reactive-form approach also uses Angular directives to bind the HTML to the View-Model. Only, instead of having the directives implicit create intermediary Form Controls on our behalf, we're binding to the Form Controls that we explicitly created in our view-model.

Do to this, we use the following Angular directives from the ReactiveFormsModule:

  • formGroup
  • formArrayName
  • formGroupName
  • formControlName

These reactive-form directives are basically parallels to the template-driven directives (depending on how hard you squint). "formGroup" is basically "NgForm"; "formGroupName" is basically "NgModelGroup"; and "formControlName" is basically "NgModel".

The major difference is that with the addition of the "formArrayName", we can now define our "groups" as true Arrays instead of as unique keys within the parent group. Essentially:

  • ngModelGroup="pet_{{ pet.id }}"

... is replaced by this combination of nested directives:

  • formArrayName="pets"
  • [formGroupName]="petIndex"

This makes the Form Controls data-structure easier to inspect. But, since I never really consume the Form Controls in my logic, this internal structure doesn't matter all that much to me. To the degree (or lack thereof) that I use the Form Controls to drive validation in my component, all this does is add an extra element and template reference (let-petIndex). Of course your mileage may vary depending on how much you try to leverage the Form Control features.

With that said, let's look at the actual HTML:

  • <form [formGroup]="form" (submit)="processForm( form )">
  •  
  • <h2>
  • ReactiveForm&lt;Pets&gt;
  • </h2>
  •  
  • <!--
  • Defines the "form.pets" context. It seems that I have to create this arbitrary
  • container in order for the control-mapping to work properly based on index.
  • -->
  • <div formArrayName="pets">
  •  
  • <!--
  • NOTE: I need to get the pets from a method call because we need to CAST the
  • value to a FormArray. If we try to access "form.pets.controls" directly, we
  • get the error, "Property 'controls' does not exist on type 'AbstractControl'".
  • -->
  • <ng-template ngFor let-pet let-petIndex="index" [ngForOf]="getPets().controls">
  •  
  • <!--
  • The formGroupName uses the index of the FormArray iteration, essentially
  • mapping form.pets[ petIndex ] to the current FormGroup.
  • -->
  • <div
  • [formGroupName]="petIndex"
  • class="pet"
  • [class.pet--invalid]="( pet.touched && pet.invalid )">
  •  
  • <!--
  • NOTE: While we don't need a NAME attribute to bind our inputs to our
  • FormControls, we DO NEED to create a unique FOR/ID combination for
  • our labels. As such, we need to reach into the pet VALUE in order to
  • access the pseudo-control, "id".
  • -->
  •  
  • <div class="field">
  • <label for="type_{{ pet.value.id }}" class="field__label">
  • Type:
  • </label>
  • <div class="field__control">
  • <select id="type_{{ pet.value.id }}" formControlName="type">
  • <option value="Dog">Dog</option>
  • <option value="Cat">Cat</option>
  • </select>
  • </div>
  • </div>
  •  
  • <div class="field">
  • <label for="name_{{ pet.value.id }}" class="field__label">
  • Name:
  • </label>
  • <div class="field__control">
  • <input
  • type="text"
  • id="name_{{ pet.value.id }}"
  • formControlName="name"
  • required
  • autofocus
  • size="20"
  • placeholder="Name..."
  • />
  •  
  • <!-- Create a local array context for pet.nicknames. -->
  • <div formArrayName="nicknames" class="nicknames">
  •  
  • <ng-template
  • ngFor
  • let-nickname
  • let-nicknameIndex="index"
  • [ngForOf]="getNicknames( pet ).controls">
  •  
  • <!-- Create a local group context for nickname. -->
  • <div
  • [formGroupName]="nicknameIndex"
  • class="nicknames__item">
  •  
  • <input
  • type="text"
  • formControlName="value"
  • required
  • autofocus
  • size="20"
  • placeholder="Nickname..."
  • />
  •  
  • <a
  • (click)="removeNickname( pet, nickname )"
  • title="Remove Nickname"
  • class="nicknames__remove">
  • &times;
  • </a>
  •  
  • </div>
  •  
  • </ng-template>
  •  
  • <a (click)="addNickname( pet )" class="nicknames__add">
  • + nickname
  • </a>
  •  
  • </div>
  •  
  • </div>
  • </div>
  •  
  • <div class="field">
  • <label for="age_{{ pet.value.id }}" class="field__label">
  • Age:
  • </label>
  • <div class="field__control">
  • <input
  • type="text"
  • id="age_{{ pet.value.id }}"
  • formControlName="age"
  • size="10"
  • placeholder="Age..."
  • />
  • </div>
  • </div>
  •  
  • <div class="field">
  • <label for="isPastOn_{{ pet.value.id }}" class="field__label">
  • Passed:
  • </label>
  • <div class="field__control">
  • <input
  • type="checkbox"
  • id="isPastOn_{{ pet.value.id }}"
  • formControlName="isPastOn"
  • />
  • ( ie, is deceased )
  • </div>
  • </div>
  •  
  • <a (click)="removePet( pet )" title="Remove Pet" class="pet__remove">
  • &times;
  • </a>
  •  
  • </div>
  •  
  • </ng-template>
  •  
  • <p class="actions">
  • <a (click)="addPet()">Add Another Pet</a>
  • </p>
  •  
  • </div>
  •  
  • <!--
  • Since we have explicitly defined each FormControl inside of a set of nested
  • FormGroup classes, the validity of the form will be an aggregation of the
  • individual control validity. As such, we can disable the form submission if
  • the form looks invalid as a whole.
  • -->
  • <button type="submit" [disabled]="( ! getPets().length || ! form.valid )">
  • Process Form
  • </button>
  •  
  • </form>

Now, keep in mind that this is my first ever look at Reactive-Forms in Angular; so, maybe I'm really missing the mark on how this works. But, I was surprised that the HTML for this form was just as - if not slightly more - complicated than the HTML used in the template-driven form approach. Yes, I no longer have to add "name" to my input elements or expose a #petModelGroup reference. But, the DOM (Document Object Model) has an additional level of nesting (for "formArrayName"); and, my NgFor loops expose both an index value and require special methods that cast the AbstractControl "as FormArray".

At best, the two templates are equally complex. And, at worst, the reactive-one feels like it has to jump through one or two more hoops. Which is surprising considering that the Reactive-Forms approach already tightly couples the view-model to the concept of a Form (with its explicit creation of Form Controls).

I know I probably sound a little bit jaded. And, that's not without some basis. It literally took me 3-mornings to figure out how to put the Reactive-Forms version of this demo together. The whole casting of Form Controls for template-consumption and understanding how to properly nest things in the FormBuilder - it was radically different than anything I've done so far with forms. This one mistake cost me several hours:

nicknames: [ this.formBuilder.array( [] ) ]

... which should have been:

nicknames: this.formBuilder.array( [] )

Given my complete lack of experience with Reactive-Forms - and with the FormBuilder class - it was a mistake that I didn't spot quickly. Especially considering that the component didn't "blow up" with this mistake. Instead, it just didn't quite work.

So, please consider that any tone of frustration that comes through in my writing to be - at least in part - a byproduct of the learning curve required by the Reactive-Forms methodology.

That said, if we load the Reactive-Forms version in the browser and submit some data, we get the following output:


 
 
 

 
 Using Reactive-Forms in Angular 7.2.13. 
 
 
 

Yay, it worked - we have arbitrarily nested arrays of form data using reactive-forms.

It's hard to provide a fair-and-impartial comparison of the two types of Angular Forms. After all, I have 7-years of experience with template-driven forms and only 3-mornings of experience with reactive forms. But - and I really am trying to be honest with my emotions - there's nothing about Reactive Forms that immediately strikes a chord in me. From everything that I can see in my own demo, template-driven forms use a more straightforward data-model and - at worst - require an equally complex HTML template.

For the way that I tend to use forms in my Angular applications, the reactive forms approach appears to add unnecessary complexity. If you go hardcore into validation and use synchronous and asynchronous validators and you heavily use CSS classes based on form state, then maybe Reactive Forms holds a lot of value? I can't really say from any experience. I know that template-driven forms also have a lot of that same functionality; but, I don't have a lot of experience with it there either.


 
 
 

 
 Reactive forms may have been the opportunity of your life but i don't want your life. 
 
 
 

I'm happy that I finally looked into Reactive Forms in Angular 7.2.13. I was curious to see what everyone in the Angular community was raving about. But, template-driven forms feel like they are more in alignment with the way that I currently approach form-based user interactions. Perhaps this will change over time. But, form-processing, even with template-driven forms, is not currently a point-of-friction for me in my Angular applications.



Reader Comments

This is a great review. I think one of the main advantages of a Reactive form is that it can be unit tested.

The second advantage which you haven't touched upon, is that Reactive Forms use an Observable based API, which turns not just the form itself, but each of its fields into a value stream, like:

this.form.valueChanges	
	.map((value) => {
	value.firstName = value.firstName.toUpperCase();
	return value;
	})
	.filter((value) => this.form.valid)
	.subscribe((value) => {
	console.log("Reactive Form valid value: vm = ",
	JSON.stringify(value));
	})

Admittedly, there is more set up code in the controller, but I think overall, it is worth it, for the advantages that a Reactive Form can provide.

Reply to this Comment

Great to hear smart people question the need to make things more complex. What happened to the idea where we use simple building blocks to build complex systems - rather than using an overengineered approach for simple tasks.

Lets develop maintainable systems that don't require 3 f'ing mornings to put together simple forms.

Reply to this Comment

Honestly Dan. If you use Reactive Forms all the time, they're not really that complicated. Yes, they require a little more set up code in the controller, but they certainly don't take 3 mornings to set up.

Using a Reactive form with something like TinyMCE, is a real godsend, because you can tap into 'valueChanges', which notifies us when the editor content changes.

Having said that, I do appreciate the simplicity of the 'ngModel' approach. And, I will be using Template driven forms, for certain tasks, where it makes sense.

Reply to this Comment

@Dan,

Yeah, please don't take my 3-morning journey as a common case. This was my first look at Reactive Forms. And, admittedly, most of that 3-morning stuff came specifically from trouble with FormArray values. I had messed up my nested data structure; and, I was trying so hard to remove the getPets() and getNicknames(pet) calls from the template. I really really really didn't want to have to cast as FormArray. But, it seems like that is what everyone does. So, I finally gave in and just accepted it.

What's weird, though, is that I see demos where people did just loop over field.controls in the Template; so, I wonder if this was something that changes. Or, if this is specifically an Ahead of Time (AoT) compiling issue, since the error is thrown in the template, which I think won't happen in the Just in Time (JIT) compiler.

Reply to this Comment

@Charles,

You raise a good point. There are ways to listen for changes in Template-Driven Forms. Mostly, I just used (ngModelChange) event-bindings. But, I believe there are ways to listen for "status" and "value" changes on the whole form itself. That is something I should take a look at and get better at.

Reply to this Comment

Ben. Yes, absolutely. You can indeed listen to both value & status changes on the form itself. So, I guess that for some projects, this could be an important reason going down the Reactive route.

To be honest, I like the fact that Angular developers can choose which type of paradigm fits the brief.

But, after my exploration, yesterday, with a Template form, I shall certainly be using it more often. I didn't even have to use the FORM tag!

There is just one issue, I came across.

Essentially, I had to create a set of radio buttons, like:

<label id="user-admin-group">What would you like to do?</label>
<mat-radio-group
          aria-labelledby="user-admin-group"
          class="user-admin-group"
          [(ngModel)]="userAdminUnselectedChanges">
          <mat-radio-button class="user-admin-button" *ngFor="let option of userAdminUnselectedChangesOptions" [value]="option">
            {{ option }}
          </mat-radio-button>
 </mat-radio-group> 

Now in my controller, I have:

userAdminUnselectedChangesOptions: string[] = ['Let the system select the rows automatically and continue with the submission?', 'Let the system select the rows where the changes were made and allow you to make the submission manually?', 'Continue with the submission?'];

What I wanted to do, was add a unique numeric identifier into each radio button, so that when it is clicked, I can read this value instead of a long string of text?

Reply to this Comment

Just to clarify. I need to be able to access the questions, as well as a numeric identifier.

So, I could do something like create an array of objects like:

userAdminUnselectedChangesOptions: string[] = [{id:1,value:'Let the system select the rows automatically and continue with the submission?'}, {id:2,value:'Let the system select the rows where the changes were made and allow you to make the submission manually?'}, {id:3,value:'Continue with the submission?'}];

But, is it possible to use an object as the [(ngModel)] value? And, even if this were possible, it only solves half the problem.

How do I make the 'id' available when a user clicks on my one of the text string options?

In the end, I had to use an ENUM to create a map between the text string value of [(ngModel)] and a numeric identifier. The approach works but it feels a bit dirty:

export enum userAdminUnselectedChangesOptionsStatus {
  'Let the system select the rows automatically and continue with the submission?' = 1 ,
  'Let the system select the rows where the changes were made and allow you to make the submission manually?' = 2,
  'Continue with the submission?' = 3
}

Then, after a user selects a radio button option:

const value = userAdminUnselectedChangesOptionsStatus[this.userAdminUnselectedChanges];
        switch(value) {
          case 1:
            // do something...
            break;
          case 2:
            // do something...
            break;
          default:
          // do something...
        }
Reply to this Comment

I'm beginning to wonder whether the 'value' attribute on the radio button is the key to this whole problem? Maybe, this is where I put the 'id'? I'm not really sure, because I have never used radio buttons in Angular before?

Reply to this Comment

I agree 110%. I've been looking at Vue as a more simple method to reactive but I try to stay away from it as much as possible.

Reply to this Comment

Great article. Quick question - how come you use ng-template with [ngFor] rather than ng-container with *ngFor?

It's funny, as I had just read your article about the difference between the two:-)

Reply to this Comment

@Brian,

*ngFor gets transformed to ng-template and ngFor, so he's just doing it manually.

`
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
({{i}}) {{hero.name}}

<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
<div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>`
Reply to this Comment

@Charles,

There's definitely a way to get Radio boxes to work with Objects, where it should use the object-reference in order to determine whether or not the checked property is set, essentially:

[checked]="( option == selectedOption )"

... but, implicitly. I'm 98% sure you shouldn't have to do this; but, I will try it out. I don't use radio-boxes all that much, so they approach is not front-of-mind. But, certainly, they are just specialized checkboxes, which should 100% work with ngModel.

Reply to this Comment

@Brian, @Shawn,

Exactly, they are doing more-or-less the same thing. Sometimes, if an element starts to have too many attributes, I'll break the *ngFor out into an ng-template[ngFor] so that I can separate the "looping" from the "item". I find that it helps me see what is going on.

But, to be clear, I use both approaches. The less complex the HTML, the more likely I am to use the *ngFor syntactic sugar. And, the more complex, the more I like to use the ng-template approach.

Reply to this Comment

I think the advantage of reactive forms is not in simpler control logic, but in better control of events. In Template Driven forms, you have no control over the sequence of things that happens. If you have complicated field-to-field modification logic, you can easily end up with the dreaded Expression has changed after it was checked.

I have a component that handles dates (written because there is a material datepicker), and when I tried to use its (change) event to modify other fields (actually, modify public class properties bound to other ngModel controls), it nearly always ends up in trouble.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.