Reusable Form Controls in Angular

using ControlValueAccessor

Syed M. Taha (@smtaha512)

Software Engineer

  • Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM.

ControlValueAccessor

  • Implement this interface to create a custom form control directive that integrates with Angular forms.

@smtaha512

Learning by Example

@smtaha512

Rating Component

@smtaha512

<div>
  <img 
    class="star" 
    [ngClass]="{ 
        disabled: isDisabled, 
        selected: 
            selectedRating?.star !== null && 
            selectedRating?.star >= i
    }"
    *ngFor="let item of ratings; let i=index;" 
    src="/assets/star.svg" 
    (click)="onStarClick(item)" 
  />
</div>
<p>{{ selectedRating?.text }}</p>

Rating component

@smtaha512

@Component({
  selector: 'app-rating',
  templateUrl: './rating.component.html',
  styleUrls: ['./rating.component.scss']
})
export class RatingComponent implements OnInit {
  ratings = [
    { star: 0, text: 'Worst' },
    { star: 1, text: 'Bad' },
    { star: 2, text: 'Satisfactory' },
    { star: 3, text: 'Good' },
    { star: 4, text: 'Best' }
  ];

  @Output() ratingChange = new EventEmitter();
  selectedRating = this.ratings[0];
  isDisabled: boolean;
  constructor() {}

  ngOnInit() {}

  onStarClick(selectedRating: { star: number; text: string }) {
    this.selectedRating = selectedRating;
    this.ratingChange.emit(selectedRating.star);
  }
}

Rating component

@smtaha512

<app-rating (ratingChange)="onRatingChange($event)">
</app-rating>
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  private readonly form = new FormGroup({
    rating: new FormControl()
  });

  onRatingChange(event) {
    this.rating.setValue(event);
  }

  get rating() {
    return this.form.get('rating');
  }
}

Parent component

@smtaha512

All set. We don't need ControlValueAccessor

@smtaha512

No, we do. Why?

  • Validations???
  • Managing extra code everywhere we use rating component for:
    • Setting values
    • Change listening

@smtaha512

Coming towards ControlValueAccessor

@smtaha512

To make any component / directive ControlValueAccessor, we have to:

  1. Register itself as NG_VALUE_ACCESSOR
  2. Implement ControlValueAccessor interface

@smtaha512

Registering as NG_VALUE_ACCESSOR

@Component({
  selector: 'app-rating',
  templateUrl: './rating.component.html',
  styleUrls: ['./rating.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingComponent),
      multi: true
    }
  ]
})
export class RatingComponent { }

@smtaha512

Implement ControlValueAccessor interface

@smtaha512

interface ControlValueAccessor {
  writeValue(obj: any): void;
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
  setDisabledState?(isDisabled: boolean): void;
}

@smtaha512

This method is called by the forms API to write to the view when programmatic changes from model to view are requested.

Native / Custom Input

CVA

Angular Form

writeValue(value)

Communication

interface

specific to

form control

writeValue(value: any): void

@smtaha512

registerOnChange(fn: (_: any) => void): void  

This method is called by the forms API on initialization to update the form model when values propagate from the view to the model.

Native / Custom Input

CVA

Angular Form

registerOnChange(fn)

Communication

interface

specific to

form control

fn: (_: any) => void

@smtaha512

registerOnTouched(fn: any): void

This method is called by the forms API on initialization to update the form model on blur.

Native / Custom Input

CVA

Angular Form

registerOnTouched(fn)

Communication

interface

specific to

form control

fn: (_: any) => void

@smtaha512

setDisabledState?(isDisabled: boolean): void

Function that is called by the forms API when the control status changes to or from 'DISABLED'. Depending on the status, it enables or disables the appropriate DOM element.

Native / Custom Input

CVA

Angular Form

setDisabledState(isDisabled)

Communication

interface

specific to

form control

@smtaha512

Implement ControlValueAccessor interface

@Component({})
export class RatingComponent implement ControlValueAccessor { 
  value: any;
  disable: boolean;

  onChange: any = () => { };
  onTouched: any = () => { };

  writeValue(obj: any): void {
    this.value = obj;
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = onTouched;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
}

@smtaha512

Resources:

Thank you

@smtaha512

s.m.taha10@gmail.com

Custom Form Controls in Angular

By Syed M. Taha

Custom Form Controls in Angular

  • 17