Angular - How to make input field formated in percent but with percent removed when editing it?

tom picture tom · Nov 8, 2017 · Viewed 10.3k times · Source

I'm trying to find a way to have a html input field displayed in percent (e.g. 97,52 %) upon initial page load (data fetched via a angular service to a backend, i.e. observable/subscribe), but also have this input field loose it's percent formating when I edit it (i.e. when DOM (focus) event is raised).

The data for format is bound to a model. Let's call the model field myModel.percentNumber, where percentNumber=1 for '100 %' (.69 for '69 %' or again 100 for '10 000 %')

So when I click on the field indicating '97,52 %' I want it to become '97.52', but the data-bound value in the model should be 0.9752 as this is what I need to store.

The field sould be bound using ngModel.

Note : this should work with internationalization, so I can't use a 'diry' solution based on removing spaces, replacing '.' by ',' ... Here you can see I've taken an example where the formated percent value should have a coma as decimal separator and a space as a thousands separator, but this depends on the user's localization.

So far, I've tried a few things, but none solved all issues.

1) using 2 way binding as a starting point, i.e.

<input [(ngmodel)]="myModel.percentNumber | percent:'1.2-2'">

...but it does cause an issue with the pipe:

Uncaught Error: Template parse errors:

2) from 2 way binding short syntax I want to the long syntax, i.e.

<input [ngmodel]="myModel.percentNumber | percent:'1.2-2'" (ngModelChange)="myModel.percentNumber = $event">

This allows to have a proper format in percent upon initial load (on response from the back-end, if myModel.percentNumber=0.9752 it is properly displayed as '97,52 %'), however, when you click on the input to edit it, the formatting stays when I would want it to display '97.52' I don't think the proper 'angular' way to address this would be to add a (focus) event handler where I would do the reverse operation than PercentPipe.transform(), e.g. something like PercentPipe.parse() (which doesn't exists). This would require to add a (focus) event handler for each field thy should have this behavior. Re-usability would be low. I believe a angular Directive approach should be preferable .

3) with an angular Directive : I created a directive PercentFormatterDirective with a selector percentFormatter and an input percentDigits of type string that would accept the same format as PercentPipe (i.e. digitInfo). A usage example would be:

<input percentFormatter percentDigits="1.2-2" [ngModel]="myModel.percentNumber" (ngModelChange)="myModel.percentNumber = $event">

I would rather not have to still also the 'normal' PercentPipe in [ngModel] as it would require to write the digitInfo 1.2-2 twice (i.e. I don't want to have to write <input percentFormatter percentDigits="1.2-2" [ngModel]="myModel.percentNumber | percent:'1.2-2'" (ngModelChange)="myModel.percentNumber = $event">

I'm able to format the value upon clicking on the input (focus) as I've implemented in my PercentFormatterDirective as listener on the (focus) event :

  @HostListener("focus", ["$event.target.value"])
  onfocus(value) {
    if (value) {
      // we have to do the oposite than the transform (e.g. we want to go from % to number)
      this.htmlInputElement.value = this.parse(value);
    }
  }

So this gets met from '97,52 %' to '97.52' when I click in the field. Here I don't get into details of the parse() method I've created (you just need to know that it works and is based in the PercentPipe that I use to get the users localization & this find which is the decimal separator and thousands separator).

I'm also able to format the value back with the percent format upon leaving the input field as I've implemented in my PercentFormatterDirective as listener on the (blur) event :

  @HostListener("blur", ["$event.target.value"])
  onblur(value) {
    if ((value as string).indexOf('%') === -1) {
      this.formatNumberInPercent('100for100percent');
    }
  }

So this gets met from '97.52' to '97,52 %' when I leave the field. Here I don't get into details of the formatNumberInPercent() method (you just need to know that it works and is based in the PercentPipe.transform() method but dividing the value by 100 just before as PercentPipe.transform() will get you from '0.9752' to '97,52 %' but in my input I have '97.52' which is what the user is obviously is going to input)

This issue is that on initial load (more exactly when there is the return from back-end, via observable/subscribe, when myModel.percentNumber is set), neither the (focus) or (blur) @HostListener from the PercentFormatterDirective are called as there is no user interaction with the DOM. If I add an ngOnInit() in the PercentFormatterDirective, then YES I can format the bound values (i.e. transform '0.9752' to '97,52 %' using a custom method I created named formatNumberInPercent('100for100percent') that uses again the PercentPipe.transform() method but without dividing the value by 100)...however this work if I bind the data directly in the Component, e.i. without using observable/subscribe. When using observable/subscribe (which I have to use) to get the answer from the back-end, the ngOnInit() of the PercentFormatterDirective has already been executed.

I would need to be able to execute the formating when the response (from the observable/subscribe) is obtained. I have tried adding ngOnChanges(changes: SimpleChanges) in the PercentFormatterDirective (and then read de array of SimpleChange with for (let propName in changes)), however I don't see the newly bound value from myModel.percentNumber. It seems ngOnChanges(changes: SimpleChanges) only provides an array of SimpleChange with the property corresponding my Directive's input (i.e. percentDigits).

I have private elementRef: ElementRef injected in the Directive's constructor, but as I said from ngOnChanges() I can't get the value of myModel.percentNumber (this.elementRef.nativeElement.value gives me "" and never any value).

Maybe I am still too soon in the life cycle ?

Or maybe another approach should be taken ?

If you have any advice I would be very happy to hear bring it out ! :)

Answer

Bob picture Bob · Jan 9, 2018

Ahh! so close with #2. You can do this all in the template. You can set the value using [value] just as an input - then use (input) - or possibly (change) to convert the value back. I don't think you can use pipes in ngModel:

<input type="number" [value]="myModel.percentNumber * 100" (input)="myModel.percentNumber = $event.target.value / 100">

No text conversions or dom manipulation this way either