Torsten Müller

Angular custom form field for Reactive Forms

published Oct 5th, 2019

Developing my music library example app, I came across a problem with the HTML time input field: It is limited to accept entries on a 24-hour clock schema, but some of the songs in my library are over 23:59 in length (some over 30 minutes, don’t ask). Re-purposing the HTML time field was not an option and so I took the opportunity to learn about building my own form field with the following properties:

  1. Usable with Angular Reactive Forms
  2. Allow time values up to 99:59 minutes
  3. As a first approximation, I will not validate entries to have fewer than 60 seconds in the “seconds” field, as my foremost goal is to figure out how to write my own two-form-field component.

Setting up a Custom Form Field

Angular provides the ControlValueAccessor interface with the following properties:

Method name Parameter Functionality
writeValue any Used to prepopulate the form field(s). Takes a time value, in our case a string, and extracts the minutes and seconds values
registerOnChange function Gets passed a function that should be called when the value in the custom form field changes
registerOnTouched function Gets passed a function that should be called when the field has been touched
setDisabledState boolean This optional method is called by Angular when the active status of the field should change

From this interface definition, we can start writing our FormTimeEntryComponent and provide stubs for the required methods. We also will want to have access to the template of this component in order to have access to the form fields. Therefore, we are defining a @ViewChild which will store a reference to the template of this component in the durationElement property. The first iteration of the component class then looks like this:

form-time-entry.component.ts
export class FormTimeEntryComponent implements ControlValueAccessor {

  @ViewChild('durationEntry', {read: ElementRef, static: true})
  durationElement: ElementRef;

  private minuteValue = '';
  private secondValue = '';

  public getLengthValue(): void {}

  public registerOnChange(fn: any): void {}

  public registerOnTouched(fn: any): void {}

  public writeValue(timeInMinAndSec: string): void {}

The rudimentary template for our new form input will be making use of the @ViewChild identifier durationEntry to set up the reference and include two separate “number” input fields, one each for the minutes and seconds:

form-time-entry.component.html
<div class="timeduration-input" #durationEntry>
    <div class="timeduration-formcontainer">
        <input type="number" name="time-minutes" maxlength="2" (keyup)="getLengthValue()" />:
        <input type="number" name="time-seconds" minlength="2" maxlength="2" (keyup)="getLengthValue()"/>
    </div>
</div>

Processing user input

From the HTML markup you can tell that we’ve set up an event handler for onKeyUp events, which will call the getLengthValue() method in the component and thus provides for us a way to get the latest value in the form field and pass it to Angular. To do that, we need to implement the getLengthValue() method, which will look like this:

form-time-entry.component.ts
  public getLengthValue(): void {
    this.minuteValue = this.getFormFieldVal(0);
    this.secondValue = this.getFormFieldVal(1);
    const timeValue = `${this.minuteValue}:${this.secondValue}`;
  }

  private getFormFieldVal(fieldNum: number): string {
    const fieldVal = this.durationElement.nativeElement.childNodes[0].children[fieldNum].value;
    return this.forceDoubleDigits(fieldVal);
  }

  private forceDoubleDigits(numberValue: string): string {
    return `00${ numberValue }`.substr(-2);
  }

This code is all pretty straightforward. We

  1. use DOM traversal to retrieve a reference to the input field in the markup
  2. get the current value in both form fields
  3. pad both values with leading zeroes and return a string from forceDoubleDigits()
  4. concatenate the padded input from both fields to a time value in the format MM:SS

What’s still missing is the hookup to the Reactive Form instance, so that the changes get propagated when they are detected and the form instance gets updated with the new value. This will look like this:

form-time-entry.component.ts
  private propagateChange;

  public getLengthValue(): void {
    this.minuteValue = this.getFormFieldVal(0);
    this.secondValue = this.getFormFieldVal(1);
    const timeValue = `${this.minuteValue}:${this.secondValue}`;
    this.propagateChange(timeValue);
  }

  ...

  public registerOnChange(fn: any): void {
    this.propagateChange = fn;
  }

This snippet contains the method registerOnChange(), which as mentioned earlier is part of the interface and accepts as parameter a function which will update the data in the form instance (and thereby also in the form fields). In this case, we’re storing a reference to the function Angular provides us in the propagateChange property (set up in line 1, populated in line 13) and call that method in the last line of getLengthValue() in line 7 with our time value.

To summarize, on every keypress, the following happens in succession:

  1. They keyup event triggers the execution of getLengthValue()
  2. getLengthValue() reads and formats the current value in the field and
  3. passes the value to Angular via a function provided to us by Angular, registerOnChange()

Prepopulating the input fields

The implementation works well, as long as you only want to add new records. For editing a record, we need to accept a value from Angular and pre-populate the corresponding fields accordingly. For that, we need to implement the writeValue() method from the interface.

In this method, we take the value provided as a string and extract the respective minute and second values from it, to set the field values in our component. Via the property binding on the input fields, we reflect the loaded value in the fields. And that is pretty much everything there is to it, so we get the following implementation for writeValue():

form-time-entry.component.ts
public writeValue(timeinMinAndSec: string): void {
  const [minutes, seconds, ...rest] = timeinMinAndSec.split(':'); // Format mm:ss
  this.minuteValue = minutes;
  this.secondValue = seconds;
}

Making Angular aware of the new form field

We now have a functioning form element, but Angular does not yet know about it, so we need to tell it. And this is where it gets a little tricky, as we need to think about the order in which all the various pieces of the Angular app are created and instantiated.

Without going into too much detail, object references are hoisted, but their corresponding instances are not, so that we can run into issues where our form element is trying to be created, but Angular doesn’t yet know about how to do that. For more information on this topic, check out Multi Providers in Angular by Pascal Precht and Dependency Injection Providers

To get around that problem, we need to add providers to the @Component decorator in the following way (lines 5-9):

form-time-entry.component.ts
@Component({
  selector: 'app-form-time-entry',
  templateUrl: './form-time-entry.component.html',
  styleUrls: ['./form-time-entry.component.css'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => FormTimeEntryComponent),
    multi: true,
  }]
})
export class FormTimeEntryComponent implements ControlValueAccessor { ... }

Finally, the following HTML snippet demonstrates how one would use this custom form element in a reactive form template. You’ll notice that there is no difference in its markup from any predefined HTML form elements:

container.component.ts
<div class="form-element">
  <label for="length">Length</label>
  <app-form-time-entry formControlName="length" id="length" name="length"></app-form-time-entry>
</div>

Summary

In this post, we have looked at how to create a custom form element and learned how to:

  1. Create and manage a form field with multiple input elements
  2. Integrate the form field into Angular Reactive Forms like a regular form element
  3. Integrate it with the Angular DI system
  4. Make it behave like a normal form element by sending and receiving the data it should represent.