Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Mark Drew
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Mark Drew ( @markdrew )

Using Dynamic Template-Driven Forms In Angular 7.2.7

By 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.

Want to use code from this post? Check out the license.

Reader Comments

426 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?

426 Comments

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!

15,640 Comments

@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.

426 Comments

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.

1 Comments

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).

15,640 Comments

@All,

So, I finally took a look - my first - at Reactive Forms in Angular:

www.bennadel.com/blog/3603-my-first---and-possibly-last---look-at-reactive-forms-in-angular-7-2-13.htm

I tried to take this same demo (Pets) and update it to allow for N-number of Pets that each contain N-number of Nicknames (allowing for Arrays inside Arrays). I then implemented it with both template-driven forms and reactive forms. It's not really a fair comparison due to the fact that I have years of experience with template-driven forms and essentially no experience with reactive forms. But, template-driven forms seems much easier and less coupled to the view-model.

That said, I don't use a ton of validation in my Angular forms. And the vast majority of them are very simple. So, your mileage may vary greatly from mine.

15,640 Comments

@Oleksa,

That's exactly what I was thinking -- "one place". With template-driven forms, your view-model doesn't really know anything about the form. But, with reactive forms, your view-model and your template are very tightly coupled.

5 Comments

Hey Ben,

You have given great solution to save all the data at one go which will sent to server for persisting into database.

But my requirement is very simple. Just to show column headings at TOP and I tried hard to do it but not able to succeed.

Would you pl. guide in how to show column headings at top (Not as a placeholder inside the cell) so that it will give an excel like appearance to user?

Thanks,

Dilip

15,640 Comments

@Dilip,

When showing the headers at the top, the idea is fundamentally the same. The only difference would be that you would have some additional row at the top that would replace and / or augment the row-level placeholder / labels. This could be as old-school (and still very valid) as using a table element where your thead has your labels:

<table>
	<thead>
		<tr>
			<th>Type</th>
			<th>Name</th>
			<th>Age</th>
			<th>Passed</th>
		</tr>
	</thead>
	<tbody>
		<tr *ngFor="let pet in form.pets">

			<!-- Your other item markup. -->

		</tr>
	</tbody>
</table>

Of course, you could get fancy and using something like css grid instead of table; but, I don't see a huge reason to do that, unless you prefer it.

Not sure that is answering your question. But, I think you might be overthinking the problem.

1 Comments

Hey Ben I was facing issue while i was creating dynamic form with the help of *ngFor index. Index will be always unique but when i was adding two items and deleting first item, again when i was adding one more item, the all previous added item values gets removed.

15,640 Comments

@Prem,

That's a strange behavior. Is that something you can replicate in my demo? I just tried to Add several pets (in the demo), then remove the first one, and add some more - they all seem to show up.

Without knowing more about your issue, I am wondering if you are using the ngFor index as some sort of unique identifier that is possibly messing you up. Maybe you can add a ngForTrackBy function to help track item references? Just a shot in the dark.

1 Comments

thanks for the article.
My question is, since you are saving each form control as its own unique name, how can you save it (to the database) as an object, how would you group each age value to each name value and so on. since you end up with a formPets.controls list of all the unique values, they are not grouped. Are you able to get it from the formPets.controls? or do you have to use the form.pets array?

15,640 Comments

@Cm,

Great question! Ignoring the mechanics of how you save the actual data (ie, relational-database, document database, key-value store, etc), I would the view-model of the component as the source of truth. The form controls are only there to allow the user to interact with the view-model. As such, once the interaction is over, the view-model (in this case, this.form.pets) is the source of truth. That's what you was persist. And, that's what you would populate when pulling data out of the database as well.

5 Comments

Hi Ben,
I am using this functionality and it is working fine. I am able to persist the data into database at one go. It's great feature you have provided.

Now I am having one simple requirement to show age immediately when the Pet is selected, which means user need not have to either enter or select age. It should auto populate once Pet is selected.

Coming back to my requirement: I have saved Product details in database with Product_name, price and other fields related to that product.
Now I am using this form to select product and enter quantity. But the price is already in database which I have already fetched by using (ngModelChange) as per product selected by user.
The price is also populated as mat-select in the above form but user needs to click to select the Price. Here how to show price directly in the form instead of selecting it through mat-select.

This is very simple requirement which I already did using normal forms but not able to do using above form.

So would you pl. support in solving this issue.

1 Comments

Thanks for good tutorial on dynamically adding input fields in template driven forms.I had learned a lot from your blog.I am having a question here.After submitting form data I had stored it in subjects by defining a model class.While I am retrieving the submitting data to edit, I am again setting those stored values into the form.I am setting values to the form for editing by using petsForm.setValue({})).But I am facing a problem here.We can statically set data as we know how many pets we are adding but how can I use setValue dynamically for the number of pets added.Please help me with this.

15,640 Comments

@Ambareesh,

I am not familiar with the .setValue() function. I think that may be more relevant for reactive forms, not template-driven forms. With template-driven forms, you should just be able to define your "view-model" and then the HTML templates will just update to reflect your view-model. By using .setValue(), you may be trying to mix two different form paradigms.

But, again, I'm not familiar with the .setValue() method, so maybe I am misunderstanding.

15,640 Comments

@Dilip,

I'm having a little trouble understanding your context, so let me see if I can echo what I think I am hearing:

You have some sort of multi-row Product selection. And, there's a drop-down for the Product. And, when the user selects a Product, you want to show the associated Price in the row based on the Product selection?

If that's the case, let's consider what the ngFor is doing - it's looping over some data-structure that is used to render the rows in your table. Let's call it productSelections. Each one of these productSelections items might then have a .selectedProduct which is what we can tie to our ngModel.

Now, it sounds like you're using the (ngModelChanges) to listen for changes on that .selectedProduct. I think it that case then, could we then just use a .price property on the .selectedProduct? Such that your code would look something like this:

<div *ngFor="let selection of productSelections ; index = index">

	<select name="product_{{ index }}" [(ngModel)]="selection.selectedProduct">
		// ....
	</select>

	Price: {{ selection.selectedProduct.price }}
</div>

Does that make sense at all?

5 Comments

Hi Ben,
Thanks for the revert.
I tried using above code but not able to integrate with your original code to send all data to backend.

But no issues. I have used matAutocomplete without for loop to display price of selected product. In this case price is displayed as selection option l and user needs to select that price. (He can modify the value as well before saving).

Using this approach I am able to send all the products selected alognwith price, quantity and other fields to the backend and persist into db.

<ng-template ngFor let-rate [ngForOf]="formData.rates" let-index="index" let-isLast="last"
div class="pet" [class.pet--invalid]="(nameControl.touched && nameControl.invalid)"

select name="product_{{rate.id}}" [(ngModel)]="rate.productName" required>

{{product}}

<input [matAutocomplete]="auto2" type="number" name="price_{{ rate.id }}" [(ngModel)]="rate.price" required />
<mat-autocomplete #auto2="matAutocomplete">
<mat-option [value]="price" >{{price}}

/ng-template

....... Rest of the code to persist the data

Thanks for the support Ben,
Regards,
Dilip

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel