Tutorial

How To Use ControlValueAccessor to Create Custom Form Controls in Angular

Updated on July 12, 2021
author

Mark P. Kennedy

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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Mark P. Kennedy

author

Still looking for an answer?

Ask a questionSearch for more help

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/

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.