Better Angular Form Components with ngModel and formControl Implementation

June 9, 2020

2,340 words

Post contents

One of Angular's greatest strengths over its contemporaries like React or Vue is that it's a framework. What does this mean in the practical sense? Well, because you're providing the defaults for everything right out-of-the-box, you have a set of guard rails to follow when architecting new things. A set of baseline rules for things to follow, so to speak.

One such guard rail comes in the form of the @angular/forms package. If you've used Angular for long, you're doubtlessly familiar with the [(ngModel)] method of two-way data binding in the UI. Seemingly all native elements have support for this feature (so long as you have FormsModule imported in your module).

More than that, if you want more powerful functionality, such as disabling an entire form of fields, tracking a collection of fields in a form, and doing basic data validation, you can utilize Angular Reactive Forms' [formControl] and do all of that and more.

These features are hugely helpful when dealing with complex form logic throughout your application. Luckily for us, they're not just exclusive to native elements - we can implement this functionality into our own form!

Example

It's hard for us to talk about the potential advantages to a component without taking a look at it. Let's start with this component, just for fun.

It'll allow you to type in data, have a header label (as opposed to a floating label, which is notoriously bad for A11Y), and even present a fun message when "Unicorns" is typed in.

Here's the code:

import { Component, Input } from "@angular/core";@Component({  selector: "app-example-input",  template: `    <label class="inputContainer">      <span class="inputLabel">{{ placeholder }}</span>      <input        placeholder=""        class="inputInput"        [(ngModel)]="value"      />    </label>    <p      class="hiddenMessage"      [class.hideTheMessage]="!isSecretValue"      aria-hidden="true"    >      You unlocked the secret unicorn rave!<span>🦄🦄🦄</span>    </p>    <!-- This is for screen-readers, since the animation doesn't work with the 'aria-live' toggle -->    <p aria-live="assertive" class="visually-hidden">      {{        isSecretValue          ? "You discovered the secret unicorn rave! They're all having a party now that you summoned them by typing their name"          : ""      }}    </p>  `,  styleUrls: ["./example-input.component.css"]})export class ExampleInputComponent {  @Input() placeholder: string;  value: any = "";  get isSecretValue() {    return /unicorns/.exec(this.value.toLowerCase());  }}

With only a bit of CSS, we have a visually appealing, A11Y friendly, and quirky input component. Look, it even wiggles the unicorns!

Now, this component is far from feature complete. There's no way to disable the input, there's no way to extract data out from the typed input, there's not a lot of functionality you'd typically expect to see from an input component. Let's change that.

ControlValueAccessor

Most of the expected form functionality will come as a complement of the ControlValueAccessor interface. Much like you implement ngOnInit by implementing class methods, you do the same with ControlValueAccessor to gain functionality for form components.

The methods you need to implement are the following:

  • writeValue
  • registerOnChange
  • registerOnTouched
  • setDisabledState

Let's go through these one-by-one and see how we can introduce change to our component to support each one.

Setup

To use these four methods, you'll first need to provide them somehow. To do this, we use a combination of the component's providers array, NG_VALUE_ACCESSOR, and forwardRef.

import { forwardRef } from '@angular/core';import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';/** * Provider Expression that allows your component to register as a ControlValueAccessor. This * allows it to support [(ngModel)] and ngControl. */export const EXAMPLE_CONTROL_VALUE_ACCESSOR: any = {    /**   * Used to provide a `ControlValueAccessor` for form controls.   */  provide: NG_VALUE_ACCESSOR,  /**   * Allows to refer to references which are not yet defined.   * This is because it's needed to `providers` in the component but references   * the component itself. Handles circular dependency issues   */  useExisting: forwardRef(() => ExampleInputComponent),  multi: true};

Once we have this example provide setup, we can now pass it to a component's providers array:

@Component({  selector: 'app-example-input',  templateUrl: './example-input.component.html',  styleUrls: ['./example-input.component.css'],  providers: [EXAMPLE_CONTROL_VALUE_ACCESSOR]})export class ExampleInputComponent implements ControlValueAccessor {

With this, we'll finally be able to use these methods to control our component.

If you're wondering why you don't need to do something like this with ngOnInit, it's because that functionality is baked right into Angular. Angular always looks for an onInit function and tries to call it when the respective lifecycle method is run. implements is just a type-safe way to ensure that you're explicitly wanting to call that method.

writeValue

writeValue is a method that acts exactly as you'd expect it to: It simply writes a value to your component's value. As your value has more than a single write method (from your component and from the parent), it's suggested to have a setter, getter, and private internal value for your property.

 private _value: any = null;  @Input()  get value(): any { return this._value; }  set value(newValue: any) {    if (this._value !== newValue) {      // Set this before proceeding to ensure no circular loop occurs with selection.      this._value = newValue;    }  }

Once this is done, the method is trivial to implement:

 writeValue(value: any) {    this.value = value;  }

However, you may notice that your component doesn't properly re-render when you update your value from the parent component. Because you're updating your value outside of the typical pattern, change detection may have a difficult time running when you'd want it to. To solve for this, provide a ChangeDetectorRef in your constructor and manually check for updates in the writeValue method:

export class ExampleInputComponent implements ControlValueAccessor {  // ...  constructor(private _changeDetector: ChangeDetectorRef) { }   // ... writeValue(value: any) {    this.value = value;    this._changeDetector.markForCheck();  }

Now, when we use a value like new FormValue('test') and pass it as [formControl] to our component, it will render the correct default value

setDisabledState

Implementing the disabled state check is extremely similar to implementing value writing. Simply add a setter, getter, and setDisabledState to your component, and you should be good-to-go:

 private _disabled: boolean = false;  @Input()  get disabled(): boolean { return this._disabled; }  set disabled(value) {    this._disabled = coerceBooleanProperty(value);  }  setDisabledState(isDisabled: boolean) {    this.disabled = isDisabled;    this._changeDetector.markForCheck();  }

Just as we did with value writing, we want to run a markForCheck to allow change detection to work as expected when the value is changed from a parent

It's worth mentioning that unlike the other three methods, this one is entirely optional for implementing a ControlValueAccessor. This allows us to disable the component or keep it enabled but is not required for usage with the other methods. ngModel and formControl will work without this method implemented.

registerOnChange

While the previous methods have been implemented in a way that required usage of markForCheck, these last two methods are implemented in a bit of a different way. You only need look at the type of the methods on the interface to see as much:

registerOnChange(fn: (value: any) => void);

As you might be able to deduce from the method type, when registerOnChange is called, it passes you a function. You'll then want to store this function in your class instance and call it whenever the user changes data.

/** The method to be called to update ngModel */_controlValueAccessorChangeFn: (value: any) => void = () => {};registerOnChange(fn: (value: any) => void) {    this._controlValueAccessorChangeFn = fn;}

While this code sample shows you how to store the function, it doesn't outline how to call it once stored. You'll want to make sure to call it with the updated value on every update. For example, if you are expecting an input to change, you'd want to add it to (change) output of the input:

<input       placeholder=""       [disabled]="disabled"       [(ngModel)]="value"       (change)="_controlValueAccessorChangeFn($event.target.value)"/>

registerOnTouched

Like how you store a function and call it to register changes, you do much of the same to register when a component has been "touched" or not. This tells your consumer when a component has had interaction or not.

onTouched: () => any = () => {};registerOnTouched(fn: any) {	this.onTouched = fn;}

You'll want to call this onTouched method any time that your user "touches" (or, interacts) with your component. In the case of an input, you'll likely want to place it on the (blur) output:

<input    placeholder=""    [disabled]="disabled"    [(ngModel)]="value"    (change)="onChange($event)"    (blur)="onTouched()"/>

Consumption

Now that we've done that work let's put it all together, apply the styling from before, and consume the component we've built!

We'll need to start by importing FormModule and ReactiveFormModule into your AppModule for ngModel and formControl support respectively.

import { NgModule } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { FormsModule } from '@angular/forms';import { ReactiveFormsModule } from '@angular/forms';import { AppComponent } from './app.component';import { ExampleInputComponent } from './example-input/example-input.component';@NgModule({  imports:      [ ReactiveFormsModule, FormsModule, BrowserModule ],  declarations: [ AppComponent, ExampleInputComponent ],  bootstrap:    [ AppComponent ]})export class AppModule { }

Once you have support for them both, you can move onto adding a formControl item to your parent component:

import { Component } from '@angular/core';import {FormControl} from '@angular/forms';@Component({  selector: 'my-app',  templateUrl: './app.component.html',  styleUrls: [ './app.component.css' ]})export class AppComponent  {  control = new FormControl('');  modelValue = "";}

Finally, you can pass these options to ngModel and formControl (or even formControlName) and inspect the value directly from the parent itself:

<h1>Form Control</h1><app-example-input placeholder="What's your favorite animal?" [formControl]="control"></app-example-input><p>The value of the input is: {{control.value}}</p><h1>ngModel</h1><app-example-input placeholder="What's your favorite animal?" [(ngModel)]="modelValue"></app-example-input><p>The value of the input is: {{modelValue}}</p>

If done properly, you should see something like this:

Form Control Classes

Angular CSS masters might point to classes that's applied to inputs when various state changes are made.

These classes include:

  • ng-pristine
  • ng-dirty
  • ng-untouched
  • ng-touched

They reflect states so that you can update the visuals in CSS to reflect them. When using [(ngModel)], they won't appear, since nothing is tracking when a component is pristine or dirty. However, when using [formControl] or [formControlName], these classes will appear and act accordingly, thanks to the registerOnChange and registerOnTouched functions. As such, you're able to display custom CSS logic for when each of these states are met.

Gain Access To Form Control Errors

Something you'll notice that wasn't implemented in the ControlValueAccessor implementation is support for checking whether validators are applied. If you're a well-versed Angular Form-ite, you'll recall the ability to validate forms using validators appended to FormControls. Although a niche situation — since most validation happens at the page level, not the component level — wouldn't it be nice to check when a form is valid or not directly from the component to which the form is attached?

Well, thanks to Angular's DI system, we can do just that!

However, we'll need to make a few changes to the form input we made before. While we previously implemented a provider for form controls, we now need to manually assign the provider ourselves in the constructor:

import {  Component,  Input,  ChangeDetectorRef,  Optional,  Self,  AfterContentInit} from "@angular/core";import { ControlValueAccessor, NgControl } from "@angular/forms";@Component({  selector: "app-example-input",  templateUrl: "./example-input.component.html",  styleUrls: ["./example-input.component.css"]})export class ExampleInputComponent implements ControlValueAccessor, AfterContentInit {  constructor(    @Optional() @Self() public ngControl: NgControl,    private _changeDetector: ChangeDetectorRef  ) {    if (ngControl != null) {      // Setting the value accessor directly (instead of using      // the providers) to avoid running into a circular import.      ngControl.valueAccessor = this;    }  }      // ...}

In this code sample, we're using the @Self decorator to tell the dependency injection system that "this component itself should have been provided a formControl or formControlName". However, we want the component to work even when FormModule isn't being used, so we allow the dependency injection to return null if nothing's passed by utilizing the @Optional decorator.

Now that you have the ngControl, you can access the formControl by using ngControl.control.

ngOnInit() {    const control = this.ngControl && this.ngControl.control;    if (control) {        console.log("ngOnInit", control);        // FormControl should be available here    }}

You have a ton of different props you're able to access for the control's metadata. For example, if you want to check when errors are present, you can do the following:

get errors() {    const control = this.ngControl && this.ngControl.control;    if (control) {    	return control.touched && control.errors;    }    return null;}

And then reference it in the template:

<span class="inputLabel" [class.redtext]="errors">{{ placeholder }}</span>

Now that you have the component implementation, you can add validators to your FormControl:

import { Component } from '@angular/core';import {FormControl, Validators} from '@angular/forms';@Component({  selector: 'my-app',  templateUrl: './app.component.html',  styleUrls: [ './app.component.css' ]})export class AppComponent  {  control = new FormControl('', Validators.required);}

Not only do you have a wide range of Angular-built validators at your disposal, but you're even able to make your own validator!

Conclusion

Enabling formControl and ngModel usage is an extremely powerful tool that enables you to have feature-rich and consistent APIs across your form components. Using them, you can ensure that your consumers are provided with the functionality they'd expect in a familiar API to native elements. Hopefully, this article has provided you with more in-depth insight that you're able to use with your own components.

If you're interested in learning more about Angular, please sign up for our newsletter down below! We don't spam and will notify you when new Angular articles are live! Additionally, if you'd like to ask in-depth questions or chat about anything Angular related, don't forget to join our Discord Server, where we talk code and more!

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.