In this post we will see how the Angular 2 Forms API works and how it can be used to build complex forms. We will go through the following topics, based on examples available in this repository:
A large category of frontend applications are very form-intensive, specially in the case of enterprise development. Many of these applications are basically just huge forms, spanning multiple tabs and dialogs and with non-trivial validation business logic.
Every form-intensive application has to provide answers for the following problems:
All of these are non-trivial tasks that are similar across applications, and as such could benefit from a framework.
The Angular 2 framework provides us two alternative strategies for handling forms, and its up to us to decide what suits our project best.
Angular 1 tackles forms via the famous ng-model
directive (read more about it in this post).
The instantaneous two-way data binding of ng-model
in Angular 1 is really a life-saver as it allows to transparently keep in sync a form with a view model. Forms built with this directive can only be tested in an end to end test because this requires the presence of a DOM, but still this mechanism is very useful and simple to understand.
Angular 2 provides a similar mechanism similarly called ngModel
, that allow us to build what is now called Template-Driven forms. Let's take a look at a form built using it:
<section class="sample-app-content">
<h1>Template-driven Form Example:</h1>
<form #f="ngForm" (ngSubmit)="onSubmitTemplateBased()">
<p>
<label>First Name:</label>
<input type="text"
[(ngModel)]="vm.firstName" required>
</p>
<p>
<label>Password:</label>
<input type="password"
[(ngModel)]="vm.password" required>
</p>
<p>
<button type="submit" [disabled]="!f.valid">Submit</button>
</p>
</form>
</section>
There is actually quite a lot going on in this simple example. What we have done here is to declare a simple form with two controls: first name and password, both of which are required.
The form will trigger the controller method onSubmitTemplateBased
on submission, but the submit button is only enabled if both required fields are filled in. But that is only a small part of what is going on here.
Notice the use of [(ngModel)]
, this notation emphasizes that the two form controls are bi-directionally binded with a view model variable, named in Angular 1 style as simply vm
.
More than that, when the user clicks a required field, the field is shown in red until the user types in something. Angular is actually tracking three form field states for us and applying CSS classes for each to the form and its controls:
These CSS state classes are very useful for styling form error states.
Angular is actually tracking the validity state of the whole form as well, using it to enable / disable the submit button. This functionality is actually common to both template-driven and form-driven forms.
Let's take a look at the controller associated to this view to see how all this commonly used form logic is implemented:
@Component({
selector: "template-driven-form",
templateUrl: 'template-driven-form.html'
})
export class TemplateDrivenForm {
vm: Object = {};
onSubmitTemplateBased() {
console.log(this.vm);
}
}
Not much to see here! We only have a declaration for a view model object vm
, and an event handler used by ngSubmit
.
All the very useful functionality of tracking form errors and registering validators is taken care without any special configuration!
The way that this works, is that there is a set of implicitly defined form directives (named FORM_DIRECTIVES
) that is being applied to the view. Angular will automatically apply a NgForm
directive to the form element in a transparent way, making it effectively a ControlGroup
.
If by some reason you don't want this you can always disable this functionality by adding ngNoForm
as a form attribute.
Furthermore, each ngControl
will also get applied a directive that will register itself with the control group, and validators are registered if elements like required
or maxlength
are applied to the ngControl
.
The presence of [(ngModel)]
will also register a directive that will plug-in the bidirectional binding between form and view model, and in the end there is not much more to do at the level of the controller.
This is why this is called template-driven forms, because all the validation logic is declared in the template. This is nearly identical to the way that this is done in Angular 1.
In this simple example we cannot really see it, but keeping the template as the source of all form validation truth is something that can become pretty hairy rather quickly.
As we add more and more validator tags to a field or when we start adding complex cross-field validations the readability of the form decreases, to the point where it will be harder to hand it off to a web designer.
The up-side of this way of handling forms is its simplicity, and its probably more than enough to build a very large range of forms.
On the downside the form validation logic cannot be unit tested. The only way to test this logic is to run an end to end test with a browser, for example using a headless browser like PhantomJs
.
There is nothing wrong with template driven forms, but from a programming technique point of view its a solution that promotes mutability.
Each form has a state that can be updated by many different interactions and its up to the application developer to manage that state and prevent it from getting corrupted. This can get hard to do for very large forms and can introduce a whole category of potential bugs.
Inspired by what was going on in the React world, the Angular 2 team added a different alternative for managing forms, so let's go through it.
A model driven form looks on the surface pretty much like a template driven form. Let's take our previous example and re-write it:
<section class="sample-app-content">
<h1>Model-based Form Example:</h1>
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
<p>
<label>First Name:</label>
<input type="text" ngControl="firstName">
</p>
<p>
<label>Password:</label>
<input type="password" ngControl="password">
</p>
<p>
<button type="submit" [disabled]="!form.valid">Submit</button>
</p>
</form>
</section>
There are a couple of differences here. first there is a ngFormModel
directive applied to the whole form, binding it to a controller variable named form
.
Notice also that the required
validator attribute is not applied to the form controls. This means the validation logic must be somewhere in the controller, where it can be unit tested.
There is a bit more going on in the controller of a Model Driven Form, let's take a look at the controller for the form above:
@Component({
selector: "model-driven-form",
templateUrl: 'model-driven-form.html'
})
export class ModelDrivenForm {
form: ControlGroup;
firstName: Control = new Control("", Validators.required);
constructor(fb: FormBuilder) {
this.form = fb.group({
"firstName": this.firstName,
"password":["", Validators.required]
});
}
onSubmitModelBased() {
console.log("model-based form submitted");
console.log(this.form);
}
}
We can see that the form is really just a ControlGroup
, which keeps track of the global validity state. The controls themselves can either be instantiated individually or defined using a simplified array notation using the form builder.
In the array notation, the first element of the array is the initial value of the control, and the remaining elements are the control's validators. In this case both controls are made mandatory via the Validators.required
built-in validator.
ngModel
? Note that ngModel
can still be used with model driven forms. Its just that the form value would be available in two different places: the view model and the ControlGroup
, which could potentially lead to some confusions.
You are probably wondering what we gained here. On the surface there is already a big gain:
We can now unit test the form validation logic !
We can do that just by instantiating the class, setting some values in the form controls and perform assertions against the form global valid state and the validity state of each control.
But this is really just the tip of the iceberg. The ControlGroup
and
Control
classes provide an API that allows to build UIs using a completely different programming style known as Functional Reactive Programming.
This deserves it's own blog post, but the main point is that the form controls and the form itself are now Observables. You can think of observables simply as streams.
This mean that both the controls and the whole form itself can be viewed as a continuous stream of values, that can be subscribed to and processed using commonly used functional primitives.
For example, its possible to subscribe to the form stream of values using the Observable API like this:
this.form.valueChanges
.map((value) => {
value.firstName = value.firstName.toUpperCase();
return value;
})
.filter((value) => this.form.valid)
.subscribe((value) => {
alert("Model Driven Form valid value: vm = " + JSON.stringify(value));
});
What we are doing here is taking the stream of form values (that changes each time the user types in an input field), and then apply to it some commonly used functional programming operators: map
and filter
.
In fact, the form stream provides the whole range of functional operators available in Array
and many more.
In this case we are converting the first name to uppercase using map
and taking only the valid form values using filter
. This creates a new stream of modified valid values to which we subscribe, by providing a callback that defines how the UI should react to a new valid value.
We are not obliged to use FRP techniques with Angular 2 Model Driven Forms. Simply using them to make the templates cleaner and allow for component unit testing is already a big plus.
But the use of FRP can really allow us to completely change the way we build UIs. Imagine a UI layer that basically holds no state for the developer to manage, there are really only streams of either browser events, backend replies or form values binding everything together.
This could potentially eliminate a whole category of bugs that come from mutability and corrupted application state. Building everything around the notion of stream might take some getting used it and probably reaps the most benefit in the case of more complex UIs.
Also FRP techniques can help easily implement many use cases that would otherwise be hard to implement such as:
Angular 2 provides two ways to build forms: Template Driven and Form Driven, both with their advantages and disadvantages.
The Template Driven approach is very familiar to Angular 1 developers, and is ideal for easy migration of Angular 1 applications into Angular 2.
The Model Driven approach provides the immediate benefit ot testability and removes validation logic from the template, keeping the templates clean of validation logic. But also allows for a whole different way of building UIs that we can optionally use, very similar to what is available in the React world.
Its really up to us to assess the pros and cons of each approach, and mix and match depending on the situation choosing the best tool for the job at hand.
If you want to know more about Angular 2 Forms, the podcast of Victor Savkin on Angular Air goes into detail on the two form types and
ngModel
.
This blog post gives a high level overview of how Angular 2 will better enable Functional Reactive Programming techniques.
If you are interested in learning about how to build components in Angular 2, check also The fundamentals of Angular 2 components
Original link: http://blog.jhades.org/introduction-to-angular-2-forms-template-driven-vs-model-driven/