Skip to content

Instantly share code, notes, and snippets.

@tw3
Created August 30, 2018 14:52
Show Gist options
  • Save tw3/8722086e89cd2755ec7d0064be967dcb to your computer and use it in GitHub Desktop.
Save tw3/8722086e89cd2755ec7d0064be967dcb to your computer and use it in GitHub Desktop.
Currency input directive for Angular
/* tslint:disable:no-forward-ref */
import { OnInit, Directive, HostListener, ElementRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { I18NCurrencyPipe } from '../pipes';
export const CURRENCY_INPUT_DIRECTIVE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CurrencyInputDirective),
multi: true
};
@Directive({
selector: '[appCurrencyInput]',
providers: [
CURRENCY_INPUT_DIRECTIVE_VALUE_ACCESSOR,
I18NCurrencyPipe
]
})
export class CurrencyInputDirective implements ControlValueAccessor, OnInit {
private el: HTMLInputElement;
private onModelChange: Function;
private onModelTouched: Function;
private lastNumVal: number;
private DECIMAL_MARK = '.';
constructor(
private elementRef: ElementRef,
private currencyPipe: I18NCurrencyPipe
) { }
ngOnInit(): void {
this.el = this.elementRef.nativeElement;
}
@HostListener('focus', ['$event'])
handleFocus(event: any): void {
const strVal: string = this.getInputValue();
const unmaskedStr: string = this.getUnmaskedValue(strVal);
this.updateInputValue(unmaskedStr);
}
@HostListener('cut', ['$event'])
handleCut(event: any): void {
setTimeout(() => {
this.inputUpdated();
}, 0);
}
@HostListener('keypress', ['$event'])
handleKeypress(event: any): void {
// Restrict characters
// TODO: I18N issue, $ hard coded
const newChar: string = String.fromCharCode(event.which);
const allowedChars: RegExp = /^[\d.]+$/;
if (!allowedChars.test(newChar)) {
event.preventDefault();
return;
}
// TODO: I18N issue, some currencies have 3 decimal places.
// NOTE: Assume this is just restricting, I18NCurrencyPipe handles formatting decimal places
// Handle decimal mark input
const currentValue: string = event.target.value;
const separatorIdx: number = currentValue.indexOf(this.DECIMAL_MARK);
const hasFractionalPart: boolean = (separatorIdx >= 0);
if (!hasFractionalPart || newChar !== this.DECIMAL_MARK) {
return;
}
const isOutsideSelection: boolean = !this.isIdxBetweenSelection(separatorIdx);
if (isOutsideSelection) {
const positionAfterMark: number = separatorIdx + 1;
this.setCursorPosition(positionAfterMark);
event.preventDefault();
return;
}
}
@HostListener('input', ['$event'])
handleInput(event: any): void {
this.inputUpdated();
}
@HostListener('paste', ['$event'])
handlePaste(event: any): void {
setTimeout(() => {
this.inputUpdated();
}, 1);
}
@HostListener('blur', ['$event'])
handleBlur(event: any): void {
const strVal: string = this.getInputValue();
const numVal: number = this.convertStrToDecimal(strVal);
this.maskInput(numVal);
this.onModelTouched.apply(event);
}
registerOnChange(callbackFunction: Function): void {
this.onModelChange = callbackFunction;
}
registerOnTouched(callbackFunction: Function): void {
this.onModelTouched = callbackFunction;
}
setDisabledState(value: boolean): void {
this.el.disabled = value;
}
writeValue(numValue: number): void {
this.maskInput(numValue);
}
private maskInput(numVal: number): void {
if (!this.isNumeric(numVal)) {
this.updateInputValue('');
return;
}
const strVal: string = this.convertDecimalToStr(numVal);
const newVal: string = this.transformWithPipe(strVal);
this.updateInputValue(newVal);
}
private inputUpdated(): void {
this.restrictDecimalValue();
const strVal: string = this.getInputValue();
const unmaskedVal: string = this.getUnmaskedValue(strVal);
const numVal: number = this.convertStrToDecimal(unmaskedVal);
if (numVal !== this.lastNumVal) {
this.lastNumVal = numVal;
this.onModelChange(numVal);
}
}
private restrictDecimalValue(): void {
const strVal: string = this.getInputValue();
const dotIdx: number = strVal.indexOf(this.DECIMAL_MARK);
const hasFractionalPart: boolean = (dotIdx >= 0);
if (!hasFractionalPart) {
return;
}
const fractionalPart: string = strVal.substring(dotIdx + 1);
if (fractionalPart.length > 2) {
const choppedVal: string = strVal.substring(0, dotIdx + 3);
this.updateInputValue(choppedVal, true);
return;
}
}
private transformWithPipe(str: string): string {
return this.currencyPipe.transform(str);
}
private getUnmaskedValue(value: string): string {
return value.replace(/[^-\d\\.]/g, '');
}
private updateInputValue(value: string, savePosition = false): void {
if (savePosition) {
this.saveCursorPosition();
}
this.el.value = value;
}
private getInputValue(): string {
return this.el.value;
}
private convertStrToDecimal(str: string): number {
return (this.isNumeric(str)) ? parseFloat(str) : null;
}
private convertDecimalToStr(n: number): string {
return (this.isNumeric(n)) ? n.toString() : '';
}
private isNumeric(n: any): boolean {
return !isNaN(parseFloat(n)) && isFinite(n);
}
private saveCursorPosition(): void {
const position: number = this.el.selectionStart;
setTimeout(() => {
this.setCursorPosition(position);
}, 1);
}
private setCursorPosition(position: number): void {
this.el.selectionStart = position;
this.el.selectionEnd = position;
}
private isIdxBetweenSelection(idx: number): boolean {
if (this.el.selectionStart === this.el.selectionEnd) {
return false;
}
return (idx >= this.el.selectionStart && idx < this.el.selectionEnd);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment