Angular presents two different methods for creating forms, template-driven (what we were used to in AngularJS 1.x), or reactive. We’re going to explore the absolute fundamentals of the template-driven Angular forms, covering ngForm
, ngModel
, ngModelGroup
, submit events, validation and error messages.
Before we begin, let’s clarify what “template-driven” forms mean from a high level.
When we talk about “template-driven” forms, we’ll actually be talking about the kind of forms we’re used to with AngularJS, whereby we bind directives and behaviour to our templates, and let Angular roll with it. Examples of these directives we’d use are ngModel
and perhaps required
, minlength
and so forth. On a high-level, this is what template-driven forms achieve for us - by specifying directives to bind our models, values, validation and so on, we are letting the template do the work under the scenes.
Table of contents
Form base and interface
I’m a poet and didn’t know it. Anyway, here’s the form structure that we’ll be using to implement our template-driven form:
<form novalidate>
<label>
<span>Full name</span>
<input
type="text"
name="name"
placeholder="Your full name">
</label>
<div>
<label>
<span>Email address</span>
<input
type="email"
name="email"
placeholder="Your email address">
</label>
<label>
<span>Confirm address</span>
<input
type="email"
name="confirm"
placeholder="Confirm your email address">
</label>
</div>
<button type="submit">Sign up</button>
</form>
We have three inputs, the first, the user’s name, followed by a grouped set of inputs that take the user’s email address.
Things we’ll implement:
- Bind to the user’s
name
,email
, andconfirm
inputs - Required validation on all inputs
- Show required validation errors
- Disabling submit until valid
- Submit function
Secondly, we’ll be implementing this interface:
// signup.interface.ts
export interface User {
name: string;
account: {
email: string;
confirm: string;
}
}
ngModule and template-driven forms
Before we even dive into template-driven forms, we need to tell our @NgModule
to use the FormsModule
from @angular/forms
:
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
...,
FormsModule
],
declarations: [...],
bootstrap: [...]
})
export class AppModule {}
You will obviously need to wire up all your other dependencies in the correct @NgModule
definitions.
Tip: use
FormsModule
for template-driven, andReactiveFormsModule
for reactive forms.
Template-driven approach
With template-driven forms, we can essentially leave a component class empty until we need to read/write values (such as submit and setting initial or future data). Let’s start with a base SignupFormComponent
and our above template:
// signup-form.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'signup-form',
template: `
<form novalidate>...</form>
`
})
export class SignupFormComponent {
constructor() {}
}
So, this is a typical component base that we need to get going. So what now? Well, to begin with, we don’t need to actually create any initial “data”, however, we will import our User
interface and assign it to a public variable to kick things off:
..
import { User } from './signup.interface';
@Component({...})
export class SignupFormComponent {
user: User = {
name: '',
account: {
email: '',
confirm: ''
}
};
}
Now we’re ready. So, what was the purpose of what we just did with public user: User;
? We’re binding a model that must adhere to the interface we created. Now we’re ready to tell our template-driven form what to do, to update and power that Object.
Binding ngForm and ngModel
Our first task is “Bind to the user’s name, email, and confirm inputs”.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
So let’s get started. What do we bind with? You guessed it, our beloved friends ngForm
and ngModel
. Let’s start with ngForm
.
<form novalidate #f="ngForm">
<label>
<span>Full name</span>
<input type="text" placeholder="Your full name">
</label>
</form>
In this <form>
we are exporting the ngForm
value to a public #f
variable, to which we can render out the value of the form.
Tip:
#f
is the exported form Object, so think of this as the generated output to your model’s input.
Let’s see what that would output for us when using f.value
:
{{ f.value | json }} // {}
There is a lot going on under the hood with
ngForm
which for the most part you do not need to know about to use template-driven forms but if you want more information, you can read about it here
Here we get an empty Object as our form value has no models, so nothing will be logged out. This is where we create nested bindings inside the same form so Angular can look out for them. Now we’re ready to bind some models, but first there are a few different ngModel
flavours we can roll with - so let’s break them down.
ngModel, [ngModel] and [(ngModel)]
Three different ngModel
syntaxes, are we going insane? Nah, this is awesome sauce, trust me. Let’s dive into each one.
- ngModel = if no binding or value is assigned,
ngModel
will look for aname
attribute and assign that value as a new Object key to the globalngForm
Object:
<form novalidate #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
ngModel>
...
</form>
However, this will actually throw an error as we need a name=""
attribute for all our form fields:
<form novalidate #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
name="name"
ngModel>
...
</form>
Tip:
ngModel
“talks to” the form, and binds the form value based on thename
attribute’s value. In this casename="name"
. Therefore it is needed.
Output from this at runtime:
{{ f.value | json }} // { name: '' }
Woo! Our first binding. But what if we want to set initial data?
- [ngModel] = one-way binding syntax, can set initial data from the bound component class, but will bind based on the
name="foo"
attribute, example:
Some initial data for our user
Object:
...
user: User = {
name: 'Todd Motto',
account: {
email: '',
confirm: ''
}
};
...
We can then simply bind user.name
from our component class to the [ngModel]
:
<form #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
name="name"
[ngModel]="user.name">
...
</form>
Output from this at runtime:
{{ f.value | json }} // { name: 'Todd Motto' }
So this allows us to set some initial data from this.user.name
, which automagically binds and outputs to f.value
Note: The actual value of
this.user.name
is never updated upon form changes, this is one-way dataflow. Form changes from ngModel are exported onto the respectivedf.value
properties.
It’s important to note that [ngModel]
is in fact a model setter. This is ideally the approach you’d want to take instead of two-way binding.
- [(ngModel)] = two-way binding syntax, can set initial data from the bound component class, but also update it:
<form #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
name="name"
[(ngModel)]="user.name">
...
</form>
Output from this (upon typing, both are reflected with changes):
{{ user | json }} // { name: 'Todd Motto' }
{{ f.value | json }} // { name: 'Todd Motto' }
This isn’t such a great idea, as we now have two separate states to keep track of inside the form component. Ideally, you’d implement one-way databinding and let the ngForm
do all the work here.
Side note, these two implementations are equivalents:
<input [(ngModel)]="user.name">
<input [ngModel]="user.name"` (ngModelChange)="user.name = $event">
The [(ngModel)]
syntax is sugar syntax for masking the (ngModelChange)
event setter, that’s it.
ngModels and ngModelGroup
So now we’ve covered some intricacies of ngForm
and ngModel
, let’s hook up the rest of the template-driven form. We have a nested account
property on our user
Object, that accepts an email
value and confirm
value. To wire these up, we can introduce ngModelGroup
to essentially created a nested group of ngModel
friends:
<form novalidate #f="ngForm">
<label>
<span>Full name</span>
<input
type="text"
placeholder="Your full name"
name="name"
ngModel>
</label>
<div ngModelGroup="account">
<label>
<span>Email address</span>
<input
type="email"
placeholder="Your email address"
name="email"
ngModel>
</label>
<label>
<span>Confirm address</span>
<input
type="email"
placeholder="Confirm your email address"
name="confirm"
ngModel>
</label>
</div>
<button type="submit">Sign up</button>
</form>
This creates a nice structure based on the representation in the DOM that pseudo-looks like this:
ngForm -> '#f'
ngModel -> 'name'
ngModelGroup -> 'account'
-> ngModel -> 'email'
-> ngModel -> 'confirm'
Which matches up nicely with our this.user
interface, and the runtime output:
// { name: 'Todd Motto', account: { email: '', confirm: '' } }
{{ f.value | json }}
This is why they’re called template-driven. So what next? Let’s add some submit functionality.
Template-driven submit
To wire up a submit event, all we need to do is add a ngSubmit
event directive to our form:
<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
...
</form>
Notice how we just passed f
into the onSubmit()
? This allows us to pull down various pieces of information from our respective method on our component class:
export class SignupFormComponent {
user: User = {...};
onSubmit({ value, valid }: { value: User, valid: boolean }) {
console.log(value, valid);
}
}
Here we’re using Object destructuring to fetch the value
and valid
properties from that #f
reference we exported and passed into onSubmit
. The value
is basically everything we saw from above when we parsed out the f.value
in the DOM. That’s literally it, you’re free to pass values to your backend API.
Template-driven error validation
Oh la la, the fancy bits. To roll out some validation is actually very similar to how we’d approach this in AngularJS 1.x as well (hooking into individual form field validation properties).
First off, let’s start simple and disable our submit button until the form’s valid:
<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
...
<button type="submit" [disabled]="f.invalid">Sign up</button>
</form>
Here we’re binding to the disabled
property of the button, and setting it to true
dynamically when f.invalid
is true. When the form is valid
, the submit curse shall be lifted and allow submission.
Next, the required
attributes on each <input>
:
<form novalidate #f="ngForm">
<label>
...
<input
...
ngModel
required>
</label>
<div ngModelGroup="account">
<label>
...
<input
...
name="email"
ngModel
required>
</label>
<label>
...
<input
...
name="confirm"
ngModel
required>
</label>
</div>
<button type="submit">Sign up</button>
</form>
So, onto displaying errors. We have access to #f
, which we can log out as f.value
. Now, one thing we haven’t touched on is the inner workings of these magical ngModel
and ngModelGroup
directives. They actually, internally, spin up their own Form Controls and other gadgets. When it comes to referencing these controls, we must use the .controls
property on the Object. Let’s say we want to show if there are any errors on the name
property of our form:
<form novalidate #f="ngForm">
{{ f.controls.name?.errors | json }}
</form>
Note how we’ve used f.controls.name
here, followed by the ?.errors
. This is a safeguard mechanism to essentially tell Angular that this property might not exist yet, but render it out if it does. Similarly if the value becomes null
or undefined
again, the error is not thrown.
Tip:
?.prop
is called the “Safe navigation operator”
Let’s move onto setting up an error field for our form by adding the following error box to our name
input:
<div *ngIf="f.controls.name?.required" class="error">
Name is required
</div>
Okay, this looks a little messy and is error prone if we begin to extend our forms with more nested Objects and data. Let’s fix that by exporting a new #userName
variable from the input itself based on the ngModel
Object:
<label>
...
<input
...
#userName="ngModel"
required>
</label>
<div *ngIf="userName.errors?.required" class="error">
Name is required
</div>
Now, this shows the error message at runtime, which we don’t want to alarm users with. What we can do is add some userName.touched
into the mix:
<div *ngIf="userName.errors?.required && userName.touched" class="error">
Name is required
</div>
And we’re good.
Tip: The
touched
property becomestrue
once the user has blurred the input, which may be a relevant time to show the error if they’ve not filled anything out
Let’s add a minlength
attribute just because:
<input
type="text"
placeholder="Your full name"
name="name"
ngModel
#userName="ngModel"
minlength="2"
required>
We can then replicate this validation setup now on the other inputs:
<!-- name -->
<div *ngIf="userName.errors?.required && userName.touched" class="error">
Name is required
</div>
<div *ngIf="userName.errors?.minlength && userName.touched" class="error">
Minimum of 2 characters
</div>
<!-- account: { email, confirm } -->
<div *ngIf="userEmail.errors?.required && userEmail.touched" class="error">
Email is required
</div>
<div *ngIf="userConfirm.errors?.required && userConfirm.touched" class="error">
Confirming email is required
</div>
Tip: it may be ideal to minimise model reference exporting and inline validation, and move the validation to the
ngModelGroup
Let’s explore cutting down our validation for email
and confirm
fields (inside our ngModelGroup
) and create a group-specific validation messages if that makes sense for the group of fields.
To do this, we can export a reference to the ngModelGroup
by using #userAccount="ngModelGroup"
, and adjusting our validation messages to the following:
<div ngModelGroup="account" #userAccount="ngModelGroup">
<label>
<span>Email address</span>
<input
type="email"
placeholder="Your email address"
name="email"
ngModel
required>
</label>
<label>
<span>Confirm address</span>
<input
type="email"
placeholder="Confirm your email address"
name="confirm"
ngModel
required>
</label>
<div *ngIf="userAccount.invalid && userAccount.touched" class="error">
Both emails are required
</div>
</div>
We’ve also removed both #userEmail
and #userConfirm
references.
Final code
We’re all done for this tutorial. Keep an eye out for custom validation, reactive forms and much more. Here’s the fully working final code from what we’ve covered:
Angular (v2+) presents two different methods for creating forms, template-driven (what we were used to in AngularJS 1.x), or reactive. We’re going to explore the absolute fundamentals of the template-driven Angular forms, covering ngForm
, ngModel
, ngModelGroup
, submit events, validation and error messages.
High-level terminology
Before we begin, let’s clarify what “template-driven” forms mean from a high level.
Template-driven
When we talk about “template-driven” forms, we’ll actually be talking about the kind of forms we’re used to with AngularJS, whereby we bind directives and behaviour to our templates, and let Angular roll with it. Examples of these directives we’d use are ngModel
and perhaps required
, minlength
and so forth. On a high-level, this is what template-driven forms achieve for us - by specifying directives to bind our models, values, validation and so on, we are letting the template do the work under the scenes.
Form base and interface
I’m a poet and didn’t know it. Anyway, here’s the form structure that we’ll be using to implement our template-driven form:
<label>
<span>Full name</span>
</label>
<div>
<label>
<span>Email address</span>
</label>
<label>
<span>Confirm address</span>
</label>
</div>
<button type="submit">Sign up</button>
We have three inputs, the first, the user’s name, followed by a grouped set of inputs that take the user’s email address.
Things we’ll implement:
- Bind to the user’s
name
,email
, andconfirm
inputs - Required validation on all inputs
- Show required validation errors
- Disabling submit until valid
- Submit function
Secondly, we’ll be implementing this interface:
// signup.interface.ts
export interface User {
name: string;
account: {
email: string;
confirm: string;
}
}
ngModule and template-driven forms
Before we even dive into template-driven forms, we need to tell our @NgModule
to use the FormsModule
from @angular/forms
:
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
...,
FormsModule
],
declarations: [...],
bootstrap: [...]
})
export class AppModule {}
You will obviously need to wire up all your other dependencies in the correct @NgModule
definitions.
Tip: use
FormsModule
for template-driven, andReactiveFormsModule
for reactive forms.
Template-driven approach
With template-driven forms, we can essentially leave a component class empty until we need to read/write values (such as submit and setting initial or future data). Let’s start with a base SignupFormComponent
and our above template:
// signup-form.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'signup-form',
template: `
<form novalidate>...</form>
`
})
export class SignupFormComponent {
constructor() {}
}
So, this is a typical component base that we need to get going. So what now? Well, to begin with, we don’t need to actually create any initial “data”, however, we will import our User
interface and assign it to a public variable to kick things off:
import { User } from './signup.interface';
@Component({...})
export class SignupFormComponent {
user: User = {
name: '',
account: {
email: '',
confirm: ''
}
};
}
Now we’re ready. So, what was the purpose of what we just did with public user: User;
? We’re binding a model that must adhere to the interface we created. Now we’re ready to tell our template-driven form what to do, to update and power that Object.
Binding ngForm and ngModel
Our first task is “Bind to the user’s name, email, and confirm inputs”.
So let’s get started. What do we bind with? You guessed it, our beloved friends ngForm
and ngModel
. Let’s start with ngForm
.
<label>
<span>Full name</span>
</label>
In this <form>
we are exporting the ngForm
value to a public #f
variable, to which we can render out the value of the form.
Tip:
#f
is the exported form Object, so think of this as the generated output to your model’s input.
Let’s see what that would output for us when using f.value
:
{{ f.value | json }} // {}
There is a lot going on under the hood with
ngForm
which for the most part you do not need to know about to use template-driven forms but if you want more information, you can read about it here
Here we get an empty Object as our form value has no models, so nothing will be logged out. This is where we create nested bindings inside the same form so Angular can look out for them. Now we’re ready to bind some models, but first there are a few different ngModel
flavours we can roll with - so let’s break them down.
ngModel, [ngModel] and [(ngModel)]
Three different ngModel
syntaxes, are we going insane? Nah, this is awesome sauce, trust me. Let’s dive into each one.
- ngModel = if no binding or value is assigned,
ngModel
will look for aname
attribute and assign that value as a new Object key to the globalngForm
Object:
<form novalidate #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
ngModel>
...
</form>
However, this will actually throw an error as we need a name=""
attribute for all our form fields:
<form novalidate #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
name="name"
ngModel>
...
</form>
Tip:
ngModel
“talks to” the form, and binds the form value based on thename
attribute’s value. In this casename="name"
. Therefore it is needed.
Output from this at runtime:
{{ f.value | json }} // { name: '' }
Woo! Our first binding. But what if we want to set initial data?
- [ngModel] = one-way binding syntax, can set initial data from the bound component class, but will bind based on the
name="foo"
attribute, example:
Some initial data for our user
Object:
...
user: User = {
name: 'Todd Motto',
account: {
email: '',
confirm: ''
}
};
...
We can then simply bind user.name
from our component class to the [ngModel]
:
<form #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
name="name"
[ngModel]="user.name">
...
</form>
Output from this at runtime:
{{ f.value | json }} // { name: 'Todd Motto' }
So this allows us to set some initial data from this.user.name
, which automagically binds and outputs to f.value
Note: The actual value of
this.user.name
is never updated upon form changes, this is one-way dataflow. Form changes from ngModel are exported onto the respectivedf.value
properties.
It’s important to note that [ngModel]
is in fact a model setter. This is ideally the approach you’d want to take instead of two-way binding.
- [(ngModel)] = two-way binding syntax, can set initial data from the bound component class, but also update it:
<form #f="ngForm">
...
<input
type="text"
placeholder="Your full name"
name="name"
[(ngModel)]="user.name">
...
</form>
Output from this (upon typing, both are reflected with changes):
{{ user | json }} // { name: 'Todd Motto' }
{{ f.value | json }} // { name: 'Todd Motto' }
This isn’t such a great idea, as we now have two separate states to keep track of inside the form component. Ideally, you’d implement one-way databinding and let the ngForm
do all the work here.
Side note, these two implementations are equivalents:
<input [(ngModel)]="user.name">
<input [ngModel]="user.name"` (ngModelChange)="user.name = $event">
The [(ngModel)]
syntax is sugar syntax for masking the (ngModelChange)
event setter, that’s it.
ngModels and ngModelGroup
So now we’ve covered some intricacies of ngForm
and ngModel
, let’s hook up the rest of the template-driven form. We have a nested account
property on our user
Object, that accepts an email
value and confirm
value. To wire these up, we can introduce ngModelGroup
to essentially created a nested group of ngModel
friends:
<form novalidate #f="ngForm">
<label>
<span>Full name</span>
<input
type="text"
placeholder="Your full name"
name="name"
ngModel>
</label>
<div ngModelGroup="account">
<label>
<span>Email address</span>
<input
type="email"
placeholder="Your email address"
name="email"
ngModel>
</label>
<label>
<span>Confirm address</span>
<input
type="email"
placeholder="Confirm your email address"
name="confirm"
ngModel>
</label>
</div>
<button type="submit">Sign up</button>
</form>
This creates a nice structure based on the representation in the DOM that pseudo-looks like this:
ngForm -> '#f'
ngModel -> 'name'
ngModelGroup -> 'account'
-> ngModel -> 'email'
-> ngModel -> 'confirm'
Which matches up nicely with our this.user
interface, and the runtime output:
// { name: 'Todd Motto', account: { email: '', confirm: '' } }
{{ f.value | json }}
This is why they’re called template-driven. So what next? Let’s add some submit functionality.
Template-driven submit
To wire up a submit event, all we need to do is add a ngSubmit
event directive to our form:
<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
...
</form>
Notice how we just passed f
into the onSubmit()
? This allows us to pull down various pieces of information from our respective method on our component class:
export class SignupFormComponent {
user: User = {...};
onSubmit({ value, valid }: { value: User, valid: boolean }) {
console.log(value, valid);
}
}
Here we’re using Object destructuring to fetch the value
and valid
properties from that #f
reference we exported and passed into onSubmit
. The value
is basically everything we saw from above when we parsed out the f.value
in the DOM. That’s literally it, you’re free to pass values to your backend API.
Template-driven error validation
Oh la la, the fancy bits. To roll out some validation is actually very similar to how we’d approach this in AngularJS 1.x as well (hooking into individual form field validation properties).
First off, let’s start simple and disable our submit button until the form’s valid:
<form novalidate (ngSubmit)="onSubmit(f)" #f="ngForm">
...
<button type="submit" [disabled]="f.invalid">Sign up</button>
</form>
Here we’re binding to the disabled
property of the button, and setting it to true
dynamically when f.invalid
is true. When the form is valid
, the submit curse shall be lifted and allow submission.
Next, the required
attributes on each ``:
<form novalidate #f="ngForm">
<label>
...
<input
...
ngModel
required>
</label>
<div ngModelGroup="account">
<label>
...
<input
...
name="email"
ngModel
required>
</label>
<label>
...
<input
...
name="confirm"
ngModel
required>
</label>
</div>
<button type="submit">Sign up</button>
</form>
So, onto displaying errors. We have access to #f
, which we can log out as f.value
. Now, one thing we haven’t touched on is the inner workings of these magical ngModel
and ngModelGroup
directives. They actually, internally, spin up their own Form Controls and other gadgets. When it comes to referencing these controls, we must use the .controls
property on the Object. Let’s say we want to show if there are any errors on the name
property of our form:
<form novalidate #f="ngForm">
{{ f.controls.name?.errors | json }}
</form>
Note how we’ve used f.controls.name
here, followed by the ?.errors
. This is a safeguard mechanism to essentially tell Angular that this property might not exist yet, but render it out if it does. Similarly if the value becomes null
or undefined
again, the error is not thrown.
Tip:
?.prop
is called the “Safe navigation operator”
Let’s move onto setting up an error field for our form by adding the following error box to our name
input:
<div class="error">
Name is required
</div>
Okay, this looks a little messy and is error prone if we begin to extend our forms with more nested Objects and data. Let’s fix that by exporting a new #userName
variable from the input itself based on the ngModel
Object:
<label>
...
</label>
<div class="error">
Name is required
</div>
Now, this shows the error message at runtime, which we don’t want to alarm users with. What we can do is add some userName.touched
into the mix:
<div class="error">
Name is required
</div>
And we’re good.
Tip: The
touched
property becomestrue
once the user has blurred the input, which may be a relevant time to show the error if they’ve not filled anything out
Let’s add a minlength
attribute just because:
<input
type="text"
placeholder="Your full name"
name="name"
ngModel
#userName="ngModel"
minlength="2"
required>
We can then replicate this validation setup now on the other inputs:
<!-- name -->
<div class="error">
Name is required
</div>
<div class="error">
Minimum of 2 characters
</div>
<!-- account: { email, confirm } -->
<div class="error">
Email is required
</div>
<div class="error">
Confirming email is required
</div>
Tip: it may be ideal to minimise model reference exporting and inline validation, and move the validation to the
ngModelGroup
Let’s explore cutting down our validation for email
and confirm
fields (inside our ngModelGroup
) and create a group-specific validation messages if that makes sense for the group of fields.
To do this, we can export a reference to the ngModelGroup
by using #userAccount="ngModelGroup"
, and adjusting our validation messages to the following:
<div>
<label>
<span>Email address</span>
</label>
<label>
<span>Confirm address</span>
</label>
<div class="error">
Both emails are required
</div>
</div>
We’ve also removed both #userEmail
and #userConfirm
references.
Final code
We’re all done for this tutorial. Keep an eye out for custom validation, reactive forms and much more. Here’s the fully working final code from what we’ve covered: