top of page

When to Use Angular ControlValueAccessor and What’s the Difference Without It?

ControlValueAccessor provides the ability to use FormControl with any component that implements this interface. This gives a lot of development flexibility and is a useful feature in Angular Forms.

There are several ways with some boilerplate to control decoration and usage. Let’s look at it and find the differences with an example.


Example

Let’s say we need to create a counter component and use its value through FormControl.


Counter component code with ControlValueAccessor:

@Component({
    selector: 'app-counter-cva',
    templateUrl: './counter-cva.component.html',
    styleUrls: ['./counter-cva.component.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(()=>CounterCVAComponent),
        multi: true,
    }]
})
        
export class CounterCVAComponent<T> implements OnInit, 
                                ControlValueAccessor
{
      public control!: FormControl;
      
      protected readonly destroy = new Subject<void>();
      
      constructor(@Inject(Injector) private injector: Injector){}
      
      public ngOnInit(): void
      {
          this.setComponentControl();
      }
      
      public changeValueTo(value: number): void
      {
          this.control.patchValue((this.control.value??0)+value);
      }
      
      public writeValue(value: T): void
      {
          this.onChange(value);
      }
      
      public registerOnChange(fn: (value: T|null)=>T): void
      {
          this.onChange=fn;
      }
      
      public registerOnTouched(fn: ()=>void): void
      {
          this.onTouch=fn;
      }
      
      public onChange=(value: T|null): T|null=>value;
      
      public onTouch=(): void=>{};
      
      public ngOnDestroy(): void
      {
          this.destroy.next();
          this.destroy.complete();
      }
      
      private setComponentControl(): void
      {
      try
      {
          const formControl=this.injector.get(NgControl);
      
          switch(formControl.constructor)
          {
              case NgModel: 
              {
                  const{ control, update } = formControl as NgModel;
                  
                  this.control=control;
                  
                  this.control.valueChanges
                      .pipe(
                          tap((value: T)=>update.emit(value)),
                          takeUntil(this.destroy),
                      )
                      .subscribe();
                  break;
              }
              case FormControlName: 
              {
                  this.control=this.injector.get(FormGroupDirective)
                          .getControl(formControlasFormControlName);
              break;
              }
              default: {
                  this.control=(formControl as FormControlDirective)
                                                  .form as FormControl;
                  break;
               }
             }
          }catch(error)
          {
              this.control = new FormControl()          
          }
    }
}

<button(click)="changeValueTo(-1)">-</button>
<div>{{ control.value || 0 }}</div>
<button(click)="changeValueTo(1)">+</button>

A lot of code, yeah?


It’s too complicated to use in this case. What is better — to make an abstract class with ControlValueAccessor boilerplate and inherit from it. But that’s actually not necessary at this time.

Can we just make the component simpler? Sure:

@Component({
    selector: 'app-counter-simple',
    templateUrl: './simple.component.html',
    styleUrls: ['./simple.component.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterSimpleComponent
{  
    @Input()
    control: FormControl;
    
    public changeValueTo(value: number): void
    {
        this.control.patchValue((this.control.value ?? 0) + value);
    }
}

Really simple component. But what restrictions do we get with this simplicity?

  • First of all, we have to pass control directly, not with FormControlName or ngModel:

<app-counter-simple[control]="control"></app-counter-simple>

  • Second, we lose template validators and can’t use it like this:

<app-counter-simple[control]="control"[pattern]="^[1-9]\d*$"></app-counter-simple>


Improvements

Let’s pass formControl directly to @Input for use as the Angular directive. But if we do this, there will be an error, — Error: No value accessor for form control with the unspecified name attribute. This error can be fixed with another directive ngDefaultControl:

<app-counter-simple [formControl]="control" pattern="^[1-9]\d*$" ngDefaultControl></app-counter-simple>

Now it works perfectly with template validators and has no errors.


Another trick is if the control is used inside a FormGroup — we have to pass control with a function. But instead of that, just use a pure pipe with no recalculation on every ChangeDetection pass. For example:

@Pipe({
    name: 'getControlFrom'
})
export class GetControlFromPipe implements PipeTransform
{
   public transform(value: FormGroup, name: string): FormControl
   {
      return(value.get(name) as FormControl) ?? new FormControl();
   }
}

Finally, our counter control becomes:

<app-counter-simple[formControl]="form | getControlFrom: 'counter'"pattern="^[1-9]\d*$"ngDefaultControl></app-counter-simple>

Hope this article was helpful to you.



Source: medium - Anton Marinenko


The Tech Platform

bottom of page