The Good and the Bad of Programming Forms in Angular 2

Compared to Angular 1, Angular 2 offer more choices for working with forms and more control over the data flow between a form and a model. However, I feel that Angular 2 provides form-related APIs and directives that are quirky and confusing, while also increasing the amount of work required to manage simple forms. This article isn’t intended to be a tutorial for working with forms in Angular 2 and some of the code inside is not code you want in a real application. Some of the code only exists to demonstrate how forms work. 

I apologize for not taking the time to make this post shorter.

Introduction

Angular 1 simplified working with forms by providing the ngModel directive. ngModel could synchronize an input’s value with a model property in a JavaScript object using 2-way data binding. ngModel also worked behind the curtains with an ngForm directive to build a data structure representing the state of the form and all the inputs inside the form. The data synchronization and the current form state gave us a straightforward approach to initializing, validating, and receiving form input.

At a high level, Angular 2 offers two different approaches to working with forms. Both approaches can use the new NgModel directive for 2-way data binding, although the new NgModel doesn’t work exactly like the old ngModel of Angular 1.

The first approach we’ll look at is a declarative approach using NgForm and NgModel. We’ll call this approach the NgForm approach, because the form level directive we’ll use is NgForm.

The second approach we’ll look at uses more imperative code and interacts with the form through one or more objects that abstractly represent controls on the page. We’ll call this the NgFormModel approach, because the form level directive we’ll use is named NgFormModel. 

Because there is a lot of flexibility in Angular 2, we’ll probably see many different variations of these 2 different approaches to forms. The code in this article is not the only way to work with forms.

The NgForm Approach

As a sample scenario, imagine we want to edit information about a movie. We have a Movie model and a MovieService to translate JSON from the server into the Movie model.  Given these pieces, a component to edit a specific movie might have code inside like the following.

movie: Movie;
 
constructor(private movieData: MovieService, 
            private params: RouteParams,
            private router: Router) {   
}
 
ngOnInit() {
    var id = this.params.get("id");
    this.movieData.get(id)
                  .subscribe(movie => {
                      this.movie = movie;
                  });
}

The important piece here being that during initialization, the component uses a service to retrieve a movie model and assign the movie to a bindable field named movie. Here’s the essence of a template to edit a movie.

<form>
     
    <input type="text" [(ngModel)]="movie.title" required>        
     
    <!-- other inputs and labels omitted ... -->
     
    <input type="submit" value="Save" (click)="save()">
     
</form>    

This simple template will give us 80% of the functionality we need. When the component updates the movie with fresh data from the server, NgModel will push updated values into the form. And, when the user types into the form, NgModel will push the form values back into the movie model. When the user presses submit, the save method only needs to take the existing movie model and ultimately serialize the values into an HTTP POST.

At this point we can even write style rules to highlight elements when they are in an invalid state. Some Angular 2 tutorials imply this is only possible after manually adding an additional directive. However, there is an NgControlStatus directive that automatically attaches to elements using the NgModel directive and modifies the classNames property to indicate when an input is valid, invalid, valid, dirty, pristine, etc. Since we have a required attribute on the input for a movie title, we could turn the entire input red when the input is empty using the following CSS.

.ng-invalid {
    background: red;
}

Angular 1 users will recognize the CSS name ng-invalid. We could use the same style rule with Angular 1. The similarities with Angular 1 mostly end here, however. To achieve the last 20% of the functionality the form requires we need to dive into some additional directives and syntax involving an unhealthy amount of misdirection.

Imagine we want to add a simple validation message that appears when the movie title is empty. In Angular 1 you could name a form and the inputs inside a form and have access to a data structure representing the entire form in the template scope. In Angular 2 there is still an NgForm directive that automatically attaches itself to <form> elements and provides an API to determine if a form is valid or invalid. Unfortunately, the NgModel directive no longer coordinates with NgForm to track the validity of a specific input.  This is unfortunate because the NgModel directive knows about the validity of its associated input (NgControlStatus relies on this knowledge). We can see NgModel at work by grabbing a local reference to the NgModel directive inside the template.

<input type="text" required
       [(ngModel)]="movie.title"
       #title="ngForm">        
 
<span *ngIf="!title.valid">
    Title is required!
</span>

In the above code, title becomes a local variable in the template and is assigned a reference to the ngForm directive. There is no actual ngForm directive on the input, however, but the code works because NgModel uses the exportAs property of its directive definition to alias itself as ngForm for these types of expressions. A confusing bit of indirection, I think, but an indirection that at least consistently repeats itself in one other scenario we’ll see later.

Although the above code can help us write validation messages for a specific input, it won’t help us track the validity of the form or the model as a whole. To achieve the holistic view, we need to start working with the real NgForm directive, the one attached to the form element. We’ll also switch to using NgForm’s submit event instead of using a click handler on the submit button.

<form #form="ngForm" (ngSubmit)="save(form)" novalidate>
     
    <input type="text" required 
           [(ngModel)]="movie.title"
           #title="ngForm">        
     
    <span *ngIf="!title.valid">
        Title is required!
    </span>
     
    <!-- other inputs and labels omitted ... -->
     
    <input type="submit" value="Save" />
     
</form>  

With this new code we are now passing a reference to the NgForm directive into the save method of the component. The save method might look like the following.

save(form) {
    if(form.valid) {
         this.movieData.save(this.movie)
             .subscribe(updatedMovie => {
                this.router.navigate(['Details', {id: updatedMovie.id}]);
            });
    }
} 

Except … the check for form.valid in the above code won’t work. Again, in Angular 1 the NgModel directive would register itself with its parent NgForm so the form would know about the validity of all the data bound inputs of the form. In Angular 2, NgModel knows if the model is in a valid state, or not, but NgModel doesn’t coordinate with the form directive. In other words, the user could have an empty title input, but the NgForm we are working with will still present itself as valid.

In order to add the input’s state to the form, we need to use a new directive with an alias of NgControl.

<input type="text" required 
       [(ngModel)]="movie.title"
       ngControl="title">        
 
<span *ngIf="!form.controls.title?.valid">
    Title is required!
</span>

Now the form will know about the title input and form.valid will include a validity check of the title. We can even dot into the form.controls object to find the title, as demonstrated in the NgIf expression. We can still alias the input to a local variable if the expression is too cumbersome.

<input type="text" required 
       [(ngModel)]="movie.title"
       ngControl="title"
       #title="ngForm">        
 
<span *ngIf="!title.valid">
    Title is required!
</span>

Note that in this case the title variable is not going to get a reference to NgModel, but instead reference the directive put in place by the ngControl attribute (which is the NgControlName directive because this directive, like NgModel, uses exportAs to alias itself as ngForm). 

Here then, are some of the facts to know about the current situation.

  • NgControlName uses a selector of ngControl but exports itself as ngForm
  • Both NgControlName and NgModel derive from an NgControl abstract base class, so both can work with validation directives (like the RequiredValidator) and report on their validation status.
  • The NgModel directive selector excludes elements where the NgControlName directive is present, but NgControlName can use an existing NgModel expression to effectively two-way bind the model and input value.
  • NgModel and NgControlName thus share an amazing number of similarities, but …
  • Only NgControlName will register itself with the parent NgForm and give us the complete picture for validation

When I dig into the details like the ones listed above, I have to wonder why NgControlName exists. NgModel could give us the same capabilities without adding an extra directive. I’m biased with the (relative) simplicity of Angular 1, but what’s happening looks like unnecessary complexity.

 

The NgFormModel Approach

Perhaps one reason for the complexity of the model approach is that Angular is pushing developers away from using two-way data binding features. I feel this is unfortunate. Two-way binding has gotten a bad reputation because it was easy to abuse in situations where two-way binding isn’t an appropriate solution. For example, updating a model value in one controller of the application and allowing the change to implicitly propagate through other controllers on the screen which in turn could also update the same value. Other UI frameworks provide solutions for these complex UI scenarios using events, messages buses, or event aggregators.

Most forms, on the other hand, are a closed loop. I need to populate the inputs with values, and then retrieve the input values into an object I can serialize and and send off to the server. NgModel was perfect for this scenario where data would only flow between the form and the model. Certainly there are complex forms out there in the world, but I suspect the simple case represents more than 80% of the forms in existence.   

For complex forms, Angular 2 provides control and form abstractions that allow explicit data shuffling and validation in code.  The template code in this approach is a bit simpler.

<form [ngFormModel]="form" (ngSubmit)="save()">
     
    <input type="text" [ngFormControl]="title">
     
    <span *ngIf="!form.controls.title?.valid">
        The title is required!
    </span>
     
    ...
     
    <input type="submit" value="Save">
     
</form>

In the above template, NgFormModel and NgFormControl will bind their respective DOM elements to controls created by the underlying component. In contrast to our previous snippet of component code, the component associated with this template does not contain a field of type Movie. Instead, the component contains fields representing the form and the inputs inside the form.

form: ControlGroup;
title: Control;
 
constructor(private movieData: MovieService, 
            private params: RouteParams,
            private router: Router) {  
 
    this.title = new Control("initial value", Validators.required);
    this.form = new ControlGroup({
        title: this.title
        // ... 
    });      
}

Notice how validation rules have moved. Instead of being attributes in the template the rules are applied in code. The API here feels a bit clumsy and forces you to compose all rules for a control into a single composite rule using Validators.compose. There are many places in the Angular 2 API where arrays of things are accepted as a parameter, but not here for validation rules.

Also, instead of constructing a ControlGroup directly, the component could inject a FormBuilder and use the builder to create the control group. The API is the same for both, and again a bit clumsy since you pass the grouped controls as key value pairs. I think a reasonable alternative would be to give each individual control a name (which a control should have in any case), and then create a group from a collection of controls.

In theory, creating controls and their associated validation rules inside the component makes unit tests against the component more valuable. We can write asserts to make sure the proper validation rules are in place. In practice I think this is overkill for most simple forms with declarative validations. There is a lot of test code needed to make sure a single required attribute is in place, and it is the type of test that will live in the test mass for years without failing. Certainly complex validation logic needs testing, but perhaps not the application of the logic. If anything, an argument could be made to push complex validations into the model itself instead of coupling the logic with a specific UI component.

Although it is not necessary to create a field for each individual control, doing so allows for cleaner code when you access the control. For example, when data arrives from the server we can use the fields to update the screen.

ngOnInit() {
    var id = this.params.get("id");
    this.movieData.get(id)
                  .subscribe(movie =>{
                      this.title.updateValue(movie.title);                     
                  });
} 

Here again there is a bit of an oddity in the API. A control has several set methods (setParent, setErrors), but when it comes to the value property, the method to set the value is named updateValue. Yes, I’m picky, but it is often the small inconsistencies that tell the biggest story about an API.

Updating a model or sending the form information back to the server is the above operation in reverse. In other words, you can go to each control and read the value property to receive the associated input value.

If all the code required to update and read the control values is bothersome, you can still use NgModel in combination with NgFormControl, just like NgModel works with NgControlName.

<input type="text" [ngFormControl]="title" 
       [(ngModel)]="movie.title" >
 
<span *ngIf="!form.controls.title?.valid">
    The title is required!
</span>

Now a component only needs to set a movie field to set the control values, and read the field to fetch the control values.

Summary

There are many variations to the different approaches I’ve presented here. The approach you’ll want to use comes down to answering a couple questions. Do you want to avoid using NgModel because two-way binding is evil? If so, use NgFormModel and explicitly read and write control values from the code. Do you want to avoid writing component code for every individual control on the screen? If so, use NgForm and NgControlName to instantiate controls from the template (and synchronize values using NgModel).

Although I might sound critical of the Angular 2 approach to forms, there are some interesting scenarios these approaches enable which I’ve run out of room to cover. For example, treating changes in a form as an observable stream of events. Also, all the APIs and directives provided allow for a tremendous amount of flexibility and cover more application scenarios compared to Angular 1. 

Overall though, I worry that all of the similar names (ngControl versus ngFormControl), the similar but different syntaxes (ngControl=”..” versus [ngFormControl]=”..”), and aliasing (NgControlName uses an NgControl selector but exports as ngForm) is too much for busy developers who will cargo cult a solution without understanding the implications. And in the end, there is no simple approach to handle simple forms.

JavaScript Promises and Error Handling

Errors in asynchronous code typically require a messy number of if else checks and a careful inspection of parameter values. Promises allow asynchronous code to apply structured error handling. When using promises, you can pass an error handler to the then method or use a catch method to process errors. Just like exceptions in regular code, an exception or rejection in asynchronous code will jump to the nearest error handler.

As an example, let’s use the following functions which log the execution path into a string variable.

var log = "";
 
function doWork() {
    log += "W";
    return Promise.resolve();
}
 
function doError() {
    log += "E";
    throw new Error("oops!");
}
 
function errorHandler(error) {
    log += "H";
}

We’ll use these functions with the following code.

doWork()
    .then(doWork)
    .then(doError)
    .then(doWork) // this will be skipped
    .then(doWork, errorHandler)
    .then(verify);
     
  function verify() {
    expect(log).toBe("WWEH");
    done();
}

The expectation is that the log variable will contain “WWEH” when the code finishes executing, meaning the flow of calls with reach doWork, then doWork, then doError, then errorHandler. There are two observations to make about this result, one obvious, one subtle.

The first observation is that when the call to doError throws an exception, execution jumps to the next rejection handler (errorHandler) and skips over any potential success handlers. This behavior is obvious once you think of promises as a tool to transform asynchronous code into a procedural flow of method calls. In synchronous code, an exception will jump over statements and up the stack to find a catch handler, and the asynchronous code in this example is no different.

What might not be immediately obvious is that the verify function will execute as a success handler after the error. Just like normal execution can resume in procedural code after a catch statement, normal execution can resume with promises after a handled error. Technically, the verify function executes because the error handler returns a successfully resolved promise. Remember the then method always returns a new promise, and unless the error handler explicitly rejects a new promise, the new promise resolves successfully.

A promise object also provides a catch method to handle errors. The last code sample could be written with a catch statement as follows. ]

doWork()
    .then(doWork)
    .then(doError)
    .then(doWork) 
    .then(doWork)
    .catch(errorHandler)
    .then(verify);

The catch method takes only a rejection handler method. There can be a difference in behavior between the following two code snippets:

.then(doWork, errorHandler)

… and …

.then(doWork)
.catch(errorHandler)

In the first code snippet, if the success handler throws an exception or rejects a promise, execution will not go into the error handler since the promise was already resolved at this level. With catch, you can always see an unhandled error from the previous success handler.

Finally, imagine you have a rejected promise in your code, but there is no error handler attached. You can simulate this scenario with the following line of code.

Promise.reject("error!");

Some native environments and promise polyfills will warn you about unhandled promise rejections by displaying a message in the console of the developer tools. An unhandled promise rejection means your application could be missing out on a critical error!

Karma Is Not Just For AngularJS

The AngularJS team created Karma, but Karma isn’t tied to AngularJS. As a test runner, I can use Karma to run tests against any JavaScript code using a variety of testing frameworks in a variety of browsers. All I need is Node.js.

Pretend I am writing code to represent a point in two dimensional space. I might create a file point.js.

var Point = function(x,y) {
    this.x = x;
    this.y = y;
}

I’ll test the code using specifications in a file named pointSpecs.js.

describe("a point", function() {
    it("initializes with x and y", function() {
        var p1 = new Point(3,5);
         
        expect(p1.x).toBe(3);
        expect(p1.y).toBe(5);
    });
});

What Karma can do is:

- Provide web browsers with all of the HTML and JavaScript required to run the tests I’ve written.

- Automate web browsers to execute all the tests

- Tell me if the tests are passing or failing

- Monitor the file system to re-execute tests whenever a file changes.

Setup

The first step is using npm to install the Karma command line interface globally, so I can run Karma from anywhere.

npm install karma-cli –g

Then I can install Karma locally in the root of my project where the Point code resides.

npm install karma --save-dev

Karma requires a configuration file so it knows what browsers to automate, and which files I’ve authored that I need Karma to load into the browser. The easiest way to create a basic configuration file is to run karma init from the command line. The init command will walk you through a series of questions to create the karma.conf.js file. Here is a sample session.

>karma init 
 
Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine 
 
Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no 
 
Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> PhantomJS
> Chrome
> 
 
What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> js/**/*.js
> specs/**/*.js
> 
 
Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
> 
 
Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes 
 
Config file generated at "C:\temp\testjs\karma.conf.js".

I can open the config file to tweak settings and maintain the configuration in the future. The code is simple.

module.exports = function(config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'js/**/*.js',
      'specs/**/*.js'
    ],
    exclude: [
    ],
    preprocessors: {
    },
    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['PhantomJS', 'Chrome'],
    singleRun: false
  });
};

Note that when I enter the location of the source and test files, I want to enter the location of the source files first. It’s helpful if you can organize source and test files into a directory structure so you can use globbing (**) patterns, but sometimes you need to explicitly name individual files to control the order of loading.

At this point I can start Karma, and the tests will execute in two browsers (Chrome, which I can see, and PhantomJS, which is headless). I’d probably tweak the config file to only use Phantom in the future.