When creating forms in Angular, sometimes you want to have an input that is not a standard text input, select, or checkbox. By implementing the ControlValueAccessor
interface and registering the component as a NG_VALUE_ACCESSOR
, you can integrate your custom form control seamlessly into template-driven or reactive forms just as if it were a native input!
In this article, you will transform a basic star rating input component into a ControlValueAccessor
.
To complete this tutorial, you will need:
This tutorial was verified with Node v16.4.2, npm
v7.18.1, angular
v12.1.1.
First, create a new RatingInputComponent
.
This can be accomplished with @angular/cli
:
- ng generate component rating-input --inline-template --inline-style --skip-tests --flat --prefix
This will add the new component to the app declarations
and produce a rating-input.component.ts
file:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'rating-input',
template: `
<p>
rating-input works!
</p>
`,
styles: [
]
})
export class RatingInputComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
Add the template, styles, and logic:
import { Component } from '@angular/core';
@Component({
selector: 'rating-input',
template: `
<span
<^>*ngFor="let starred of stars; let i = index"
(click)="rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"<^>
>
<ng-container *ngIf="starred; else noStar">⭐</ng-container>
<ng-template #noStar>·</ng-template>
</span>
`,
styles: [`
span {
display: inline-block;
width: 25px;
line-height: 25px;
text-align: center;
cursor: pointer;
}
`]
})
export class RatingInputComponent {
stars: boolean[] = Array(5).fill(false);
get value(): number {
return this.stars.reduce((total, starred) => {
return total + (starred ? 1 : 0);
}, 0);
}
rate(rating: number) {
this.stars = this.stars.map((_, i) => rating > i);
}
}
We can get the value
of the component (0
to 5
) and set the value of the component by calling the rate
function or clicking the number of stars desired.
You can add the component to the application:
<rating-input></rating-input>
And run the application:
- ng serve
And interact with it in a web browser.
This is great, but we can’t just add this input to a form and expect everything to work just yet. We need to make it a ControlValueAccessor
.
In order to make the RatingInputComponent
behave as though it were a native input (and thus, a true custom form control), we need to tell Angular how to do a few things:
writeValue
registerOnChange
registerOnTouched
setDisabledState
These four things make up the ControlValueAccessor
interface, the bridge between a form control and a native element or custom input component. Once our component implements that interface, we need to tell Angular about it by providing it as a NG_VALUE_ACCESSOR
so that it can be used.
Revisit rating-input.component.ts
in your code editor and make the following changes:
import { Component, forwardRef, HostBinding, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'rating-input',
template: `
<span
*ngFor="let starred of stars; let i = index"
(click)="onTouched(); rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))"
>
<ng-container *ngIf="starred; else noStar">⭐</ng-container>
<ng-template #noStar>·</ng-template>
</span>
`,
styles: [`
span {
display: inline-block;
width: 25px;
line-height: 25px;
text-align: center;
cursor: pointer;
}
`],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingInputComponent),
multi: true
}
]
})
export class RatingInputComponent implements ControlValueAccessor {
stars: boolean[] = Array(5).fill(false);
// Allow the input to be disabled, and when it is make it somewhat transparent.
@Input() disabled = false;
@HostBinding('style.opacity')
get opacity() {
return this.disabled ? 0.25 : 1;
}
// Function to call when the rating changes.
onChange = (rating: number) => {};
// Function to call when the input is touched (when a star is clicked).
onTouched = () => {};
get value(): number {
return this.stars.reduce((total, starred) => {
return total + (starred ? 1 : 0);
}, 0);
}
rate(rating: number) {
if (!this.disabled) {
this.writeValue(rating);
}
}
// Allows Angular to update the model (rating).
// Update the model and changes needed for the view here.
writeValue(rating: number): void {
this.stars = this.stars.map((_, i) => rating > i);
this.onChange(this.value);
}
// Allows Angular to register a function to call when the model (rating) changes.
// Save the function as a property to call later here.
registerOnChange(fn: (rating: number) => void): void {
this.onChange = fn;
}
// Allows Angular to register a function to call when the input has been touched.
// Save the function as a property to call later here.
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
// Allows Angular to disable the input.
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
This code will allow the input to be disabled, and when it is make it somewhat transparent.
Run the application:
- ng serve
And interact with it in a web browser.
You can also disable the input controls:
<rating-input [disabled]="true"></rating-input>
We can now say that our RatingInputComponent
is a custom form component! It will work just like any other native input (Angular provides the ControlValueAccessors
for those!) in template-driven or reactive forms.
In this article, you transformed a basic star rating input component into a ControlValueAccessor
.
You’ll notice that now:
ngModel
just “works”.ngModel
, such as ng-dirty
and ng-touched
classes.If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Thanks for the great article! Two remarks: It’s tagged on AngularJS, should be Angular. And second, what would have helped me as well would have been a simpler example and an explanation what is boilerplate and what can always stay the same. I have written down examples like this now here https://www.tsmean.com/articles/angular/angular-control-value-accessor-example/