Template-driven Forms in Angular
Angular comes with three different ways of building forms in our applications. There’s the template-driven approach which allows us to build forms with very little to none application code required, then there’s the model-driven or reactive approach using low level APIs, which makes our forms testable without a DOM being required, and last but not least, we can build our forms model-driven but with a higher level API called the FormBuilder
.
Hearing all these different solutions, it’s kind of natural that there are also probably many different tools to reach the goal. This can be sometimes confusing and with this article we want to clarify a subset of form directives by focussing on template-driven forms in Angular.
Activating new Form APIs
The form APIs have changed in RC2 and in order to not break all existing apps that have been built with RC1 and use forms, these new APIs are added on top of the existing ones. That means, we need to tell Angular explicitly which APIs we want to use (of course, this will go away in the final release).
In order to activate the new APIs we need to import Angular’s FormsModule
into our application module.
Here’s what that could look like:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export AppModule {}
We then bootstrap our application module like this:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
ngForm
Directive
Let’s start off with a simple login form that asks for some user data:
<form>
<label>Firstname:</label>
<input type="text">
<label>Lastname:</label>
<input type="text">
<label>Street:</label>
<input type="text">
<label>Zip:</label>
<input type="text">
<label>City:</label>
<input type="text">
<button type="submit">Submit</button>
</form>
We’ve probably all built one of those forms several times. A simple HTML form with input controls for a name and an address of a user. Nothing special going on here.
What we can’t see here is that Angular comes with a directive ngForm
that matches the <form>
selector, so in fact, our form element already has an instance of ngForm
applied. ngForm
is there for a reason. It provides us information about the current state of the form including:
- A JSON representation of the form value
- Validity state of the entire form
Accessing the ngForm
instance
Angular comes with a very convenient way of exposing directive instances in a component’s template using the exportAs
property of the directive metadata. For example, if we’d build a directive draggable
, we could expose an instance to the template via the name draggable
like so:
@Directive({
selector: '[draggable]',
exportAs: 'draggable'
})
class Draggable {
...
}
And then, in the template where it’s used, we can simply ask for it using Angular’s local variable mechanism:
<div draggable #myDraggable="draggable">I'm draggable!</div>
From this point on myDraggable
is a reference to an instance of Draggable
and we can use it throughout our entire template as part of other expressions.
You might wonder why that’s interesting. Well, it turns out that ngForm
directive is exposed as ngForm
, which means we can get an instance of our form without writing any application code like this:
<form #form="ngForm">
...
</form>
Submitting a form and Accessing its value
We can now use form
to access the form’s value and it’s validity state. Let’s log the value of the form when it’s submitted. All we have to do is to add a handler to the form’s submit
event and pass it the form’s value. In fact, there’s a property on the ngForm
instance called value
, so this is what it’d look like:
<form #form="ngForm" (submit)="logForm(form.value)">
...
</form>
Even though this would work, it turns out that there’s another output event ngForm
fires when it’s submitted. It’s called ngSubmit
, and it seems to be doing exactly the same as submit
at a first glance. However, ngSubmit
ensures that the form doesn’t submit when the handler code throws (which is the default behaviour of submit
) and causes an actual http post request. Let’s use ngSubmit
instead as this is the best practice:
<form #form="ngForm" (ngSubmit)="logForm(form.value)">
...
</form>
In addition, we might have a component that looks something like this:
@Component({
selector: 'app',
template: ...
})
class App {
logForm(value: any) {
console.log(value);
}
}
Running this code makes us realize, that the form’s value is an empty object. This seems natural, because there’s nothing in our component’s template yet, that tells the form that the input controls are part of this form. We need a way to register them. This is where ngModel
comes into play.
ngModel
Directive
In order to register form controls on an ngForm
instance, we use the ngModel
directive. In combination with a name
attribute, ngModel
creates a form control abstraction for us behind the scenes. Every form control that is registered with ngModel
will automatically show up in form.value
and can then easily be used for further post processing.
Let’s give our form object some structure and register our form controls:
<form #form="ngForm" (ngSubmit)="logForm(form.value)">
<label>Firstname:</label>
<input type="text" name="firstname" ngModel>
<label>Lastname:</label>
<input type="text" name="lastname" ngModel>
<label>Street:</label>
<input type="text" name="street" ngModel>
<label>Zip:</label>
<input type="text" name="zip" ngModel>
<label>City:</label>
<input type="text" name="city" ngModel>
<button type="submit">Submit</button>
</form>
Great! If we now enter some values and submit the form, we’ll see that our application will log something like this:
{
firstname: 'Pascal',
lastname: 'Precht',
street: 'thoughtram Road',
zip: '00011',
city: 'San Francisco'
}
Isn’t that cool? We can basically take this JSON object as it is and send it straight to a remote server for whatever we want to do with it. Oh wait, what if we actually want to have some more structure and make our form object look like this?
{
name: {
firstname: 'Pascal',
lastname: 'Precht',
},
address: {
street: 'thoughtram Road',
zip: '00011',
city: 'San Francisco'
}
}
Do we now have to wire everything together by hand when the form is submitted? Nope! Angular has us covered - introducing ngModelGroup
.
ngModelGroup
Directive
ngModelGroup
enables us to semantically group our form controls. In other words, there can’t be a control group without controls. In addition to that, it also tracks validity state of the inner form controls. This comes in very handy if we want to check the validity state of just a sub set of the form.
And if you now think: “Wait, isn’t a form then also just a control group”, then you’re right my friend. A form is also just a control group.
Let’s semantically group our form control values with ngModelGroup
:
<fieldset ngModelGroup="name">
<label>Firstname:</label>
<input type="text" name="firstname" ngModel>
<label>Lastname:</label>
<input type="text" name="lastname" ngModel>
</fieldset>
<fieldset ngModelGroup="address">
<label>Street:</label>
<input type="text" name="street" ngModel>
<label>Zip:</label>
<input type="text" name="zip" ngModel>
<label>City:</label>
<input type="text" name="city" ngModel>
</fieldset>
As you can see, all we did was wrapping form controls in <fieldset>
elements and applied ngModelGroup
directives to them. There’s no specific reason we used <fieldset>
elements. We could’ve used <div>
s too. The point is, that there has to be an element, where we add ngModelGroup
so it will be registered at our ngForm
instance.
We can see that it worked out by submitting the form and looking at the output:
{
name: {
firstname: 'Pascal',
lastname: 'Precht',
},
address: {
street: 'thoughtram Road',
zip: '00011',
city: 'San Francisco'
}
}
Awesome! We now get the wanted object structure out of our form without writing any application code.
What about ngModel with expressions?
So ngModel
is the thing in Angular that implements two-way data binding. It’s not the only thing that does that, but it’s in most cases the directive we want to use for simple scenarios. So far we’ve used ngModel
as attribute directive without any value, but we might want to use it with an expression to bind an existing domain model to our form. This, of course works out of the box!
There are two ways to handle this, depending on what we want to do. One thing we can do is to apply property binding using the brackets syntax, so we can bind an existing value to a form control using one-way binding:
<fieldset ngModelGroup="name">
<label>Firstname:</label>
<input type="text" name="firstname" [ngModel]="firstname">
<label>Lastname:</label>
<input type="text" name="lastname" [ngModel]="lastname">
</fieldset>
Whereas the corresponding model could look something like this:
@Component({
selector: 'app',
template: ...
})
class App {
firstname = 'Pascal';
lastname = 'Precht';
logForm(value: any) {
console.log(value);
}
}
In addition, we can of course use ngModel
and two-way data binding, in case we want to reflect the model values somewhere else in our template:
<fieldset ngModelGroup="name">
<label>Firstname:</label>
<input type="text" name="firstname" [(ngModel)]="firstname">
<p>You entered {{firstname}}</p>
<label>Lastname:</label>
<input type="text" name="lastname" [(ngModel)]="lastname">
<p>You entered {{lastname}}</p>
</fieldset>
This just simply works as expected.
More to cover
Of course, this is really just the tip of the ice berg when it comes to building forms. We haven’t talked about validation yet and how to display error messages when the validity state of a form control or a control group changes. We will talk about these and other things in future articles. However, if you’re interested in how to build a custom validator in Angular, checkout this article.
Watch out for more articles on forms in Angular!