Angular 8 custom currency mask

15,207

Solution 1

Stackblitz https://stackblitz.com/edit/angular-8-currency-directive-insert-jdwx4b

currency custom input

import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-currency',
  template: '<input [(ngModel)]="value" (keyup)="setValue(value)">',
  styleUrls: ['./currency.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CurrencyComponent),
      multi: true
    }
  ]
})
export class CurrencyComponent implements ControlValueAccessor {

  value;

  constructor() {
  }

  setValue(event) {
    let newVal = event.toString().replace(/\D/g, '');
    if (newVal.length === 0) {
      newVal = '';
    } else if (newVal.length <= 3) {
      newVal = newVal.replace(/^(\d{0,3})/, '$1');
    } else {
      newVal = newVal.substring(0, 4);
      newVal = newVal.replace(/^(\d{0,1})(\d{1,3})/, '$1,$2');
    }
    newVal = '$' + newVal;
    if (newVal) {
      this.value = newVal;
      setTimeout(() => {
        // sometimes it needs a tick, specially first time
        this.propagateChange(this.value);
      });
    }
  }

  writeValue(value: any) {
    if (value !== undefined) {
      this.setValue(value);
    }
  }

  registerOnChange(fn) {
    this.propagateChange = fn;
  }

  registerOnTouched() {
  }

  propagateChange = (_: any) => {
  }

}

usage

<app-currency formControlName="currency"></app-currency>

Solution 2

Your question inspired me to create a CurrencyDirective that I would use. It does not approach this the way you have but I believe it could be used instead or hopefully to help others.

StackBlitz - Currency Format Directive

Reasons:

  • We should not be putting currency symbols in our value $1,234
  • We should format as the user types (painless UX)
  • We should be saving our currency values as raw numbers (strip formatting)
  • We should not be regex'ing for 3 chars, 4 chars, 5 chars etc to conditionally add formatting (commas or dots)

Here's what I did instead. I handle paste, input and drop events but the formatting is done within getCurrencyFormat():

getCurrencyFormat(val) {
    // 1. test for non-number characters and replace/remove them
    const filtered = parseInt(String(val).replace(this.currencyChars, ''));

    // 2. format the number (add commas)
    const usd = this.decimalPipe.transform(filtered, '1.0');

    // 3. replace the input value with formatted numbers
    this.renderer.setProperty(this.el.nativeElement, 'value', usd);
}

I believe that saving currency should be done in raw numbers. So on form submit I do this:

Number(this.form.get('currency').value.replace(/[^0-9]g/, ''));
Share:
15,207
imnickvaughn
Author by

imnickvaughn

I am a problem solver at heart. As a secondary occupation, I own the front end of the web development scene in Rochester, NY and Boston, MA. I know what goes into architecting enterprise-level applications and live on the bleeding edge. I am comfortable in Angular but capable of touching all web ecosystems.

Updated on July 28, 2022

Comments

  • imnickvaughn
    imnickvaughn almost 2 years

    Right now I am making a currency mask directive that should be compatible with angular reactive forms. Here's my Stackblitz https://stackblitz.com/edit/angular-8-currency-directive-insert.

    In the input element, I expect that when I enter 1, then 2, then 3, then 4, then 5 that I would see in the console {currency: "$1,234"} because the mask runs .substring(0,4) however I see {currency: "$1,2345"}. I see the correct display value of $1,234 within the input element.

    If I change .substring(0,4) to .substring(0,3) then the display value within the input element displays $1,23 when I expect it to display $1,234. The console outputs the correct value of {currency: "$1,234"}

    Any suggestions that get to the root of the problem are very welcome! I have already done work arounds which involve things like splitting into an array, checking, popping off the end, and joining but those fixes are not ideal. Any suggestions are still welcome though!

    Thank you for your support.

    The code to focus on is found in currency.directive.ts provided below:

     onInputChange(event, backspace) {
        let newVal = event.replace(/\D/g, '');
        if (newVal.length === 0) {
          newVal = '';
        } else if (newVal.length <= 3) {
          newVal = newVal.replace(/^(\d{0,3})/, '$1');
        // } else if (newVal.length <= 4) {
        //   newVal = newVal.replace(/^(\d{0,1})(\d{0,3})/, '$1,$2');
        } else {
          newVal = newVal.substring(0, 4);
          newVal = newVal.replace(/^(\d{0,1})(\d{1,3})/, '$1,$2');
        }
        this.ngControl.valueAccessor.writeValue("$"+ newVal);
        // console.log(this.toNumber(newVal))
      }
    
    • Ben Racicot
      Ben Racicot almost 5 years
      Before I write an answer I need to ask, have you evaluated Angular's has a built in currencyPipe.transform(numberValue, 'USD', 'symbol', '1.0')?
    • Robert
      Robert almost 5 years
      i think you are doing it the wrong way, check blog.thoughtram.io/angular/2016/07/27/…
    • Robert
      Robert almost 5 years
      please share if you have docs regrading the method you are using
    • imnickvaughn
      imnickvaughn almost 5 years
      Hi @BenRacicot, thanks. Yes we investigated that but discovered it did not meet our requirements so decide on this approach which is just has the one last requirement specified above. I left some comments on stackblitz too.
    • imnickvaughn
      imnickvaughn almost 5 years
      Hi @Robert, thanks for your reply. I don't have documentation for this method that is currently in development. How am I doing it the wrong way can you please provide an example of the right way?
    • Ben Racicot
      Ben Racicot almost 5 years
      I'm working on this, and yes I do see that CurrencyPipe leaves a lot to be desired.
  • imnickvaughn
    imnickvaughn almost 5 years
    Thank you Robert! Unfortunately ngModel is deprecated :(
  • Robert
    Robert almost 5 years
    What do you mean?
  • Robert
    Robert almost 5 years
    @immickvaughn I googled it and found that ngmodel is depercated only when you use it with reactive forms, also it's not that difficult to rewrite my solution to reactive form
  • imnickvaughn
    imnickvaughn almost 5 years
    Ok excellent I can see it now. If you want just replace the previous solution with that solution in stackblitz and I will mark this as answered. Thank you sir
  • imnickvaughn
    imnickvaughn almost 5 years
    Thank you! Do you have any idea why the length is never 0? Reactive form validations (required) don't work... it's weird it always will hold the last value. So I tried deleting that last value too but after a bunch of trying, I was unable. I tried to get the backspace char code in the host listener event and when the length was 1 & backspace was pressed then set input value as an empty string. This did not work for me.stackblitz.com/edit/…
  • Robert
    Robert almost 5 years
    because in your logic you always add $ to the value so it's never empty, check Stackblitz again i have edited the code to show that it actually is working fine if the logic changed
  • imnickvaughn
    imnickvaughn almost 5 years
    thanks, did your changes enable the validation.required that I put there? When I delete everything in the input field the validation warning is not displayed. If you look at the console the $ is not factored into that length... is it that simple? Am I missing something?
  • Robert
    Robert almost 5 years
  • Robert
    Robert almost 5 years
    stackblitz.com/edit/currency-format-directive-achp8p?file=sr‌​c/… your code isn't working as intended, the value inside the input and the value outside it isn't the same, for example write 12345678, inside the input it will be 1,234, but outside it will be 1,234,5678
  • imnickvaughn
    imnickvaughn almost 5 years
    this one, forked from yours. just added validations on inputs stackblitz.com/edit/…
  • Robert
    Robert almost 5 years
    sorry didn't notice it
  • Robert
    Robert almost 5 years
    if you know why please let me know
  • Robert
    Robert almost 5 years
    maybe it's cached open it from incognito
  • Robert
    Robert almost 5 years
    BTW the link you commented refer to your Stackblitz, not mine
  • Ben Racicot
    Ben Racicot almost 5 years
    @Robert I fixed it. It was a simple regex issue lacking the g flag. I'd love your opinions on this.
  • imnickvaughn
    imnickvaughn almost 5 years
    @BenRacicot btw I think what you made is impressive. Angular should adopt this