// Tutorial //

How To Use ControlValueAccessor to Create Custom Form Controls in Angular

Published on July 24, 2017 · Updated on July 12, 2021
Default avatar
By Mark P. Kennedy
Developer and author at DigitalOcean.
How To Use ControlValueAccessor to Create Custom Form Controls in Angular

Introduction

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!

Animated gif of the Rating Input Component example selecting a different number of stars.

In this article, you will transform a basic star rating input component into a ControlValueAccessor.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v16.4.2, npm v7.18.1, angular v12.1.1.

Step 1 — Setting Up the Project

First, create a new RatingInputComponent.

This can be accomplished with @angular/cli:

  1. 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:

src/app/rating-input.component.ts
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:

src/app/rating-input.component.ts
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:

src/app/app.component.html
<rating-input></rating-input>

And run the application:

  1. 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.

Step 2— Creating a Custom Form Control

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:

  • Write a value to the input - writeValue
  • Register a function to tell Angular when the value of the input changes - registerOnChange
  • Register a function to tell Angular when the input has been touched - registerOnTouched
  • Disable the input - 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:

src/app/rating-input.component.ts
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:

  1. ng serve

And interact with it in a web browser.

You can also disable the input controls:

src/app/app.component.html
<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.

Conclusion

In this article, you transformed a basic star rating input component into a ControlValueAccessor.

You’ll notice that now:

If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.


Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.

Sign up
About the authors
Default avatar
Developer and author at DigitalOcean.

Still looking for an answer?

Was this helpful?
1 Comments

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/