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: St├ęphane Vantroyen
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: St├ęphane Vantroyen@23yen )

Using Dynamic Template-Driven Forms In Angular 7.2.7

By Ben Nadel on

In Angular, there are two types of forms: template-driven forms and reactive forms. I've only ever used template-driven forms. And, I've never run into a forms-based problem that I couldn't solve with the template-driven paradigm. That said, the other day, I was listening to an episode of Real Talk JavaScript with Ward Bell and John Papa in which they were discussing dynamic forms; and, it occurred to me that almost all of my forms are completely static (in so much as I know what fields will be rendered ahead of time). As such, I wanted to play around with creating dynamic template-driven forms in Angular 7.2.7. This would be a form that is template-driven; but, in which the rendered form controls are determined at runtime.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When you create a form control using NgModel two-way data bindings, each form control either stands on its own (see NgModelOptions); or, it is registered with the NgForm parent. If it is registered with the NgForm parent, the control has to have a unique "name" attribute. This is perhaps the hardest part of creating a dynamic, template-driven forms in Angular; but, since we can use attribute interpolation to define the "name" property, this turns out to be a fairly easy challenge to overcome.

To explore the concept of dynamic, template-driven forms in Angular, I'm going to create a simple form that allows you to create a list of "Pets". The number of pets is up to the user, and can be adjusted at runtime. But, each pet entry will contain its own sense of "validity" (name will be required); and, the form-state as a whole will be an aggregation of each individual pet-state.

This example is simple enough to keep in your head; but, dynamic enough to demonstrate some of the challenges and solutions of dynamic, template-driven forms.

First, let's look at the App Component code that defines the data for the form. This component simply contains a "form" object with a collection of "Pet" entries:

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • interface Pet {
  • id: number;
  • type: string;
  • name: string;
  • age: string; // NOTE: This is a String because it is an open-ended form value.
  • isPastOn: boolean;
  • }
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • templateUrl: "./app.component.htm"
  • })
  • export class AppComponent {
  •  
  • public form: {
  • pets: Pet[];
  • };
  •  
  • // I initialize the app component.
  • constructor() {
  •  
  • this.form = {
  • pets: []
  • };
  •  
  • // Add an initial pet form-entry.
  • this.addPet();
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I add a new pet record to the form-model.
  • public addPet() : void {
  •  
  • // CAUTION: When we output the form controls, we need to provide a unique name
  • // for each input (so that it can be registered with the parent NgForm). For the
  • // sake of this demo, we're going to use the current TIMPESTAMP (Date.now()) as a
  • // hook into something unique about this model.
  • this.form.pets.push({
  • id: Date.now(), // <--- uniqueness hook.
  • type: "Dog",
  • name: "",
  • age: "",
  • isPastOn: false
  • });
  •  
  • }
  •  
  •  
  • // I process the form-model.
  • public processForm( form: any ) : void {
  •  
  • console.warn( "Handling form submission!" );
  •  
  • console.group( "Form Data" );
  • console.log( this.form.pets );
  • console.groupEnd();
  •  
  • console.group( "Form Model" );
  • console.log( form );
  • console.groupEnd();
  •  
  • }
  •  
  •  
  • // I remove the pet at the given index.
  • public removePet( index: number ) : void {
  •  
  • this.form.pets.splice( index, 1 );
  •  
  • }
  •  
  • }

For the sake of the demo, this code is incredibly simple. It does little more than manage a collection of Pets to which the user can add and remove instances. The only part of this code worth paying attention to is the fact that each Pet instance is given a locally-unique "id" property. Since our template-driven form controls will need to be uniquely named, we'll need to use this "id" property to generate unique form-control names in our template.

The template for this App Component is the where all the magic happens. The template uses the ngFor directive to iterate over the Pet instances, providing a set of NgModel-driven inputs for each Pet:

  • <form #petsForm="ngForm" (submit)="processForm( petsForm )">
  •  
  • <h2>
  • Pets
  • </h2>
  •  
  • <ng-template ngFor let-pet [ngForOf]="form.pets" let-index="index" let-isLast="last">
  •  
  • <!--
  • NOTE: We are using the "nameControl" template variable to define our CSS
  • class. Each template variable is scoped to the template in which it was
  • defined; which means, each "nameControl" instance is scoped to the ngFor
  • loop-iteration of the given Pet model.
  • -->
  • <div
  • class="pet"
  • [class.pet--invalid]="( nameControl.touched && nameControl.invalid )">
  •  
  • <!--
  • Each form control has to have a unique "name" property so that it can be
  • registered with the parent NgForm instance (unless it is denoted as
  • "standalone"). As such, we are using attribute interpolation to give each
  • input a locally-unique name based on the model data (XXX_{{ pet.id }}).
  • -->
  • <select name="type_{{ pet.id }}" [(ngModel)]="pet.type">
  • <option value="Dog">Dog</option>
  • <option value="Cat">Cat</option>
  • </select>
  •  
  • <!--
  • NOTE: We are defining a "nameControl" template variable that will give us
  • access to the "NgModel" instance for this form input. We are then using
  • this reference to adjust the CSS class-list on the parent container.
  • -->
  • <input
  • #nameControl="ngModel"
  • type="text"
  • name="name_{{ pet.id }}"
  • [(ngModel)]="pet.name"
  • required
  • autofocus
  • size="20"
  • placeholder="Name..."
  • />
  •  
  • <input
  • type="text"
  • name="age_{{ pet.id }}"
  • [(ngModel)]="pet.age"
  • size="10"
  • placeholder="Age..."
  • />
  •  
  • <label for="isPastOn_{{ pet.id }}">
  • <input
  • type="checkbox"
  • id="isPastOn_{{ pet.id }}"
  • name="isPastOn_{{ pet.id }}"
  • [(ngModel)]="pet.isPastOn"
  • (keydown.tab)="( ( isLast && addPet() ) || true )"
  • />
  • Is pasted-on?
  • </label>
  •  
  • <a (click)="removePet( index )" title="Remove Pet" class="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
  • instance, 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]="( ! petsForm.form.valid )">
  • Process Form
  • </button>
  •  
  • </form>

As you can see, inside the ngFor directive loop, we are providing NgModel-driven form inputs for each pet. The two-way data-binding is dead-simple; each input simply binds directly to the "pet" iteration item.

The most complicated part of this template is the fact that each control has to have a unique name (since they are all being [implicitly] registered with the parent NgForm). To create unique names, I'm using attribute-interpolation that leverages the locally-unique "id" property that we assigned in the component class.

Beyond the ngFor loop and the interpolation-based "name" attributes, the rest of this is pretty standard for a template-driven form. I am grabbing references to the exported NgModel directives in order to consume the state of each pet-name form-control. And, I'm grabbing the NgForm reference in order to disable the submit button when the form is not valid. The key take-away here is that all of this "just works", despite the fact that we have a dynamic number of pets to render.

Now, if we run this Angular app in the browser and add a few pets, we get the following output:


 
 
 

 
 Dynamic template-driven forms in Angular 7.2.7 allow for arbitrary form controls to be rendered. 
 
 
 

As you can see, each input tracks it's own state via a unique NgModel instance. And, when we submit the form, we get access to the NgForm parent as well as all of the view-model data that was being updated via the two-way data-binding.

As I said before, I've only ever used template-driven forms in Angular. I don't even know how reactive-forms work. And, I've never run into any limitations. In fact, this level of dynamic input rendering is more advanced than anything that I do on a day-to-day basis. But, it's good to know that even template-driven forms can be highly dynamic in Angular 7.2.7.



Reader Comments

Great little article. I have always used FormGroup & FormControls. I'm not sure whether that is reactive or template driven? I have often seen 'ngModel', in examples like yours, but have never really spent much time trying to understand this paradigm.

I have to say, the 'ngModel' paradigm looks simpler, inside the component. In the sense, you don't have to do stuff like:

this.barInput = new FormControl('', [
      Validators.required,
      Validators.minLength(1)
]);

And then:

this.fooForm = new FormGroup({
      barInput: this.barInput
});

However, it looks slightly more complex to set up, within the view.

What I can't get to grips with, is where your reference to the 'form' is, within the component.

It seems like somehow, magically:

this.form

Manages to reference:

#petsForm="ngForm"

But, maybe, it is all about the:

Pet[]

That is important here, and the form doesn't provide an explicit reference to itself.

Please could you enlighten me?

Reply to this Comment

Ben. I feel like a complete muppet.

I completely missed:

<ng-template ngFor let-pet [ngForOf]="form.pets" let-index="index" let-isLast="last">

Things make a lot more sense now, although the 'form' reference issue, still mystifies me.

I was trying to find a reference to:

pet

In the component and thinking, how on earth, does this example work!
Then I see the 'ngFor' loop!

My only excuse, is that I always view your blog on a mobile device, and I often miss things [important things, clearly], because of the limited amount of screen real estate.

And I love the:

let-isLast="last"

Never knew about this little gem.

What I really like about this paradigm, is that you can work directly with variables, that reference form inputs, inside the component.

I may actually start using this paradigm, although, old habits die hard!

Reply to this Comment

Just one last thing.

I see you use:

#nameControl="ngModel"

In only one of the form inputs.

What is the significance of this & what does it do?

Reply to this Comment

@Charles,

Sorry about the mobile-friendly (or lack thereof) view. It's on my list of things to fix :/

As far as the references, there's nothing in the Component class-code that actually references the template stuff. With the exception of the #petsForm stuff. So, the # concept in Angular Templates is that is gives you a template-local reference to that thing. So, if I have a div like so:

<div #myDiv> .... </div>

... then I can use myDiv elsewhere in the template to reference the actual HTMLDiv element (as part of the document object model concept). However, if an element also has a directive attached to it, you can use the # syntax to get a reference to the Directive instance. That's where something like:

<form #myForm="ngForm"> ... </form>

... comes into play. The ngForm is the "exported" reference for the NgForm instance. So, this is saying, "create a template-local variable called "myForm" that holds the NgForm instance attached to the form element.".

Likewise, the #nameControl="ngModel" is creating a template-local variable to the NgModel instance attached to the name input element.

That said, I don't actually use most of this fancier stuff on a day-to-day basis. Mostly, I just use the [(ngModel)] to automatically bind the form-controls to the Component view-model. So, the component never needs to reference the template elements -- it just consumes the this.form properties which have already been updated via ngModel by the time the form is submitted.

Reply to this Comment

Brilliant. Thanks for clearing these things up. I have to say, I now understand template driven forms, thanks to this article. The only reason I never used them before is that many of the articles, I read previously on this subject, seemed to over complicate this paradigm.

I mean, the guide on Angular Docs, really doesn't explain how everything links up, which is a shame, because I actually think this methodology is easier to use than reactive forms.

Reply to this Comment

I am a big fan of template-driven forms. They are more consistent and dynamic forms are much easier to implement in one place (template) instead of two (code and template).

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.