Search code examples
angularangular-directive

How to keep format in view but not in model (Currency Directive)


I am trying to format a number to currency while the user typing in the field, which actually changes the control actual value to formatted value(number to a string).

Is there any way to format the number to currency for view purposes only, without changing the control's actual value to string? and keep a "normal" number in the model Because I have another directive that has a min and max value which depends on this control's actual value in number format.

Input Element:

<input
digitOnly
type="text"
currencyFormatter
[decimal]="false"
id="salary-range"
class="form-control"
formControlName="maxSalary"
placeholder="Enter Max Salary"
[min]="formcontrol['minSalary'].value || 1"
[max]="formcontrol['currency'].value === 'INR'? 100000000: 1000000"
/>

Directive:

@Directive({
    selector: '[currencyFormatter]'
})
export class CurrencyFormatterDirective implements AfterViewInit, OnDestroy {
    constructor(public el: ElementRef, @Self() private ngControl: NgControl) {
        this.inputElement = el.nativeElement;
        this.formatter = new Intl.NumberFormat('en-IN', {
            style: 'currency',
            currency: 'INR',
            minimumFractionDigits: 0,
            maximumFractionDigits: 0
        });
    }

    private formatter: Intl.NumberFormat;
    private inputElement: HTMLInputElement;
    private destroy$ = new Subject<boolean>();

    ngAfterViewInit(): void {
        this.ngControl.control?.valueChanges
            .pipe(takeUntil(this.destroy$))
            .subscribe(this.updateValue.bind(this));
    }

    updateValue(value: string) {
        let inputVal = value || '';
        const unformatted = this.unformatPrice(inputVal);
        const formatted = this.formatPrice(unformatted);
        this.setValue(formatted);
    }

    private formatPrice(value: number): string {
        return this.formatter.format(value);
    }

    private unformatPrice(value: string): number {
        return new RegExp(/[$₹., ]/g).test(value)
            ? Number(value.substring(1).replace(/,/g, ''))
            : Number(value);
    }

    private setValue(value: string | number): void {
        this.ngControl.control?.setValue(value, { emitEvent: false });
    }

    ngOnDestroy(): void {
        this.destroy$.next(true);
        this.destroy$.complete();
    }
}

Solution

  • I have achieved this using ControlValueAccessor. I have created a component and added the input where I am showing the formatted currency value and with the help of ControlValueAccessor I have sent the actual value.

    Currency Component HTML:

    <input
        digitOnly
        type="text"
        [id]="inputId"
        [min]="minValue"
        [max]="maxValue"
        [decimal]="false"
        [isCurrency]="true"
        class="form-control"
        [allowNegatives]="false"
        [formControl]="currency"
        [autocompleteOff]="'off'"
        [placeholder]="placeholder"
    />
    

    Currency Component TS:

    @Component({
        selector: 'currency-input',
        templateUrl: './currency-input.component.html',
        styleUrls: ['./currency-input.component.css'],
        providers: [
            {
                provide: NG_VALUE_ACCESSOR,
                useExisting: forwardRef(() => CurrencyInputComponent),
                multi: true
            }
        ]
    })
    export class CurrencyInputComponent
        implements OnInit, OnDestroy, ControlValueAccessor
    {
        constructor() {}
    
        public isDisabled = false;
        @Input('min') minValue!: number;
        @Input('max') maxValue!: number;
        public currencyValue: string = '';
        @Input('input-id') inputId!: string;
        private onTouched: Function = () => {};
        @Input('placeholder') placeholder!: string;
        public onValidationChange: Function = () => {};
        private propagateChange: Function = (_: number) => {};
        private unsubscribe$: Subject<void> = new Subject<void>();
        @Input('currency-format') currencyFormat: 'INR' | 'USD' = 'INR';
        public currency = new FormControl('', {
            nonNullable: true
        });
    
        ngOnInit(): void {
            this.currency.valueChanges
                .pipe(
                    takeUntil(this.unsubscribe$),
                    map(this.currencyToNumber),
                    tap((value) => {
                        this.setValue(value);
                        this.propagateChange(value);
                    })
                )
                .subscribe();
        }
    
        private numberToCurrency(value: number): string {
            return new Intl.NumberFormat(
                `en-${this.currencyFormat === 'INR' ? 'IN' : 'US'}`,
                {
                    style: 'currency',
                    currency: this.currencyFormat,
                    minimumFractionDigits: 0,
                    maximumFractionDigits: 0
                }
            ).format(value);
        }
    
        private currencyToNumber(value: string): number {
            return new RegExp(/[$₹., ]/g).test(value)
                ? Number(value.substring(1).replace(/,/g, ''))
                : Number(value);
        }
    
        private setValue(value: number | null): void {
            const formattedValue = !!value ? this.numberToCurrency(value) : '';
            this.currency.patchValue(formattedValue, {
                emitEvent: false
            });
        }
    
        public writeValue(value: number | null): void {
            this.setValue(value);
        }
    
        public registerOnChange(fn: Function): void {
            this.propagateChange = fn;
        }
    
        registerOnValidatorChange?(fn: () => void): void {
            this.onValidationChange = fn;
        }
    
        public registerOnTouched(fn: Function): void {
            this.onTouched = fn;
        }
    
        public setDisabledState?(isDisabled: boolean): void {
            isDisabled ? this.currency.disable() : this.currency.enable();
        }
    
        ngOnDestroy(): void {
            this.unsubscribe$.next();
            this.unsubscribe$.complete();
        }
    }