How to use pipes in Angular 5 reactive form input

2020-02-08 01:03发布

I am trying to figure out how to use a pipe within a reactive form so that the input is forced into a currency format. I have already created my own pipe for this which I have tested in other areas of the code so I know it works as a simple pipe. My pipe name is 'udpCurrency'

The closest answer I could find on stack overflow was this one: Using Pipes within ngModel on INPUT Elements in Angular2-View However this is not working in my case and I suspect it has something to do with the fact that my form is reactive

Here is all the relevant code:

The Template

<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>

The component

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]
    });
  }    
}

The error:

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

5条回答
淡お忘
2楼-- · 2020-02-08 01:16

This is what can happen when you mix template driven form and reactive form. You have two bindings fighting each other. Choose either template driven or reactive form. If you want to go the reactive route, you can use [value] for your pipe...

Note, this pipe is rather only for showing the desired output in the view.

<form [formGroup]="myForm">
  <input 
    [value]="myForm.get('amount').value | udpCurrency"
    formControlName="amount" 
    placeholder="Amount">
</form>
查看更多
走好不送
3楼-- · 2020-02-08 01:24

I thought I had this working but as it turns out, I was wrong (and accepted a wrong answer). I just redid my logic in a new way that works better for me and answers the concern of Jacob Roberts in the comments above. Here is my new solution:

The Template:

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

The Component:

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});
        }
      }
    });
  }    
}

The Pipe:

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;
    }
}

Now there are several things i learned here. One was what that what Jacob said was true, the other way only worked initially but would not update when the value had changed. Another very important thing to note was that I need a completely different type of pipe for a mask as compared to a view pipe. For example, a pipe in a view might take this value "100" and convert it to "$100.00" however you would not want that conversion to happen as you are typing the value, you would only want that to happen after were done typing. For this reason i created my currency mask pipe which simply removes non numeric numbers and restricts the decimal to two places.

查看更多
叼着烟拽天下
4楼-- · 2020-02-08 01:32

I was going to write a custom control, but found that overriding the "onChange" from the FormControl class via ngModelChange was easier. The emitViewToModelChange: false is critical during your update logic to avoid recurring loop of change events. All piping to currency happens in the component and you don't have to worry about getting console errors.

<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's value when you save data
    const value = rawValue.slice(1, rawValue.length);

    // do whatever you need to with your value
  }
}
查看更多
成全新的幸福
5楼-- · 2020-02-08 01:35

Without knowing your pipe code, it's likely throwing errors because of where you're constructing that form.

Try using Angular's change detection hooks to set that value after inputs have been resolved:

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(private builder: FormBuilder) { }    

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

}
查看更多
孤傲高冷的网名
6楼-- · 2020-02-08 01:41

The other answers here didn't work properly for me but I found a way that works very well. You need to apply the pipe transform inside the reactive form valueChanges subscription, but don't emit the event so it doesn't create a recursive loop:

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

This also requires that your pipe "unformats" whatever was there, which is usually as simply as something like this inside your pipe's transform function:

value = value.replace(/\$+/g, '');
查看更多
登录 后发表回答