Using Dynamic Template-Driven Forms In Angular 7.2.7
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">
×
</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:
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
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:
And then:
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:
Manages to reference:
But, maybe, it is all about the:
That is important here, and the form doesn't provide an explicit reference to itself.
Please could you enlighten me?
Ben. I feel like a complete muppet.
I completely missed:
Things make a lot more sense now, although the 'form' reference issue, still mystifies me.
I was trying to find a reference to:
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:
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!
Just one last thing.
I see you use:
In only one of the form inputs.
What is the significance of this & what does it do?
@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 adiv
like so:... 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:... 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 theform
element.".Likewise, the
#nameControl="ngModel"
is creating a template-local variable to the NgModel instance attached to the nameinput
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 thethis.form
properties which have already been updated viangModel
by the time the form is submitted.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.
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).
@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.
@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.
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
@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 yourthead
has your labels:Of course, you could get fancy and using something like
css grid
instead oftable
; 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.
@Ben,
That's cool,
It solved my problem by using ….table.....thead…..tr...
Thanks,
Dilip
@Dilip,
Woot woot! Glad to have been helpful :D
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.
@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.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?
@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.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.
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.
@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.@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 itproductSelections
. Each one of theseproductSelections
items might then have a.selectedProduct
which is what we can tie to ourngModel
.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:Does that make sense at all?
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
hiiiiiiiiiiiiii and this is done