Как использовать трубы в входе в реакционную форму с угловым 5

Я пытаюсь понять, как использовать трубу в реактивной форме, чтобы вход был принудительно введен в формат валюты. Я уже создал свою собственную трубу для этого, которую я тестировал в других областях кода, поэтому я знаю, что это работает как простой канал. Имя моей трубки - 'udpCurrency'

Самый близкий ответ, который я мог найти при переполнении стека, был следующим: использование Pipes внутри ngModel на элементах INPUT в Angular2-View. Однако это не работает в моем случае, и я подозреваю, что это имеет какое-то отношение к тому, что моя форма является реактивной

Вот весь соответствующий код:

Шаблон

<form [formGroup]="myForm" #f="ngForm">
  <input class="form-control col-md-6" 
    formControlName="amount" 
    [ngModel]="f.value.amount | udpCurrency" 
    (ngModelChange)="f.value.amount=$event" 
    placeholder="Amount">
</form>

Компонент

import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(
    private builder: FormBuilder
  ) {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });
  }    
}

Ошибка:

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined: '. Current value: 'undefined: undefined'

Ответ 1

Это то, что может произойти, когда вы смешиваете шаблонную форму и реактивную форму. У вас есть две привязки, сражающиеся друг с другом. Выберите шаблонную или реактивную форму. Если вы хотите пойти по реактивному маршруту, вы можете использовать [value] для своей трубы...

Обратите внимание, что этот канал предназначен только для отображения желаемого результата в представлении.

<form [formGroup]="myForm">
  <input 
    [value]="myForm.get('amount').value | udpCurrency"
    formControlName="amount" 
    placeholder="Amount">
</form>

Ответ 2

Я думал, что у меня это работает, но, как оказалось, я ошибся (и принял неправильный ответ). Я просто переделал свою логику по-новому, что работает лучше для меня и отвечает на озабоченность Джейкоба Робертса в комментариях выше. Вот мое новое решение:

Шаблон:

<form [formGroup]="myForm">
  <input formControlName="amount" placeholder="Amount">
</form>

Компонент:

import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UdpCurrencyMaskPipe } from '../../../_helpers/udp-currency-mask.pipe';

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(
    private builder: FormBuilder,
    private currencyMask: UdpCurrencyMaskPipe,
  ) {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });

    this.myForm.valueChanges.subscribe(val => {
      if (typeof val.amount === 'string') {
        const maskedVal = this.currencyMask.transform(val.amount);
        if (val.amount !== maskedVal) {
          this.myForm.patchValue({amount: maskedVal});
        }
      }
    });
  }    
}

Труба:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'udpCurrencyMask'
})
export class UdpCurrencyMaskPipe implements PipeTransform {
    amount: any;

    transform(value: any, args?: any): any {

        let amount = String(value);

        const beforePoint = amount.split('.')[0];
        let integers = '';
        if (typeof beforePoint !== 'undefined') {
            integers = beforePoint.replace(/\D+/g, '');
        }
        const afterPoint = amount.split('.')[1];
        let decimals = '';
        if (typeof afterPoint !== 'undefined') {
            decimals = afterPoint.replace(/\D+/g, '');
        }
        if (decimals.length > 2) {
            decimals = decimals.slice(0, 2);
        }
        amount = integers;
        if (typeof afterPoint === 'string') {
            amount += '.';
        }
        if (decimals.length > 0) {
            amount += decimals;
        }

        return amount;
    }
}

Теперь я узнал несколько вещей. Один из них был тем, что сказал Джейкоб, правда, другой способ работал только изначально, но не обновлялся, когда значение менялось. Еще одна важная вещь, которую нужно отметить, это то, что мне нужен совершенно другой тип трубы для маски по сравнению с трубкой представления. Например, труба в представлении может принять это значение "100" и преобразовать ее в "100,00 долларов США", однако вы не хотите, чтобы это преобразование происходило по мере ввода значения, вы хотели бы, чтобы это произошло после того, как было сделано типирование. По этой причине я создал свою трубку масляной маски, которая просто удаляет не числовые числа и ограничивает десятичное число до двух мест.

Ответ 3

Я собирался написать собственный элемент управления, но обнаружил, что переопределение "onChange" из класса ngModelChange через ngModelChange было проще. emitViewToModelChange: false имеет решающее значение во время вашей логики обновления, чтобы избежать повторения цикла событий изменений. Весь конвейер происходит в компоненте, и вам не нужно беспокоиться о получении ошибок консоли.

<input matInput placeholder="Amount" 
  (ngModelChange)="onChange($event)" formControlName="amount" />
@Component({
  providers: [CurrencyPipe]
})
export class MyComponent {
  form = new FormGroup({
    amount: new FormControl('', { validators: Validators.required, updateOn: 'blur' })
  });

  constructor(private currPipe:CurrencyPipe) {}

  onChange(value:string) {
    const ctrl = this.form.get('amount') as FormControl;

    if(isNaN(<any>value.charAt(0))) {
      const val = coerceNumberProperty(value.slice(1, value.length));
      ctrl.setValue(this.currPipe.transform(val), { emitEvent: false, emitViewToModelChange: false });
    } else {
      ctrl.setValue(this.currPipe.transform(value), { emitEvent: false, emitViewToModelChange: false });
    }
  }

  onSubmit() {
    const rawValue = this.form.get('amount').value;

    // if you need to strip the '$' from your input value when you save data
    const value = rawValue.slice(1, rawValue.length);

    // do whatever you need to with your value
  }
}

Ответ 4

Не зная код вашего канала, он, вероятно, бросает ошибки из-за того, где вы строите эту форму.

Попробуйте использовать угловые переключатели обнаружения изменений, чтобы установить это значение после того, как входы были решены:

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(private builder: FormBuilder) { }    

  ngOnInit() {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });
  }

}

Ответ 5

Другие ответы здесь не сработали, но я нашел способ, который работает очень хорошо. Вам необходимо применить конвейерное преобразование внутри подписки valueChanges реактивной формы, но не отправлять событие, чтобы оно не создавало рекурсивный цикл:

this.formGroup.valueChanges.subscribe(form => {
  if (form.amount) {
    this.formGroup.patchValue({
      amount: this.currencyMask.transform(form.amount)
    }, {
      emitEvent: false
    });
  }
});

Это также требует, чтобы ваш канал "форматировал" все, что там было, что обычно так же просто, как в вашей функции преобразования канала:

value = value.replace(/\$+/g, '');