Custom Form Controls In Angular

Omotayo Oluwatosin
4 min readOct 23, 2023

--

Anyone who has created forms with angular for while would have come across scenarios where he/she needs to add different behaviour(s) to the regular inputs provided by the native html inputs or form inputs offered by a UI libray (like Jquery, angular material etc).

Angular form directives (like ngModel and FormControls) provide facilites that helps bind HTML form elements (like <input>, <text-area>, <select> elements) to a form group.

However, there are times where you want to use a non-standard HTML element (such as a div, toggle, button etc) or even a custom angular component as a form Contorl that you are able to bind to formGroup. Angular allos support for this through the ControlValueAccessor.

In this tutorial we will be creating a custom form control that converts a native number input element (<input type=”number”>) into a custom form controls that has binary value 0 or 1.

The idea is that we want to create a custom form control that renders a number box and when a user enters a number less than 10 the form value will be false and if the number entered is greater than or equal to 10 the control value will be true.

The angular ControlValueAccessor defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM.

Here is a sample angular formGroup with a custom component that we would like to be able to bind to a formGroup:

<form [formGroup]="form">
<div>
<input placeholder="Product Name" formControlName="product"/>
</div>
<div>
<app-custom-input formControlName="price" costPrice="10" ></app-custom-input>
</div>
<div>
<br>
<button (click)="getResult()">Check For Profit</button>
</div>
</form>

The Componet.ts class is shown below:

export class App implements OnInit {
name = 'Angular';
form!: FormGroup;
result: string = '';
constructor(private fb: FormBuilder) {}

ngOnInit(): void {
this.form = this.fb.group({
product: ['', Validators.required],
price: [false, Validators.required],
});
}

getResult() {
const formValue = this.form.value;

this.result = `${formValue.product} is ${
formValue.price ? ' Profitable' : 'loss'
}`;
}
}

The Custom input component.ts is shown below:

export class CustomInputComponent implements OnInit, OnChanges {
@Input('costPrice') costPrice: string = '0';
value!: boolean;

disabled!: boolean;
constructor() {}

ngOnInit() {}

valueChanged(val: string) {
this.onChange(parseInt(val) > parseInt(this.costPrice));
}
onChange = (value: Boolean) => {};



ngOnChanges(changes: SimpleChanges): void {}


}

The HTML is shown below:

<div>
<br />
<input
type="number"
#val
(change)="valueChanged(val.value)"
placeholder="Enter Price Sold"
/>
</div>

Whithout the Custom input compnent implemeting the ControlValueAccessor it cannot be used as a form control. Adding a formControlName directive to it in the form would result in an error: ERROR Error: NG01203: No value accessor for form control name: ‘price’

<app-custom-input formControlName="price" costPrice="10" ></app-custom-input>

To add form binding support to the <app-custom> component it has to implement the ControlValueAccessor interface.

The ControlValueAccessor interface require that the following methods be implemented:

interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
  • writeValue: This method is trigered by the FORMS API when the value is set programatically (for example via the patchValue or the setValue API)
  • registerOnTouched: This methods provides the support for the custom form control to notify the parent formGroup when it has been ‘touched’.
  • registerOnChange: This methods provides the support for the custom form control to notify the parent formGroup when it value changes in the UI.
  • setDisabledState: When implemented by the cutom component, this method will be triggered by the FORMS API when form control status changes to or from ‘DISABLED’.

The writeValue is implemented as shown below:

 writeValue(value: number): void {
this.value = value > parseInt(this.costPrice);
}

This method is triggered by the forms APIS any time it wants to set the value of the formControl. Here, we will take the value and check if it’s greater than the costPrice and set the value propery to true or false based on this condition.

The registerOnTouched is implemented as shown below:

// declare a property to track the 'touched' state
private touched = false;

// intitialize an empty method to that would later be set to the
// callback function passed from the FORM API via the registerOnTouched
onTouched: any = () => {};

// this method will be triggered by the forms api and a call back function fn
// is passed as a parameter. This function notify the FORMS api when the touched
// value changes to true
registerOnTouched(fn: any): void {

this.onTouched = fn;
}
private markAsTouched(): void{
if(!this.touched){
this.touched = true;
this.onTouched();
}
}

valueChanged(val: string) {
this.onChange(parseInt(val) > parseInt(this.costPrice));

this.markAsTouched();
}

The registerOnChange method is implemented as shown below:

onChange = (value: Boolean) => {};

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

valueChanged(val: string) {
this.onChange(parseInt(val) > parseInt(this.costPrice));

.........
}

This method takes in a call back function from the FORMS API. The callback parameter is then stored/referenced by the onChange method. Any time we want to notify the parant form about the change in value we then invoke the method as shown in the valueChanged method.

The setDisabledState is implemented as shown below:

disabled!: boolean;

setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}

This method allows the forms API notify the custom component about the disabled state of the compoment.

Finally, the last required step after implementing the ControlValueAccessor is to register it in the dependency injection system as a value accessor in NG_VALUE_ACCESSOR list. This is implemented below:

@Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomInputComponent),
},
],
})

Once this is done the customComponent can now be used as a formControl.

You can check the complete code on stackblitz for a working solution

https://stackblitz.com/edit/stackblitz-starters-xymizt?file=src%2Fcustom-input%2Fcustom-input.component.ts

--

--