Search code examples
angularangular5onchangeangular-reactive-formscontrolvalueaccessor

Angular 5 | ReactiveForm with ControlValueAccessor | onChange is not triggered


I have a custom ControlValueAccessor which simply appends a currency symbol on an input.

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

  @Input() class = '';

  currencyValue: string;

  onChange: (value: number) => void;
  onTouched: () => void;

  constructor(
    private currencyPipe: CurrencyPipe
  ) { }

  parseToNumber(currencyString: string) {
    this.onChange(this.currencyPipe.parse(currencyString));
  }

  transformToCurrencyString(value: number): string {
    return this.currencyPipe.transform(value);
  }

  writeValue(value: number): void {
    if (value !== undefined) {
      this.currencyValue = this.transformToCurrencyString(value);
    }
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

}

The CurrencyPipe just parses the string to number and transforms a number to a currency string (with localized decimal seperator and currency symbol).


When I try to use ReactiveForms like this:

<app-currency-input
  name="amount"
  class="value-box"
  formControlName="amount"
  required
></app-currency-input>

... then onChange() is not triggered on manual input.


I have a workaround, where I subscribe to the valueChanges of the control and then do a

control.patchValue(newValue, { emitModelToViewChange: true })

... which successfully triggers the onChange for the ControlValueAccessor. (A patchValue without options would do the same, because true is the default value for this option. I just wanted to point out the culprit here.)

But I would love to use an inbuilt solution which does not resolve in additional needed checks and at least two valueChanges.



A simplified Plunker to try it out: https://embed.plnkr.co/c4YMw87FiZMpN5Gr8w1f/ See the commented out code in src/app.ts.


Solution

  • Try something like this

    import { Component, Input, forwardRef } from '@angular/core';
    import { CurrencyPipe, } from '@angular/common';
    import { ReactiveFormsModule, NG_VALUE_ACCESSOR, FormControl, ControlValueAccessor } from '@angular/forms';
    import { Subscription } from 'rxjs/Subscription';
    
    @Component({
      selector: 'currency-input',
      template: `<input [formControl]="formControl" (blur)="onTouched()"/>`,
      styles: [`h1 { font-family: Lato; }`],
      providers: [
        CurrencyPipe,
        {
          provide: NG_VALUE_ACCESSOR,
          useExisting: forwardRef(() => CurrencyInputComponent),
          multi: true
        }
      ]
    })
    export class CurrencyInputComponent implements ControlValueAccessor {
      constructor(private currencyPipe: CurrencyPipe) { }
    
      private onChange: Function;
      private onTouched: Function;
    
      formControl = new FormControl('');
      subscription: Subscription;
    
      ngOnInit() {
        this.subscription = this.formControl.valueChanges
          .subscribe((v) => {
            this.onChange && this.onChange(this.transform(v));
          })
      }
    
      ngOnDestroy() {
       this.subscription.unsubscribe();
      }
    
      writeValue(val) {
        this.formControl.setValue(this.transform(val), { emitEvent: false });
      }
    
      registerOnChange(fn) {
        this.onChange = fn;
      }
    
      registerOnTouched(fn) {
        this.onTouched = fn;
      }
    
      private transform(val: string) {
        return this.currencyPipe.transform(val, 'USD')
      }
    }
    

    Please note that I'm using the ReactiveFormsModule so you need to import it in your module.

    Live demo