28
How to Make a Component Compatible with Angular Forms?
The Angular framework provides 2 ways of creating forms:
- Reactive Forms
- Template Driven
The content of this article is valid to both of them.
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
So, this is an interface provided by Angular that will allow us to make our components compatibles with Angular Forms.
This element is essential as part of implementing a form-compatible component. Its usage is mainly to register the component. More info
For the aim of this example, let's imagine that we want to build a component that allows you to select your mood, just like this:
Component Code:
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
export enum Mood {
Red = 'red',
Green = 'green',
}
@Component({
selector: 'app-custom-component',
templateUrl: './custom-component.component.html',
styleUrls: ['./custom-component.component.scss'],
providers: [
// This part is very important to register the class as a ControlValueAccessor one
{
provide: NG_VALUE_ACCESSOR,
// This reference the class that implements Control Value Accessor
useExisting: forwardRef(() => CustomComponentComponent),
multi: true,
},
],
})
export class CustomComponentComponent implements ControlValueAccessor {
/* Reference to the Enum to be used in the template */
readonly moodRef = Mood;
disable: boolean = false;
selected: Mood = Mood.Green;
updateState(selectedItem: Mood): void {
this.selected = selectedItem; // Updating internal state
this.onChange(this.selected); // 'publish' the new state
}
/***********************************************************************
* Control Value Accessor Implementation
***********************************************************************/
private onChange: any;
private onTouch: any;
// Invoked by angular - update internal state
writeValue(obj: any): void {
this.selected = obj;
}
// Invoked by angular - callback function for changes
registerOnChange(fn: any): void {
this.onChange = fn;
}
// Invoked by angular - callback function for touch events
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Invoked by angular - update disabled state
setDisabledState?(isDisabled: boolean): void {
this.disable = isDisabled;
}
}
Template code:
<p>How do you feel?</p>
<ng-container *ngIf="!disable; else disabledTemplate">
<button
[ngClass]="{
custom__button__red: true,
'custom__button--selected': selected === moodRef.Red
}"
(click)="updateState(moodRef.Red)"
>
Red
</button>
<button
[ngClass]="{
custom__button__green: true,
'custom__button--selected': selected === moodRef.Green
}"
(click)="updateState(moodRef.Green)"
>
Green
</button>
</ng-container>
<ng-template #disabledTemplate>
<p>I'm disabled</p>
</ng-template>
SCSS:
.custom__button {
&__red {
background-color: red;
}
&__green {
background-color: green;
}
&--selected {
margin: 1em;
border: solid 5px black;
}
}
The component is compatible with the directives: formControlName and formControl.
<form [formGroup]="formGroup">
<app-custom-component
[formControlName]="controlsRef.Mood"
></app-custom-component>
</form>
The component is also compatible with ngModel property:
<form>
<app-custom-component
[disabled]="disabled"
[(ngModel)]="selectedMood"
[ngModelOptions]="{ standalone: true }"
></app-custom-component>
</form>
The detailed implementation is in one of my Github repos:
28