Angular 5, input formatting and parsing, what is a standard solution

Viewed 18749

I have an input field and want to enter a float number using a German locale. My solution is following, it is working, but it is complicated and there should be a better approach?

<input matInput [ngModel]="amount | number: '0.2-2'" 
(blur)="transformAmount($event)" (keyup.enter)="transformAmount($event)"/>

transformAmount(event) {
  console.log(event.target.value);
  this.amount = parseFloat(event.target.value.replace('.', '').replace(',','.'));
  console.log('amount=' + this.amount);
}
4 Answers

I had a similar problem with the formatting reactive form input field. To solve it I have used this article. I have created a slightly modified version to fit my needs.

import { Directive, ElementRef, forwardRef, HostListener, Input, OnDestroy } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material';
import { Subscription } from 'rxjs';
import { formatNumber } from '@angular/common';

@Directive({
  selector: 'input[localizedNumericInput]',
  providers: [
    { provide: MAT_INPUT_VALUE_ACCESSOR, useExisting: LocalizedNumericInputDirective },
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => LocalizedNumericInputDirective),
      multi: true
    }
  ]
})
export class LocalizedNumericInputDirective implements ControlValueAccessor, OnDestroy {
  locale = 'en';
  decimalMarker: string;

  constructor(private element: ElementRef<HTMLInputElement>) {
  }

  private _value: string | null;

  get value(): string | null {
    return this._value;
  }

  @Input('value')
  set value(value: string | null) {
    this._value = value;
    this.formatValue(value);
  }

  @HostListener('input', ['$event.target.value'])
  input(value) {
    //Find all numerics, decimal marker(, or .) and -
    //It will delete thousandSeparator cos it's always opposite to decimal marker
    const regExp = new RegExp(`[^\\d${this.decimalMarker}-]`, 'g');
    //Separate value on before and after decimal marker
    const [integer, decimal] = value.replace(regExp, '').split(this.decimalMarker);

    //Send non localized value, with dot as decimalMarker to API
    this._value = decimal ? integer.concat('.', decimal) : integer;

    // If decimal separator is last character don't update
    // because it will delete . || ,
    if (this.isLastCharacterDecimalSeparator(value)) {
      this._value = value;
    }

    // here to notify Angular Validators
    this._onChange(this._value);
  }

  @HostListener('blur')
  _onBlur() {
    /**
     * Adding thousand separators
     */
    this.formatValue(this._value);
  }

  @HostListener('focus')
  onFocus() {
    this.unFormatValue();
  }

  _onChange(value: any): void {}

  /**
   * @param value
   * apply formatting on value assignment
   */
  writeValue(value: any) {
    this._value = value;
    this.formatValue(this._value);
  }

  registerOnChange(fn: (value: any) => void) {
    this._onChange = fn;
  }

  registerOnTouched() {}

  isLastCharacterDecimalSeparator(value: any) {
    return isNaN(value[value.length - 1]);
  }


  private formatValue(value: string | null) {
    if (value === null) {
      this.element.nativeElement.value = '';
      return;
    }

    if (this.isLastCharacterDecimalSeparator(value)) {
      this.element.nativeElement.value = value;
      return;
    }

    // Conclude the decimal and thousand separators from locale
    const [thousandSeparator, decimalMarker] = formatNumber(1000.99, this.locale).replace(/\d/g, '');
    this.decimalMarker = decimalMarker;

    //Here value should always come with . as decimal marker thus any other behavior is bug
    const [integer, decimal] = value.split('.');

    //Group every three elements, and add thousandSeparator after them
    this.element.nativeElement.value = integer.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator);

    //Add decimals and decimalMarker if any
    if (decimal) {
      this.element.nativeElement.value = this.element.nativeElement.value.concat(decimalMarker, decimal);
    }
  }

  private unFormatValue() {
    const value = this.element.nativeElement.value;
    if (this.isLastCharacterDecimalSeparator(value)) {
      return;
    }
    const regExp = new RegExp(`[^\\d${this.decimalMarker}-]`, 'g');
    const [integer, decimal] = value.replace(regExp, '').split(this.decimalMarker);

    this._value = integer.concat('.', decimal);
    if (value) {
      this.element.nativeElement.value = this._value;
    } else {
      this.element.nativeElement.value = '';
    }
  }
}

To use this directive your html should look like this:

<mat-form-field>
<mat-label>Value</mat-label>
<input
        type="text"
        localizedNumericInput
        matInput
        autocomplete="off"
        formControlName="value"
      />
</mat-form-field>

Your approach is fine.

There is no better way to transform data from variable to input than Pipe and there is no better way to transform data from input to variable than a function bound to DOM events.

In order to simplify your code a bit you could do the following:

<input matInput [ngModel]="amount | number: '0.2-2'" (ngModelChange)="transformAmount($event)"/>

transformAmount(event) {
  console.log(event);
  this.amount = parseFloat(event.replace('.', '').replace(',','.'));
  console.log('amount=' + this.amount);
}

Yes this part is a little bit tricky. Futhermore sometime you need to display something that is not really the actual value.

The actual best solution I have found it to create a custom component (working with ngModel, see about ControlValueAccessor) for my input and this one look like this base on what you are trying to do.

html :

<input [formControl]="inputFormControl" #myInput />

ts :

@ViewChild('myInput') inputElm: ElementRef;
formGroup: FormGroup;
value: number;

this.inputFormControl.valueChanges.subscribe((value: string) => {
  if (value) {
    const displayValue = ('' + value).replace(',', '.');
    this.value = parseFloat(displayValue); // manage the value
    this.inputFormControl.setValue(this.value, { emitEvent: false });
    this.inputElm.nativeElement.value = new DecimalPipe(this.localId).transform(displayValue, '0.2-2'); //control the display
  }
});

In some way you will certainly find it a bit more complicated but at the end it give you also more control ;)

I created a Directive using an information from this Angular 2 Directives to Format Text as you Type article.

It is working but i don't unterstand why 'Change' suffix is necessary and why i can't separate an event from an input doing line this: (appNumberInput)=.... [appNumberInput]=....

<input [(appNumberInput)]="amount" format="0.2-2" />




@Directive({
    selector: '[appNumberInput]',

})
export class NumberInputDirective implements OnInit {

    @Input()
    appNumberInput: any;

    @Input()
    format: string;

    @HostBinding('value')
    stringValue: string;


    @Output() appNumberInputChange: EventEmitter = new EventEmitter();

    constructor(private decimalPipe: DecimalPipe) {
    }

    ngOnInit() {
      this.stringValue = this.decimalPipe.transform(this.appNumberInput, this.format);
    }

    @HostListener('blue', ['$event.target.value'])
    @HostListener('keyup.enter', ['$event.target.value'])
    formatANumber(value) {
        const numberValue = parseFloat(value.replace('.', '').replace(',', '.'));
        this.stringValue = this.decimalPipe.transform(numberValue, this.format);
        this.appNumberInputChange.next(numberValue);

  }
}

Related