How to use pipes in Angular 5 reactive form input
Asked Answered
P

9

74

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'
Portauprince answered 27/3, 2018 at 21:17 Comment(3)
is the expression error coming from the pipe or the component?Footing
The component html. specifically the line that has this "[ngModel]="f1.value.amount | udpCurrency"Portauprince
#57994232Chasten
A
117

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>
Analisaanalise answered 30/3, 2018 at 15:52 Comment(9)
this works initially but when you edit the value, the pipe doesn't reapply onblur. Is this something that has to be handled explicitly?Sisley
@JacobRoberts Sorry, i didn't see your comment there for a few months. I finally realized what you were saying and you are correct. I have a new answer below that might help if you are still having the same issue.Portauprince
I'm wondering why this won't work: <input... #amount [value]="amount.value | udpCurrency"></input>. It throws an exceptionMagpie
This doesn't work as expected because the form value change fires before the pipe transform. The displayed value will look right but the form value will be different.Antique
This doesn't work, because after the form updates the model on the view, the currency pipe again updates the model and ends up trying to pipe a string value which is where I was getting the error. Check out my solution below to avoid this.Kuenlun
work! <input matInput [value]="formGroup.get('valor').value | currency:'BRL':true" formControlName="valor">Noted
This should not be approved answer with Angular 11 or higher, as it doesn't work anymore for values 0.999999999.... The answer I got in Angular github was that it should not be used like this. github.com/angular/angular/issues/44522Screak
Don't do that. Even if that works. It interferes with reactive forms or ngModel (value accessor). Either bind value or ngModel / form control. But not both at once. Please don't blindly copy everything from Stackoverflow. Your colleagues will thank you. :)Lange
But isn't using value a template driven approach?Weimaraner
P
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.

Portauprince answered 20/6, 2018 at 20:28 Comment(0)
K
7

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
  }
}
Kuenlun answered 12/3, 2019 at 4:34 Comment(2)
Are you sure this approach is correct? As Angular v6 combine reactive and template driven strategies are deprecated. github.com/angular/angular/blob/…Arndt
My answer was targeted to Angular v5 when I wrote it 3 1/2 years ago. It definitely applied then, but this isn't the best solution now as you've pointed out.Kuenlun
A
1

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, '');
Antique answered 4/3, 2019 at 19:1 Comment(1)
It doesn't work. If you change the value by clicking Backspace key. For example, the original value was 3, you want to change it, click backspace, it becomes 30. Click backspace again it becomes 300 and so on.Chasten
F
0

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

}
Footing answered 27/3, 2018 at 21:42 Comment(1)
My form building was happening in the constructor, however it depended on other data retrieved from an http request so even though it was not in the ngOnInit hook i believe it was called after ngOnInit. as a test i moved the building to within ngOnInit and I still ge the errorPortauprince
L
0

You could wrap the pipe into a directive. For example:

import { Directive, Input, OnInit } from '@angular/core';
import { NgControl } from '@angular/forms';

// The selector can restrict usage to a specific input.
// For example: 'input[type="number"][yourPipe]' for number type input.
@Directive({ selector: 'input[yourPipeSelector]' })
export class FormControlYourPipeDirective implements OnInit {
  /** Your optional pipe args. Same name as directive to directly pass the value. */
  @Input() public yourPipeSelector: unknown;
  
  /** Control event options. For example avoid unintentional change events. */
  private eventOptions = {
    onlySelf: true,
    emitEvent: false,
    emitViewToModelChange: false,
  };

  constructor(private ngControl: NgControl, private yourPipe: YourPipe) {}

  public ngOnInit(): void {
    this.ngControl.control.valueChanges.subscribe((value) => {
      this.ngControl.control.setValue(
        this.yourPipe.transform(value, this.yourPipeSelector),
        this.eventOptions
      );
    });
  }
}

Don't forget to add YourPipe to the providers in your pipe module. And add the pipe module to the imports of your module where you want to use it. (Angular basics) ... And then use it:

<input formControlName="myValue" [yourPipeSelector]="'some args'" />

Voila

But note: You manipulate an input element that can be edited again by the user. The value should be compatible to the input (and its type).

For example: If you have an input[type="number"] and use the DecimalPipe, you should set an value typeof number to the input control instead of the typeof string which the number pipe returns.

Also note that this only works if you don't prevent event emission (emitEvent). Otherwise you should simply transform your value on the place where you set the value.

Also take a look to updateOn option of FormControl. For example set to blur to avoid annoying changes during user input.

Lange answered 9/3, 2022 at 17:42 Comment(0)
M
0

Save yourself the headache and export a function that will do what you need. In my case:

export const formatDateTimePG = (datetime: any) => {
  const dt = new Date(datetime);
  const dtDateTime = new Date(dt.valueOf() + dt.getTimezoneOffset() * 60 * 1000);
  return format(dtDateTime, 'yyyy-MM-dd HH:mm:ss');
};

Then in your component:

// TRANSFORM DATE IN FORM CONTROL FOR DISPLAY PURPOSE.
this.medicationForm.valueChanges.subscribe(val => {
 if (typeof val.medstartdate === 'string') {
   const maskedVal = formatDatePG(val.medstartdate);
    if (val.medstartdate !== maskedVal) {
        this.medicationForm.patchValue({ medstartdate: maskedVal });
    }
   }
 });
Meader answered 16/2, 2023 at 23:6 Comment(0)
C
-1
<input type="text" name="name" class="form-control mw-120 text-right"
 [value]="finalRow.controls[i].get('rental').value |currency">

This is most efficient as you are not overriding the value with formControlName.

Adding formControlName will not work correctly always, so need to take care.

<input type="text" name="name" class="form-control mw-120 text-right"
 [value]="finalRow.controls[i].get('rental').value |currency" formControlName="rental">

This may not give the resuts expected.

Cleveite answered 4/12, 2020 at 10:12 Comment(0)
P
-2

Use only this code

<input [value]="value | udpCurrency"  name="inputField" type="number" formControlName="amount" />
Paxon answered 14/3, 2021 at 11:27 Comment(1)
Please elaborate your answer, instead of just pasting in code, give the question asker a chance to understand what you are doing.Brander

© 2022 - 2024 — McMap. All rights reserved.