November 2017

Volume 32 Number 11

[The Working Programmer]

How To Be MEAN: Angular Forms

By Ted Neward | November 2017

Ted Neward

Welcome back again, MEANers.
All throughout this series, the focus has been about Angular; specifically, using Angular to display data and route through multiple pages. As much as the Web browser is built to be able to display data, however, it’s also intended to be able to gather data and pass it back to the server, and thus far, that’s been missing from the discussion.

Angular can certainly capture data through forms, but doing so is a touch tricky—not so much in the syntax of creating and defining the browser form, but in one particular aspect of the underlying behavior, which I’ll go over later. But let’s not put the cart before the horse. Time to talk about some simple form definition and capture.

Formed Components

In previous columns, I’ve talked about how you can bind “template statements” (to use the Angular terminology) to the events offered up by an Angular component. The most common example of that is capturing the click event on a <button> element to invoke a method on an Angular component:

<button (click)="console.log('Clicked')">Push me!</button>

In and of itself, this is good, but it doesn’t provide any facility to capture input—in order for this to be useful for data entry, one of two things has to happen: Either the code in the component has the ability to reference the controls on the page from inside the component (which is what ASP.NET Web Forms, among other frameworks, will do), or the code being invoked has to be able to receive the input data as a parameter. However, this can take a couple of different forms.

First, and most generic, the Angular template statement can reference the $event object, which is essentially the DOM’s event object generated during the user’s session. This simply requires referencing the parameter as part of the statement, such as:

<button (click)="capture($event)" >Save</button>

The drawback, however, is that the object passed is the DOM event representing the user’s action; in this case, a MouseEvent tracking the location on the screen where the mouse was clicked, and it doesn’t really capture the state of the other elements on the page. While it would certainly be possible to navigate the DOM hierarchy to find the control elements and extract the values from them, it’s also not really the Angular Way. Components should be isolated away from the DOM, and the template statement should be able to obtain the data it needs and pass it in to a method on the component for use.

This approach suggests that Angular needs some way to identify the input fields on the page, so that the template statement can pull values and pass them in. The need to be able to identify the form element takes the form of an “identifier” on the <input> element itself, what Angular calls a “template reference variable.” Like some of the other Angular syntax, it deliberately uses syntax that doesn’t look like HTML:

<input #firstName>

This will create an Input field in HTML, as per the normal HTML tag of the same name, but then introduce a new variable into the template’s scope, called firstName, which can be referenced from the template statement bound to an event from a field, like so:

<button (click)="addSpeaker(firstName, lastName)" >Save</button>

This is pretty self-explanatory: On a button click, invoke the addSpeaker method of the component, passing in the firstName and lastName variables, accordingly, as shown in Figure 1.

Figure 1 Invoking the addSpeaker Method

import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-speaker-edit',
templateUrl: './speaker-edit.component.html',
styleUrls: ['./speaker-edit.component.css']
})
export class SpeakerEditComponent implements OnInit {
constructor() { }
ngOnInit() { }
addSpeaker(fname: string, lname: string) {
console.log("addSpeaker(", fname, ",", lname, ")")
}
}

However, written like this, what shows up in the browser’s console isn’t the expected strings from the input; instead, values such as <input _ngcontent-crf-2> appear in place of each of those values. The reason for this is simple: The browser console returns the actual Angular representations of the DOM element, rather than the input data that was typed in. The solution to that is equally simple: Make use of the “value” property on each side of the template statement to get to the data the user typed in.

Thus, if I need to build a component for creating new speakers, I can create a component that displays two <input> fields, a <button> that has a (click) that calls addSpeaker, passing in firstName.value and lastName.value, and use that method to invoke the SpeakerService (from an earlier article) to save it to the database. But this idea of “creating a new Speaker” is conceptually very close to “editing an existing Speaker,” so much so that some modern databases have begun to talk about the insert and update operations as being essentially one and the same: the upsert. It would be quite nice—and component-oriented—if the SpeakerEdit component could serve as either create or edit within the same component.

Thanks to the power of the @Input and @Output directives you’ve seen earlier, this is actually quite trivial to do. Add an @Input field for the speaker for one to be passed in, and an @Output field to let people know when the user clicks Save. (The latter isn’t necessary, if you make the decision that the SpeakerEdit component will always save to the database, and there’s no other action that a client of the component would ever want to do. That would be a highly context-sensitive conversation in the team meeting.)

This leaves me with what’s in Figure 2 for the SpeakerEdit component code.

Figure 2 The SpeakerEdit Component

export class SpeakerEditComponent implements OnInit {
@Input() speaker: Speaker;
@Output() onSave = new EventEmitter<Speaker>();
constructor(private speakerService: SpeakerService) { }
ngOnInit() {
if (this.speaker === undefined) {
this.speaker = new Speaker();
}
}
save(fn: string, ln: string) {
this.speaker.firstName = fn;
this.speaker.lastName = ln;
this.onSave.emit(this.speaker);
}
}

And, as you might expect given my design skills, the template is pretty bare-bones but functional:

<div>
Speaker Details: <br>
FirstName: <input #firstName><br>
LastName:  <input #lastName><br>
<button (click)="save(firstName.value, lastName.value)">Save</button>
</div>

Notice, again, that I use the “.value” to extract the string values out of the firstName and lastName input fields.

Using this component (in this exercise, from the main App­Component) is pretty straightforward:

<h3>Create a new Speaker</h3>
<app-speaker-edit (onSave)="speakerSvc.save($event)"></app-speaker-edit>

In this case, I chose to not save the Speaker from within the component, but have clients do so from the emitted event. The speakerSvc object is a dependency-injected SpeakerService, from the previous article on building services in Angular (msdn.microsoft.com/magazine/mt826349).

This is what componentization buys you: The ability to create UI “controls” that you can then drop in and just use, rather than have to think about how they work internally.

Firing Events

It would be nice to wrap up here, but there’s one gotcha about events in Angular that deserves attention. It’s quite common for developers to want to trap things like keystrokes and react to the entered data—for example, to display some auto-complete suggestion values. The traditional DOM events to this are methods like onBlur or onKeyUp. For example, say it would be nice to track each keystroke and display it as the user is typing. Developers new to Angular might expect this to work:

@Component({
  selector: 'loop-back',
  template: `
    <input #box>
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

When run, however, this code will not display each character as it’s typed—in fact, it’ll do nothing at all. This is because Angular won’t fire events unless the browser fires an event. For that reason, Angular needs an event to fire, even if the code that gets fired in that event is a complete no-operation, such as the template statement 0, as in:

@Component({
  selector: 'loop-back',
  template: `
    <input #box (keyup)="0">
    <p>{{box.value}}</p>
  `
})
export class LoopbackComponent { }

Notice the “keyup” binding. This tells Angular to register for key events on the Input element, and that gives Angular the opportunity to trigger events, which will then update the view. It’s a little awkward to work with at first, but this way Angular isn’t polling for any kind of event all the time and, thus, doesn’t have to consume quite so many CPU cycles.

Wrapping Up

Some veteran Web developers might find this all a little confusing and awkward: “Why can’t we just go back to the good-ol’ HTML form?” The Angular Way isn’t always obvious, but in many ways, once the reasoning and rationale become clear, they’re usually understandable and reasonable. In this particular case, the Angular Way is to embrace the concept of components and think in terms of constructing usable “constructs” that know how to capture input and process it. This permits a degree of flexibility that wasn’t really present in the “old Web” way of thinking, such as having multiple such components on a single page. (When you have to refresh the whole page to display results, you end up having one input-validate-save-render cycle per input requirement.)

However, there are some other issues here, such as knowing how to update other parts of the system when a user edits an existing Speaker. Angular has a solution for this, but there are a few more steps to go before being able to get into the world of Reactive Programming. The home stretch is near, though, so stick with me for just a few more. But, as always, in the meantime … Happy coding!


Ted Neward is a Seattle-based polytechnology consultant, speaker and mentor, currently working as the director of Developer Relations at Smartsheet.com. He’s written a ton of articles, authored and coauthored a dozen books, and works all over the world. Reach him at ted@tedneward.com or read his blog at blogs.tedneward.com.

Thanks to the following Microsoft technical expert for reviewing this article: James Bender


Discuss this article in the MSDN Magazine forum