Angular Forms are an important part of almost every Angular app, even more so for enterprise apps — dashboards, live data apps, data collection, and so on. It is very important to have as flexible of a setup as possible, because requirements for forms can change drastically in a single moment.

There are several important things to know and remember about Angular forms.

Forms are not type-safe#

That’s it, I’ve said it. Forms are not type safe, so we cannot rely on TypeScript to catch bugs, error prone code and typos for us, which means we have to be extra cautious when dealing with forms. But of course just being cautious is not the only useful thing — there are several things we can do to make our lives easier.

  1. Limit your use of FormGroup.get usage when referring to nested controls in a FormGroup. Instead, store the references to the nested controls in component properties.
  2. Use DTO pattern when changing the data model of the form to the one required by a data service.
  3. Abstract away harder form controls to custom components implementing ControlValueAccessor.

Let’s go over these cases.

Case 1: Limit your use of FormGroup.get usage

Consider this piece of code:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  form = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    age: [''],
  });

  constructor(private formBuilder: FormBuilder) {}
}

This is a component with a simple FormGroup on it, nothing special.

Now take a look at the template:

<form [formGroup]="form">
  <div>
    <label for="firstName">First Name</label>
    <input formControlName="firstName" id="firstName"/>
    <span *ngIf="form.get('firstName').touched && form.get('firstName').hasError('required')" class="errors">
      Field is required
    </span>
  </div>

  <div>
    <label for="lastName">Last Name</label>
    <input formControlName="lastName" id="lastName"/>
    <span *ngIf="form.get('lastName').touched && form.get('larstName').hasError('required')" class="errors">
      Field is required
    </span>
  </div>
</form>

Looks like a pretty usual form setup, nothing too hard. But did you see the problem?

On line 13, it says ‘larstName’ instead of ‘lastName’, and it causes problems. Even worse: there are no errors in the console during development before we change the form value — which may well be when the QA engineers are testing the page (let’s be honest, lots of software engineers will think this code is too simple to test themselves in its entirety), and even then this error

Error screenshot that displays a hardly understandable error

will feel cryptic for lots of developers at first glance. Also, writing “form.get(‘firstName’)” multiple times is tedious (and increases the chance of further typos). Also, we can’t really rely on Intellisense autocomplete, so we have to copy-paste multiple times. Not the most pleasant thing to do as a software developer.

Here is what we can do:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
})
export class AppComponent  {
  form = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    age: [''],
  });

  controls = {
    firstName: this.form.get('firstName'),
    lastName: this.form.get('lastName'),
  }

  constructor(private formBuilder: FormBuilder) {}
}

This is pretty simple: we just stored the references to our form controls in a special object called controls so we can use them easily in our template, which will now look like this:

<form [formGroup]="form">
  <div>
    <label for="firstName">First Name</label>
    <input formControlName="firstName" id="firstName"/>
    <span *ngIf="controls.lastName.touched && controls.firstName.hasError('required')" class="errors">
      Field is required
    </span>
  </div>

  <div>
    <label for="lastName">First Name</label>
    <input formControlName="lastName" id="lastName"/>
    <span *ngIf="controls.lastName.touched && controls.lastName.hasError('required')" class="errors">
      Field is required
    </span>
  </div>
</form>

At first this looks like a minor improvement, but there are several things to consider:

  1. FormControl.get is a function and calling it inside a template means it will be called on each change detection cycle (hint: that’s a lot), but by using this methods we also solve that problem
  2. We only store references for the controls that are going to be referenced in the template
  3. If we need those controls inside the component class’ code, we can also access them through these references and avoid lookup

So, one of the problems is solved, but what about type-safety? The values of our controls are still treated by TypeScript as they are of type any, which is not a very good thing. So what can we do about it?

Case 2: Use Data Transfer Object pattern

Imagine a situation where we have to fill in a complex form and send it to the server via an HTTP Request. We could, of course, take the FormGroup.value and just send it; but most probably the server API gateway awaits for a slightly different data model. For example, it may wait for a Date in ISO format rather than a standard JS string. Or it may want to get the concatenated list of user selected custom tags rather than an array of strings. Of course, we can change the shape of our FormGroup, but on the other hands the controls that we use in the template might just work better with data types other than those the server expects. So what can we do?

Here’s is where DTO, or data transfer object, comes in. DTO is a special object, which is responsible for carrying information from one subsystem (our Angular application) to another (the backend). It’s main and original purpose was to reduce the amount of data sent over a network. But it also ensures that the subsystems talk to each other using the same language (same data types). So how are we going to benefit from this pattern? By writing a simple class, which will receive the form value in its constructor and produce an object that exactly matches the server’s API signature, handling all differences between what our raw form value is and what the server awaits. It is very important that our class does not have any other behaviors, getters, setters and so on — that is stuff that is not going to be serialized with JSON.stringify, and our class is going to have a single responsibility — produce a data transfer object. Here is an example of a DTO:

export class ArticleDTO {
  title: string;
  tags: string;
  date: string;
  referenceIds: number[];

  constructor(formValue: RawFormValue) {
    this.title = formValue.title;
    this.tags = formValue.tags.join(',');
    this.date = formValue.date.toISOString();
    if (formValue.referenceIds && formValue.referenceIds.length > 0) {
      this.referenceIds = formValue.referenceIds;
    }
  }
}

export interface RawFormValue {
  title: string;
  tags: string[];
  date: Date;
  referenceIds?: number[];
}

Now this class is pretty simple: it handles the transformation of one object (our raw form value) into another (which is going to the backend). The logic is entirely encapsulated inside the constructor. If there are problems, the constructor is the single place throughout out codebase where it could have possibly happened; if there are problems with type, the RawFormValue interface is where we should look. Here is an example of how we can use this pattern:

export class AppComponent  {
  // rest of the component implementation is ommitted for brevity

  submit() {
    if (this.form.valid) {
      const article = new ArticleDTO(this.form.value as RawFormValue);
      // now send the article DTO to the backend using one of your services
    }
  }
}

This has several benefits:

  1. The business logic of the app with data manipulations and handling is being moved out from the component (it really does not belong there)
  2. That logic is contained in a single place in our app, which is solely responsible for that logic (easier to catch bugs)
  3. This plays nicely with whatever data manipulation strategy you use, from plain old JS objects to state management and, for example, NGRX normalizations

Custom Angular Controls#

The last point I mentioned about making forms simpler is creating custom controls. Implementing the ControlValueAccessor interface in a component and registering it with NG_VALUE_ACCESSOR provider allows us to create a custom Angular Form Control. This means our <custom-component></custom-component> can now take FormControl and ngModel like this:

<custom-component formControlName="controlName"></custom-component>

This helps us abstract away complex behavior on forms and make it reusable. Whenever you find yourself doing lots of custom heavy logic on a single control in a FormGroup, think if it is possible to turn it into another component.

Don’t forget async validators#

The most common bad practice about forms that I have encountered was when developers forgot about async validators and used:

  • Directives
  • Pipes
  • Custom logic inside the parent component

to perform something mundane as checking if a user with that email exists. Main point is: don’t forget about them, and use wherever applicable

setValue vs patchValue#

There are two ways of changing the value of a FormControl in Angular: setValue and patchValue. Mainly they are the same, but there is one important difference though: if we call setValue on a FormGroup with an object that lacks some keys from the form signature, it will throw an error. This is useful, because it allows for some type safety in the dynamic world of Reactive Forms. But it has a gotcha: it also throws an error whenever the object we pass to it has a property that our form does not. So this will throw an error:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
})
export class AppComponent  {
  form = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    age: [''],
  });

  ngOnInit() {
    // throws an error
    this.form.setValue({
      firstName: 'Armen',
      lastName: 'Vardanyan',
      age: 25,
      occupation: 'Software developer, writer',
    });
  }

  constructor(private formBuilder: FormBuilder) {}
}

This will throw an error saying “Error: Cannot find form control with name: occupation”. Which is kinda useful, but comes with a serious downside we have to take care of. Consider this situation:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
})
export class AppComponent  {
  form = this.formBuilder.group({
    firstName: [''],
    lastName: [''],
    age: [''],
  });

  async ngOnInit() {
    const userData = await this.userService.getUserById(/*some id*/);
    this.form.setValue(userData);
  }

  constructor(
    private formBuilder: FormBuilder,
    private userService: UserService,
  ) {}
}

So in this case we get some data from the server and set it as form value — nothing particularly outlandish. So what is the problem?

At the moment, everything works fine, the server sends exactly the same data that is needed. But imagine this — the same API call we use to retrieve this user data is also used somewhere else in the app, in a component that is not built by us. And right there the UI needs a little more data about the user — so the API developers add an “occupation” field in the response, and think “when we added something, why would anything stop working?”. And guess what? Now it stops working, because there is a new property on the object that we try to setValue. So how do we solve this? There are two approaches:

  1. Use patchValue when setting value from external API-s. This solves the problem at hand, but may create other problems in the future — what if API design has a real breaking change and some fields go missing in the response? Rather than seeing blank screens/input fields we would want to see normal error message, and patchValue just does not throw.
  2. The complete solution: write an intermediary function (or maybe a class) which converts the server response to something compatible with our form’s signature (kind of like the opposite of what we did when sending the form’s value to the server). Here is an example:
interface RawFormValue {
  firstName: string;
  lastName: string;
  age: number;
}

function toRawFormValue<T extends RawFormValue>(serverData: T): RawFormValue {
  return {
    firstName: serverData.firstName,
    lastName: serverData.lastName,
    age: serverData.age,
  }
}

Notice how the typing is as strict as you can get — functions receives any object that contains the same fields as our forms, extracts those fields and returns an object that contains only them — fully compatible with our code. If API design changes, the only thing we will have to do is change this function.

Forms and events#

One important feature of the ReactiveForms is that they emit and read different events — control has been touched, control became dirty, value has changed, validity has changed, and so on. We utilize that in lots of forms, but the most popular is subscribing to FormControl.valueChanges Observable. This Observable provides a stream of changes on the control, either triggered by the user or programmatically.

Notice how I said or programmatically.

This means calling FormControl.setValue will trigger an emission to the subscribers of its valueChanges. This may result in unexpected results sometimes. Imagine the following scenario: a directive that binds to every [formControl], injects a reference to NgControl, reads the valueChanges0, removes all trailing spaces from it and sets the value on the control. Here is an example implementation:

@Directive({
  selector: '[formControl]'
})
export class TrimDirective implements OnInit {

  constructor(
    private ngControl: NgControl,
  ) { }

  ngOnInit() {
    this.ngControl.valueChanges.subscribe(
      (value: string) => this.ngControl.control.setValue(value.trim())
    );
  }
}

This directive does exactly that — don’t allow trailing spaces in inputs. But here’s the catch — as soon as the user inputs anything, it will trigger the directive, which will set the new value, which will trigger the directive to set a new value which will trigger the directive to… Well, you see the trap.

This may easily be avoided with an option called emitEvent, which is set to true by default. When it is false, what it essentially does is tell the FormControl that the value must be changed, but the subscribers must not be notified about that change.

Here’s how it works:

(value: string) => this.ngControl.control.setValue(value.trim(), {emitEvent: false}),
Be careful with this — take into consideration that if you set a value while emitEvent: false, subscriber’s won’t be notified.

Conclusion#

Angular Forms are very powerful tools that can be used to performs very complex operations. But they may be tricky, and this article helps to tackle some common issues related to them. But in now way is it exhaustive, so feel free to explore Angular Forms further — there’s always something new to learn!