Как я могу подписаться на изменения значения FormControl в директиве Angular ControlValueAccessor

#angular #angular-reactive-forms #angular-directive

#angular #угловые реактивные формы #angular-директива

Вопрос:

У меня есть директива в Angular, которая реализует ControlValueAccessor. Директива, похоже, работает в стороне от получения значений, установленных в FormControl.setValue(). Как мне получить это обновление значения в директиве?

Вот моя директива

 import {
  Directive,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostListener,
  Input,
  Output,
  Renderer2
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {CurrencyPipe} from '@angular/common';

@Directive({
  selector: '[currency-input]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CurrencyInputDirective),
      multi: true
    }
  ]
})
export class CurrencyInputDirective implements ControlValueAccessor {

  // TODO: Allow null value
  _currencyString: string = '';

  @Input() ciDecimals = 2;
  @Input() ciCurrencyCode = 'USD';
  @Input() ciMaximumAmount = 10000;

  // Pass true if using formControl or formControlName
  @Input() ciModelDrivenForm = false;
  @Output() ciExceededMax: EventEmitter<any> = new EventEmitter();

  onChangeCallback = (_: any) => {};
  onTouchedCallback = () => {};

  @HostListener('onNgModelChange') onNgModelChange() {
    // Never runs
    console.log("in ng model change");
  }

  @HostListener('input', ['$event']) onInput(value: any) {
    // Never runs
    console.log("in input");
  }

  @HostListener('blur', []) onBlur() {
    this.onTouchedCallback();
  }

  @HostListener('focus', []) onFocus() {
    this.onTouchedCallback();
  }

  @HostListener('keydown', ['$event']) onKeyDown(e: KeyboardEvent) {
    switch (e.key) {
      case 'Backspace':
        this.handleBackspaceKeyPress(e);
        break;
      default:
        if (isNaN( e.key)) {
          e.preventDefault();
        } else {
          this.handleNumericKeyPress(e);
        }
        break;
    }
  }

  constructor (
    private _renderer: Renderer2,
    private _elementRef: ElementRef,
    private _currencyPipe: CurrencyPipe
  ) {}

  writeValue(value: any) {
    this._renderer.setProperty(this._elementRef.nativeElement, 'value', this.buildElementValue());
    this.onChangeCallback(this.buildControlValue());
  }

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

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

  setDisabledState(isDisabled: boolean) {
    this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
  }

  private handleBackspaceKeyPress(e: KeyboardEvent) {
    e.preventDefault();
    // Remove one digit
    if (this._currencyString.length > 0) {
      this._currencyString = this._currencyString.slice(0, this._currencyString.length - 1);
    }

    this.writeValue(this._currencyString);
  }

  private handleNumericKeyPress(e: KeyboardEvent) {
    e.preventDefault();

    const newCurrencyString = this._currencyString   e.key;
    const currencyValue: number = this.convertCurrencyStringToCurrencyValue(newCurrencyString);

    if (currencyValue > this.ciMaximumAmount) {
      setTimeout(() => {
        this.ciExceededMax.emit({ amount: currencyValue, maxAmount: this.ciMaximumAmount });
      }, 1);
      return;
    }

    this._currencyString = newCurrencyString;
    this.writeValue(this._currencyString);
  }

  private buildElementValue() {
    const currencyDecimals = '1.'   this.ciDecimals   '-'   this.ciDecimals;

    const retVal: string = !this._currencyString ? null : this._currencyPipe.transform(
      this.convertCurrencyStringToCurrencyValue(this._currencyString),
      this.ciCurrencyCode,
      'symbol',
      currencyDecimals);
    return retVal ? retVal : this._currencyPipe.transform(0, this.ciCurrencyCode, 'symbol', currencyDecimals);
  }

  private buildControlValue() {
    return this.convertCurrencyStringToCurrencyValue(this._currencyString);
  }

  private convertCurrencyValueToCurrencyString(currencyValue: number): string {
    return currencyValue.toString().replace(/[^0-9]/g, '');
  }

  private convertCurrencyStringToCurrencyValue(currencyString: String): number {
    const strippedValue: string = currencyString.replace(/[^0-9]/g, '');

    if (strippedValue.length === 0) {
      return 0;
    }
    const parsedInt: number = parseInt(strippedValue, 10);
    return parsedInt / Math.pow(10, this.ciDecimals);
  }
}

  

Вот мой setValue()

 myForm.get('myField').setValue(5.00);
  

Комментарии:

1. onChangeCallback будет вызываться каждый раз, когда изменяется значение. Если вы хотите использовать что-то другое, вы можете использовать инжектор для получения NgControl зависимости, которая содержит control абстрактный элемент управления, имеющий valueChanges свойство.

2. На самом деле оказывается, что writeValue будет вызываться с новым значением. Я не знаю, почему это никогда не приходило мне в голову.

Ответ №1:

Оказывается, что writeValue вызывался при вызове FormControl.setValue().setValue(). Вот обновленная директива на случай, если она кому-нибудь поможет.

 import {
  Directive,
  ElementRef,
  EventEmitter,
  forwardRef,
  HostListener,
  Input,
  Output,
  Renderer2
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {CurrencyPipe} from '@angular/common';
import {isNullOrUndefined} from '../../util';

@Directive({
  selector: '[currencyInput]',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CurrencyInputDirective),
      multi: true
    }
  ]
})
export class CurrencyInputDirective implements ControlValueAccessor {

  private _currencyString: string = '';

  @Input() ciDecimals = 2;
  @Input() ciCurrencyCode = 'USD';
  @Input() ciMaximumAmount = 10000;
  @Input() ciAllowNull = false;

  @Output() ciExceededMax: EventEmitter<any> = new EventEmitter();

  onChangeCallback = (value: any) => {
  };
  onTouchedCallback = () => {
  };

  @HostListener('blur', []) onBlur() {
    this.onTouchedCallback();
  }

  @HostListener('focus', []) onFocus() {
    this.onTouchedCallback();
  }

  @HostListener('keydown', ['$event']) onKeyDown(e: KeyboardEvent) {
    switch (e.key) {
      case 'Tab':
        // Allow default
        break;
      case 'Backspace':
        this.handleBackspaceKeyPress(e);
        break;
      default:
        if (isNaN( e.key)) {
          e.preventDefault();
        } else {
          this.handleNumericKeyPress(e);
        }
        break;
    }
  }

  constructor(
    private _renderer: Renderer2,
    private _elementRef: ElementRef,
    private _currencyPipe: CurrencyPipe
  ) {

  }

  writeValue(value: any) {
    const currencyString: string = this.convertCurrencyValueToCurrencyString(value);

    if (currencyString !== this._currencyString) {
      const currencyValue: number = this.convertCurrencyStringToCurrencyValue(currencyString);
      if (currencyValue > this.ciMaximumAmount) {
        setTimeout(() => {
          this.ciExceededMax.emit({amount: currencyValue, maxAmount: this.ciMaximumAmount});
        }, 1);
        return;
      }
      this._currencyString = currencyString;
    }

    this._renderer.setProperty(this._elementRef.nativeElement, 'value', this.buildElementValue());
    this.onChangeCallback(this.buildControlValue());
  }

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

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

  setDisabledState(isDisabled: boolean) {
    this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
  }

  private handleBackspaceKeyPress(e: KeyboardEvent) {
    e.preventDefault();
    this.writeValue((this._currencyString.length === 0) ? this._currencyString : this._currencyString.slice(0, this._currencyString.length - 1));
  }

  private handleNumericKeyPress(e: KeyboardEvent) {
    e.preventDefault();
    this.writeValue(this._currencyString   e.key);
  }

  private buildElementValue() {
    const currencyDecimals = '1.'   this.ciDecimals   '-'   this.ciDecimals;
    const controlValue: number = this.buildControlValue();

    // Currency format return value
    if (!isNullOrUndefined(controlValue)) {
      return this._currencyPipe.transform(
        controlValue,
        this.ciCurrencyCode,
        'symbol',
        currencyDecimals);
    }
    return '';
  }

  private buildControlValue(): number {
    return this.convertCurrencyStringToCurrencyValue(this._currencyString);
  }

  private convertCurrencyValueToCurrencyString(currencyValue: any): string {
    if (isNullOrUndefined(currencyValue)) {
      return '';
    }

    if (typeof currencyValue === 'number') {
      const currencyDecimals = '1.'   this.ciDecimals   '-'   this.ciDecimals;
      currencyValue = this._currencyPipe.transform(currencyValue, this.ciCurrencyCode, 'symbol', currencyDecimals);
    }

    return currencyValue.toString().replace(/[^0-9]/g, '');
  }

  private convertCurrencyStringToCurrencyValue(currencyString: String): number {
    const strippedValue: string = currencyString.replace(/[^0-9]/g, '');

    if (strippedValue.length === 0) {
      return (this.ciAllowNull) ? null : 0;
    }
    const parsedInt: number = parseInt(strippedValue, 10);
    return parsedInt / Math.pow(10, this.ciDecimals);
  }
}