当前位置: 首页 > 工具软件 > Ideal Forms > 使用案例 >

Introduction to Angular 2 Forms - Template Driven vs Model Driven Forms

赵光赫
2023-12-01

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:

  • What is Angular 2 Forms all about
  • Template Driven Forms, or the Angular 1 way
  • Model Driven Forms, or the new Functional Reactive way
  • Advantages and disadvantages of both form types

Angular 2 Forms - what is it all about?

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:

  • how to keep track of the global form state
  • know which parts of the form are valid and which are still invalid
  • properly displaying error messages to the user so that he knows what to do to make the form valid

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 2 Template Driven Forms

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.

Angular 2 Forms out of the box functionality

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:

  • touched or untouched
  • valid or invalid
  • pristine or dirty

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.

The logic for all this must be in the controller, right?

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!

How does Angular pull this off then?

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.

Advantages and disadvantages of Template Driven Forms

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.

Template Driven Forms from a functional programming point of view

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.

Model Driven Forms

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 ngFormModeldirective 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.

What does the controller look like?

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.requiredbuilt-in validator.

But what happened to 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.

Advantages and disadvantages of Model Driven Forms

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.

Functional Reactive Programming in Angular 2

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.

Advantages of building UIs using Functional Reactive Programming (FRP)

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:

  • pre-save the form in the background at each valid state
  • typical desktop features like undo/redo

Summary

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/

 类似资料:

相关阅读

相关文章

相关问答