Model-Driven Forms
Learning Objectives
-
How to create a HTML form with a dynamic select box control.
-
How to define a form model on your component and link it to existing HTML form controls.
Form Setup
Whether we are template-driven or model-driven we need some basic form HTML to begin with.
Important
In model-driven forms, contrary to what you might think, the HTML for our form isn’t automatically created for us.
We still need to write the HTML that represents our form and then explicitly link the HTML form elements to code on our component.
We create a simple Bootstrap form with a first name, last name, email, password and language select box controls.

Copy<form novalidate>
<fieldset>
<div class="form-group">
<label>First Name</label>
<input type="text"
class="form-control">
</div>
<div class="form-group">
<label>Last Name</label>
<input type="text"
class="form-control">
</div>
</fieldset>
<div class="form-group">
<label>Email</label>
<input type="email"
class="form-control">
</div>
<div class="form-group">
<label>Password</label>
<input type="password"
class="form-control">
</div>
<div class="form-group">
<label>Language</label>
<select class="form-control">
<option value="">Please select a language</option>
</select>
</div>
</form>
We’ve added the novalidate
attribute to the form
element, by default browsers perform their own validation and show their own error popups. Since we want to handle the form validation ourselves we can switch off this behaviour by adding novalidate to the form element.
Note
Dynamic Select Controls
We want the select box to have a list of languages for the user to select.
We could hard code the languages in the HTML with <option>
tags but we want to make the list dynamic so we can easily add more languages later on. so we:
-
Add an array of languages to our component.
TypeScriptCopy
langs: string[] = [ 'English', 'French', 'German', ]
-
Use an
NgFor
loop to render these as options in the template.HTMLCopy
<select class="form-control"> <option value="">Please select a language</option> <option *ngFor="let lang of langs" [value]="lang"> (1) {{lang}} (2) </option> </select>
-
The options value
-
The options label
An option has a label and a value. The label is the text the user sees in the select box and the value is the data that’s stored for that label.
If we ask a select box what option has been selected it returns us the value, not the label.
To set the value of our select box we just bind to the input property of our option using the [value]
syntax.
Form Model
We represent the form as a model composed of instances of FormGroups
and FormControls
.
Let’s create the model for our form on our component, like so:
Copyimport { FormGroup, FormControl } from '@angular/forms';
.
.
.
class ModelFormComponent implements OnInit {
myform: FormGroup; (1)
ngOnInit() {
myform = new FormGroup({
name: new FormGroup({ (2)
firstName: new FormControl(), (3)
lastName: new FormControl(),
}),
email: new FormControl(),
password: new FormControl(),
language: new FormControl()
});
}
}
1 | myform is an instance of FormGroup and represents the form itself. |
2 | FormGroups can nest inside other FormGroups . |
3 | We create a FormControl for each template form control. |
The myform
property is an instance of a FormGroup
class and this represents our form itself.
Each form control in the template is represented by an instance of FormControl
. This encapsulates the state of the control, such as if it’s valid or invalid and even its current value.
These instances of FormControls
nest inside our top-level myform: FormGroup
, but what’s interesting is that we can nest FormGroups
inside other FormGroups
.
In our model we’ve grouped the firstName
and lastName
controls under a FormGroup
called name
which itself is nested under our top-level myform: FormGroup
.
Like the FormControl
instance, FormGroup
instances encapsulates the state of all of its inner controls, for example an instance of a FormGroup
is valid only if all of its inner controls are also valid.
Linking the Form Model to the Form Template
We have the HTML template for our form and the form model on our component, next up we need to link the two together.
We do this using a number of directives which are found in the ReactiveFormsModule
, so let’s import that and add it to the imports on our NgModule
.
Copyimport { ReactiveFormsModule } from '@angular/forms';
formGroup
Firstly we bind the <form>
element to our top-level myform
property using the formGroup
directive, like so:
Copy<form [formGroup]="myform"> ... </form>
Now we’ve linked the myform
model to the form template we have access to our myform
model in our template.
The value property of the myform
model returns the values of all of the controls as an object. We can use that with the json
pipe to output some useful debug information about our form, like so:
Copy<pre>{{myform.value | json}}</pre>
Running our application now prints out the below in the debug pre
tag:
Copy{
"name": {
"firstName": null,
"lastName": null
},
"email": null,
"password": null,
"language": null
}
Initially this seems quite exciting but as we enter values into each of the input fields in our form we would see that the model isn’t getting updated, the values remain null.
That’s because although we’ve linked the form
element to the myform
model this doesn’t automatically link each form control in the model with each form control in the template, we need to do this explicitly with the formControlName
and formGroupName
directives.
formGroupName
& formControlName
We use the formControlName
directive to map each form control in the template with a named form control in the model, like so:
Copy<div class="form-group">
<label>Email</label>
<input type="email"
class="form-control"
formControlName="email" (1)
required>
</div>
1 | This looks for a model form control called email in the top level of our myform model and links the element to that. |
We can also associate a group of template form controls to an instance of a form group on our model using formGroupName
directive.
Since our firstName
and lastName
controls are grouped under a form group called name
we’ll do just that.
Important
We then associate the fieldset
element with the form group called name
in our model like so:
Copy<fieldset formGroupName="name"> ... </fieldset>
Then inside our fieldset element we again use the formControlName
directive to map individual form controls in the template to form controls under the form group name
in our model.
In the end the template should look like this:
Copy<form [formGroup]="myform"> (1)
<fieldset formGroupName="name"> (2)
<div class="form-group">
<label>First Name</label>
<input type="text"
class="form-control"
formControlName="firstName" (3)
required>
</div>
<div class="form-group">
<label>Last Name</label>
<input type="text"
class="form-control"
formControlName="lastName" (3)
required>
</div>
</fieldset>
<div class="form-group">
<label>Email</label>
<input type="email"
class="form-control"
formControlName="email" (4)
required>
</div>
<div class="form-group">
<label>Password</label>
<input type="password"
class="form-control"
formControlName="password" (4)
required>
</div>
<div class="form-group">
<label>Language</label>
<select class="form-control"
formControlName="language" (4)
<option value="">Please select a language</option>
<option *ngFor="let lang of langs"
[value]="lang">{{lang}}
</option>
</select>
</div>
<pre>{{myform.value | json}}</pre>
</form>
1 | Use formGroup to bind the form to an instance of FormGroup on our component. |
2 | Use formGroupName to map to a child FormGroup of myform . |
3 | Use formControlName to bind to an instance of a FormControl , since these form controls are under a formGroupName of name , Angular will try and find the control in under myform['name'] . |
4 | Use formControlName to bind to an instance of a FormControl directly under myform . |
Now each form control in the template is mapped to form controls in our model and so as we type into the input elements myform.value
updates and our debug section at the bottom prints out the current value of the form.
Summary
In this lecture we created a simple HTML form. We created a form model on our component using the FormGroup
and FormControl
classes.
Then by using directives such as formGroup
, formControlName
and formGroupName
we linked our HTML form to our form model.
In the next lecture we will learn how to add validators to our forms to give visual feedback when the data entered is invalid.
Listing
Copyimport { NgModule, Component, Pipe, OnInit } from "@angular/core";
import {
ReactiveFormsModule,
FormsModule,
FormGroup,
FormControl,
Validators,
FormBuilder
} from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
@Component({
selector: "model-form",
template: `<!--suppress ALL -->
<div class="container">
<form novalidate
[formGroup]="myform"
(ngSubmit)="onSubmit()">
<fieldset formGroupName="name">
<div class="form-group">
<label>First Name</label>
<input type="text"
class="form-control"
formControlName="firstName">
</div>
<div class="form-group">
<label>Last Name</label>
<input type="text"
class="form-control"
formControlName="lastName">
</div>
</fieldset>
<div class="form-group" [ngClass]="{
'has-danger': email.invalid && email.dirty,
'has-success': email.valid && email.dirty
}">
<label>Email</label>
<input type="email"
class="form-control"
formControlName="email">
<pre>Dirty? {{ myform.controls.email.dirty }}</pre>
<pre>Pristine? {{ myform.controls.email.pristine }}</pre>
<pre>Touched? {{ myform.controls.email.touched }}</pre>
<pre>Valid? {{ myform.controls.email.valid }}</pre>
<pre>Invalid? {{ myform.controls.email.invalid }}</pre>
</div>
<div class="form-group" [ngClass]="{
'has-danger': password.invalid && (password.dirty || password.touched),
'has-success': password.valid && (password.dirty || password.touched)
}">
<label>Password</label>
<input type="password"
class="form-control"
formControlName="password">
<div class="form-control-feedback"
*ngIf="password.errors && (password.dirty || password.touched)">
<p *ngIf="password.errors.required">Password is required</p>
<p *ngIf="password.errors.minlength">Password must be {{password.errors.minlength.requiredLength}} characters long, we need another {{password.errors.minlength.requiredLength - password.errors.minlength.actualLength}} characters </p>
</div>
<pre>{{ password.errors | json }}</pre>
</div>
<div class="form-group">
<label>Language</label>
<select class="form-control"
formControlName="language">
<option value="">Please select a language</option>
<option *ngFor="let lang of langs"
[value]="lang">{{lang}}
</option>
</select>
</div>
<pre>{{myform.value | json}}</pre>
<button type="submit" class="btn btn-primary" >Submit</button>
</form>
</div>
`
})
class ModelFormComponent implements OnInit {
langs: string[] = ["English", "French", "German"];
myform: FormGroup;
firstName: FormControl;
lastName: FormControl;
email: FormControl;
password: FormControl;
language: FormControl;
createFormControls() {
this.firstName = new FormControl("", Validators.required);
this.lastName = new FormControl("", Validators.required);
this.email = new FormControl("", [
Validators.required,
Validators.pattern("[^ @]*@[^ @]*")
]);
this.password = new FormControl("", [
Validators.required,
Validators.minLength(8)
]);
this.language = new FormControl("", Validators.required);
}
createForm() {
this.myform = new FormGroup({
name: new FormGroup({
firstName: this.firstName,
lastName: this.lastName
}),
email: this.email,
password: this.password,
language: this.language
});
}
ngOnInit() {
this.createFormControls();
this.createForm();
}
onSubmit() {
if (this.myform.valid) {
console.log("Form Submitted!");
console.log(this.myform.value);
this.myform.reset();
}
}
}
@Component({
selector: "app",
template: `<model-form></model-form>`
})
class AppComponent {}
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [AppComponent, ModelFormComponent],
bootstrap: [AppComponent]
})
class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule);
Caught a mistake or want to contribute to the book? Edit this page on GitHub!

Advanced JavaScript
This unique course teaches you advanced JavaScript knowledge through a series of interview questions. Bring your JavaScript to the 2021's today.
Copy[🌲,🌳,🌴].push(🌲)If you find my courses useful, please consider planting a tree on my behalf to combat climate change. Just $4.50 will pay for 25 trees to be planted in my name. Plant a tree!