Reputation: 63
I'm working with Angular 5 template driven forms, and have several numeric inputs in a form.
The requirement I have is very simple, and I'm astounded that I can't find a solution to it
Basically each numeric input should display as a decimal with thousand separators and a configurable number of decimal places, but store the value as a number (without commas) on the model.
I've looked at pipes which aren't applicable as they are display only and do not work with two way binding I've also searched high and low for an example of a directive that does this by exposing the NgModel of the control and manipulates its data and no joy there either..
I've looked at the one way binding + a setter in the TypeScript but that is ugly and not reusable
<input type="number" [NgModel]="somevalue | number : '2.2-6'" (ngModelChange)="somevalue = formatNumber($event)"
Can you point me in the right direction? I think the directive is the way to go but I can't find a clear example of how to go about separating the input binding from the displaying, and importantly displaying the right value on load if the data is already populated in the model
Thanks in advance!
Upvotes: 3
Views: 3475
Reputation: 63
Jeto gave me 90% of the solution, and with a couple of tweaks, managed to get the exact behaviour I was after
I followed This article and got to a state where the field was displaying correctly, but it was always showing the truncated value. What I really wanted was a pure "mask" so that the user could give more than 2dps, but would only see 2dps for clarity and have a mouseover to show the entire figure.
I then used This article to expose the NgModel in the directive so that on focus, the raw value of the ngModel.viewmodel was shown instead of the truncated value
My directive now looks like this (minus the pipe for clarity. Its a cut and paste of the 1st article at the moment)
import { Directive, Input, HostListener, ElementRef, OnInit } from "@angular/core";
import { NgModel } from "@angular/forms";
import { DecimalPipe } from "./decimal-pipe";
@Directive({
selector: "[decimalFormatter][ngModel]",
providers: [NgModel]
})
export class DecimalFormatterDirective implements OnInit {
private el: HTMLInputElement;
@Input('decimals') decimals: number;
constructor(
private elementRef: ElementRef,
private decimalPipe: DecimalPipe,
private ngModel: NgModel
) {
this.el = this.elementRef.nativeElement;
}
ngOnInit() {
if (this.decimals == null)
this.decimals = 2;
console.log(this.el);
console.log(this.ngModel);
this.el.value = this.decimalPipe.transform(this.el.value, this.decimals);
}
@HostListener("focus", ["$event.target.value"])
onFocus(value) {
console.log(this.el);
console.log(this.ngModel);
this.el.value = this.ngModel.viewModel; // opossite of transform
}
@HostListener("blur", ["$event.target.value"])
onBlur(value) {
console.log(this.el);
console.log(this.ngModel);
this.el.value = this.decimalPipe.transform(value, this.decimals);
}
}
EDIT: Small update
I changed the pipe it was calling from the one in the first article to the actual number pipe that the Angular uses so its now more of a wrapper for the Angular pipe that works with two way binding. You also get to pass in the angular number formatting style, for example 2.2-6
Hopefully this helps someone coming across the same issue in the future!
The Directive :
import { Directive, Input, HostListener, ElementRef, OnInit } from "@angular/core";
import { NgModel } from "@angular/forms";
import { DecimalFormatPipe } from "./decimal-format-pipe";
@Directive({
selector: "[decimalFormatter][ngModel]",
providers: [NgModel]
})
export class DecimalFormatterDirective implements OnInit {
private el: HTMLInputElement;
@Input('decimals') decimals: string;
constructor(private elementRef: ElementRef,
private ngModel: NgModel,
private decimalFormatPipe: DecimalFormatPipe) {
this.el = this.elementRef.nativeElement;
}
ngOnInit() {
if (this.decimals == null)
this.decimals = "2.0-6";
console.log(this.el.value, this.decimals);
this.el.value = this.decimalFormatPipe.transform(this.el.value, this.decimals);
}
@HostListener("focus", ["$event.target.value"])
onFocus(value) {
console.log(this.el.value, this.decimals);
this.el.value = this.ngModel.viewModel; //Display the raw value on the model
}
@HostListener("blur", ["$event.target.value"])
onBlur(value) {
console.log(this.el.value, this.decimals);
this.el.value = this.decimalFormatPipe.transform(this.el.value, this.decimals);
}
}
The Pipe Wrapper
import { Pipe, PipeTransform, Inject, LOCALE_ID } from '@angular/core'
import { DecimalPipe } from '@angular/common';
@Pipe({ name: 'decimalFormatPipe' })
export class DecimalFormatPipe implements PipeTransform {
constructor(@Inject(LOCALE_ID) private locale: string) {}
transform(value: any, args: string) {
let pipe = new DecimalPipe(this.locale);
return pipe.transform(value, args);
}
}
And usage on an Input
<input #myNumberInputCtrl="ngModel"
name="myNumberInput"
spellcheck="false"
placeholder="Enter a Number.."
[(ngModel)]="myNumberModel"
required
decimalFormatter
[decimals]="'0.0-6'"
/>
Upvotes: 1