Angular 8 custom currency mask
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/, ''));
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, 2022Comments
-
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 almost 5 yearsBefore 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 almost 5 yearsi think you are doing it the wrong way, check blog.thoughtram.io/angular/2016/07/27/…
-
Robert almost 5 yearsplease share if you have docs regrading the method you are using
-
imnickvaughn almost 5 yearsHi @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 almost 5 yearsHi @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 almost 5 yearsI'm working on this, and yes I do see that
CurrencyPipe
leaves a lot to be desired.
-
-
imnickvaughn almost 5 yearsThank you Robert! Unfortunately ngModel is deprecated :(
-
Robert almost 5 yearsWhat do you mean?
-
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 almost 5 yearsOk 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 almost 5 yearsThank 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 almost 5 yearsbecause 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 almost 5 yearsthanks, 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 almost 5 yearsare you talking about stackblitz.com/edit/angular-8-currency-directive-insert-jdwx4b?
-
Robert almost 5 yearsstackblitz.com/edit/currency-format-directive-achp8p?file=src/… 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 almost 5 yearsthis one, forked from yours. just added validations on inputs stackblitz.com/edit/…
-
Robert almost 5 yearssorry didn't notice it
-
Robert almost 5 yearsif you know why please let me know
-
Robert almost 5 yearsmaybe it's cached open it from incognito
-
Robert almost 5 yearsBTW the link you commented refer to your Stackblitz, not mine
-
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 almost 5 years@BenRacicot btw I think what you made is impressive. Angular should adopt this