Angular 2 — Implementing a Custom Form Control with Validation (JSON-Input)

Preview of the JSON-Input Form Control with validation

This post will go over how to create a custom input component which supports validation and is compatible with both ngModel forms and Reactive Forms.

As an example we will create a custom textarea component, where the validation checks that the user input is a valid JSON string.

The validated result will be displayed outside of the form via binding, which will only update when the user input is valid.

You can go straight to the code right here: https://plnkr.co/edit/iFXRkJWVZ9tQ9i6mxmuf?p=preview

The steps below go over:

  • Creating the component
  • Implementing the Control Value Accessor interface
  • Registering the provider
  • Implementing custom validation
  • Using the component

Create the Component

Setup the Component

Lets create the component with some functionality — something like this:

import { Component } from '@angular/core';
@Component({
selector: 'json-input',
template:
`
<textarea
[value]="jsonString"
(change)="onChange($event)"
(keyup)="onChange($event)">

</textarea>

`
})
export class JsonInputComponent {

private jsonString: string;
private parseError: boolean;
private data: any;
    // change events from the textarea
private onChange(event) {

// get value from text area
let newValue = event.target.value;
try {
// parse it to json
this.data = JSON.parse(newValue);
this.parseError = false;
} catch (ex) {
// set parse error if it fails
this.parseError = true;
}
}
}

We will listen to two events, change and keyup, to update the model and validate it appropriately..

To make it usable with our form we have to now implement the ControlValueAccessor interface.

Implement Control Value Accessor

The ControlValueAccessor interface will provide us with methods that will interface operations for our control to write values to the native browser DOM.

A form component can optionally implement a Control Value Accessor, enabling it to read values and listen for changes. This interface is used in the NgModel and FormControlName directives. Angular comes with a standard provider for handling form controls called DefaultValueAccessor that works for Inputs, Checkboxes and standard browser elements, we need to add our own implementation for custom controls.

Three methods will need to be implemented to satisfy this interface:

    /**
* Write a new value to the element.
*/
writeValue(obj: any): void;
    /**
* Set the function to be called
* when the control receives a change event.
*/
registerOnChange(fn: any): void;
    /**
* Set the function to be called
* when the control receives a touch event.
*/
registerOnTouched(fn: any): void;

According to the interface documentation, we need to initialise the value using writeValue(obj: any) then register a function to emit changes to using registerOnChange(fn: any). The last method we won’t really need as it handles touch events, but to satisfy the interface it can just be implemented with an empty block.

This is how we will implement the interface in our component:

import { Component, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
@Component({
selector: 'json-input',
template:
`
<textarea
[value]="jsonString"
(change)="onChange($event)"
(keyup)="onChange($event)">

</textarea>
`
})
export class JsonInputComponent
implements ControlValueAccessor {
    private jsonString: string;
private parseError: boolean;
private data: any;
    // the method set in registerOnChange, it is just 
// a placeholder for a method that takes one parameter,
// we use it to emit changes back to the form

private propagateChange = (_: any) => { };
    // this is the initial value set to the component
public writeValue(obj: any) {
if (obj) {
this.data = obj;
// this will format it with 4 character spacing
this.jsonString =
JSON.stringify(this.data, undefined, 4);
}
}
    // registers 'fn' that will be fired when changes are made
// this is how we emit the changes back to the form

public registerOnChange(fn: any) {
this.propagateChange = fn;
}
    // not used, used for touch input
public registerOnTouched() { }
    // change events from the textarea
private onChange(event) {

.....
        // update the form
this.propagateChange(this.data);

}
}

To summarise this change:

  1. Ensure the component implements ControlValueAccessor
  2. Initialise the value for our text-area by converting it to a text string, and formatting it.
  3. Register our changes function to emit changes to.
  4. Update onChange to call the registered function with the updated value.

The interface is just a ‘contractual obligation’ to provide methods that can be called somewhere else. So we now need to register our component as value accessor provider so Angular knows that it can call these methods when used in a form.

Register the provider

Since our custom component now implements the ControlValueAccessor then we can register it as a provider.

This how it is done:

import { Component, Input, forwardRef } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR
} from '@angular/forms';
@Component({
selector: 'json-input',
template:
`
<textarea
[value]="jsonString"
(change)="onChange($event)"
(keyup)="onChange($event)">

</textarea>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => JsonInputComponent),
multi: true,
}

})
export class JsonInputComponent implements ControlValueAccessor
{
...
}

We will provide NG_VALUE_ACCESSOR with the class JsonInputComponent. We need to use forwardRef since our Component is not defined at this point. Lastly we use multi: true because NG_VALUE_ACCESSOR can actually have multiple providers registered with the same component. This will make more sense when we go through validation.

At this point we’re actually done, but there isn’t any validation. So lets go through it quickly.

Implement Custom Validation

Now we need to implement the Validatorinterface which only required one method to be implement. The method is conveniently called validate

    validate(c: AbstractControl): {
[key: string]: any;
};

This method takes in the form control and returns a key-value pair of validated items. If the form control is valid, it returns null.

In our case this is how we will implement it:

    // returns null when valid else the validation object 
// in this case we're checking if the json parsing has
// passed or failed from the onChange method

public validate(c: FormControl) {
return (!this.parseError) ? null : {
jsonParseError: {
valid: false,
},
};
}

We are just checking for the parseError object in this case since we know if the JSON has parsed correctly or not when we update the text.

We need to register the validator as a provider like how we registered the ControlValueAccessor:

import { Component, Input, forwardRef } from '@angular/core';
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
FormControl,
Validator
} from '@angular/forms';
@Component({
selector: 'json-input',
template:
`
<textarea
[value]="jsonString"
(change)="onChange($event)"
(keyup)="onChange($event)">

</textarea>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => JsonInputComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => JsonInputComponent),
multi: true,
}

})
export class JsonInputComponent
implements ControlValueAccessor, Validator {
    ...
}

With NG_VALIDATORS we can also have multiple validation, such as the built-in validators like required in addition of our custom validation we have implemented.

Now lets see how we can use it with our form.

Making use of our custom Form Control

Here are two examples of how to use our control using the traditional NgModel form or Reactive Form.

Model bound form (NgModel):

<form #form="ngForm">
<json-input [(ngModel)]="result" name="result"></json-input>
</form>
<p>form is valid: {{ form.valid ? 'true' : 'false' }}</p>
<p>Value:</p>
<pre>{{ result | json }}</pre>

Reactive Form

Component (just to intialise the form)

import {Component } from '@angular/core';
import {FormGroup, FormBuilder} from '@angular/forms';
@Component({
selector: 'material-app',
templateUrl: 'app.component.html'
})
export class AppComponent {

public result = {};
public reactiveForm: FormGroup;

constructor(private fb: FormBuilder) {
this.reactiveForm = this.fb.group({
result: [{"test123":"test456"}]
})
}
}

Template

<form [formGroup]="reactiveForm">
<json-input formControlName="result"></json-input>
</form>
<p>form is valid: {{ reactiveForm.valid ? 'true' : 'false' }}</p>
<p>Value:</p>
<pre>{{ reactiveForm.value | json }}</pre>

Here is the full plunk! https://plnkr.co/edit/iFXRkJWVZ9tQ9i6mxmuf?p=preview

Hope that helps! Feel free to comment if you have any input or questions.