Angular custom form field for Reactive Forms
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:
- Usable with Angular Reactive Forms
- Allow time values up to 99:59 minutes
- 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:
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:
<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:
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
- use DOM traversal to retrieve a reference to the input field in the markup
- get the current value in both form fields
- pad both values with leading zeroes and return a string from
forceDoubleDigits()
- 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:
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:
- They
keyup
event triggers the execution ofgetLengthValue()
getLengthValue()
reads and formats the current value in the field and- 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()
:
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):
@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:
<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:
- Create and manage a form field with multiple input elements
- Integrate the form field into Angular Reactive Forms like a regular form element
- Integrate it with the Angular DI system
- Make it behave like a normal form element by sending and receiving the data it should represent.