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

3

9

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 ! :)

Muddy answered 8/11, 2017 at 17:32 Comment(1)
did you resolve the problem?Neomineomycin
R
4

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

Rothenberg answered 8/1, 2018 at 23:47 Comment(2)
That would be such an elegant solution, if it was working lolNoun
Indeed, very elegant solution. Is there anyway to use [ngModel]="myModel" along with it? I need to assign the ngModel to #myModel="ngModel" so I can check if [ngClass]="{ 'is-invalid': myModel.invalid }". Alternatively, is there anyway to check if myModel is invalid without using ngModel?Rothrock
A
0

Just a line of thought, let me know if it contradicts with your line of thinking.

Suppose you have a pipe that transforms something like 97.52 => 97,52 % (or however you want it to look like after transforming). It also takes in a 'second argument' which is a boolean based on which you format (97,52 %) or unformat (97.52) your data.

// pipe's transform example
transform(value: any, showFormatted: boolean): string {
  if(showFormatted) { return <formatted-value>; // 97,52 % }
  else { return <unformatted-value>; // 97.52 }
}

Now, make use of a directive that toggles this second argument showFormatted (initialized inside the component to either true or false, likely true in your case) to the pipe, based on onFocus and onBlur events as shown in your question. So this will make sure all your transformations are always going through the 'one pipe' you have and that itself either bypasses the content or transforms it based on its second argument.

Now the question is only about initial load and the final saving scenario. For initial load, if you know the format it is coming in, then just do a transformation there itself to the 'unformatted form' of the value to be given to the input field. So for example, if you know it it coming as 0.9752, just convert it to 97.52 using any of the applicable js techniques. It is only a one-time thing as the subsequent interactions will be handled by the pipe and directive combined. Then upon storing the data at the end, just do this reverse transformation from 97.52 % to 0.9752.

Does that work for you?

Abirritant answered 8/11, 2017 at 17:55 Comment(3)
Hi. Thanks for the proposal, however if I understand well, I don't think this will work.Muddy
You suggested having a argument showFormatted at the Component level, however in my Component I display a table with multiple <input>, and each <input> must be independent (I don't want all <input> to be toggled once to edit mode, i.e. loosing the percent formatting, as soon as I focus on one <input> which is what would happen if I have a unique argument showFormatted).Muddy
Also for the initial load, I understand you propose to change the value from 0.9752 to 95.52; this is some thing I will have to remember to do for each use of the input field where I want to display an editable percent value. Plus it also requires to do the popsite operation each time I need to save the value (and remember to do this for each scenario where I use an editable percent value).Muddy
M
0

Add a ngOnInit in your directive

ngOnInit()
{
    this.htmlInputElement.value = this.parse(value);
}
Myrtamyrtaceous answered 8/11, 2017 at 19:37 Comment(2)
Hi. Thanks for the answer. However ngOnInit() at the component level does not work as, like I indicated in item 3), I get the data not at ngOnInit (of the Component, thus not at ngOnInit of the Directive), but after the backend response (observable/subscribe). So at ngOnInit time, I don't have the data yetMuddy
I don't probe, but in blog.ngconsultant.io/… you have an example like you are trying to doMyrtamyrtaceous

© 2022 - 2024 — McMap. All rights reserved.