2

I created a Currency Directive which I will use in every input element which needs currency format.

So I have 2 Host Listeners one is OnFocus, the second is Blur

And it works perfectly. But I need to format the value of input when I set the value of input by binding

So when I open a modal I get unformatted value... NgOnInit doesn't work because it raises too much early

Here is my directive code.

import { Directive, HostListener, Input, OnInit, ElementRef, AfterViewInit, OnChanges, Renderer2, ViewChild } from '@angular/core';

import { CurrencyPipe, getCurrencySymbol } from '@angular/common';

import { NgControl, ControlValueAccessor } from '@angular/forms';

import { CustomCurrencyPipe } from '../pipes/custom-currency.pipe';

import { ModalDirective } from 'ngx-bootstrap/modal';



@Directive({

  selector: '[appCurencyFormat]',

  providers: [CustomCurrencyPipe]

})



export class CurrencyFormatDirective implements OnInit{

  //@Input('appNumberFormat') params: any;

  @Input() decimalNumber: number = 2;

  @Input() symbol: string = "symbol";

  //@Input() OnlyNumber: boolean;

  local: string;

  decimal: string;

  currency: string;

  element: any;



  @ViewChild(ModalDirective) childModal: ModalDirective;



  constructor(private elementRef: ElementRef, private ngControl: NgControl, private currencyPipe: CustomCurrencyPipe, private _renderer: Renderer2) {


    this.element = this.elementRef.nativeElement;    

  }



  @HostListener('keydown', ['$event']) onKeyDown(event) {

    let e = <KeyboardEvent>event;

    //190 in array for .

      if ([46, 8, 9, 27, 13, 110].indexOf(e.keyCode) !== -1 ||

        // Allow: Ctrl+A

        (e.keyCode === 65 && (e.ctrlKey || e.metaKey)) ||

        // Allow: Ctrl+C

        (e.keyCode === 67 && (e.ctrlKey || e.metaKey)) ||

        // Allow: Ctrl+V

        (e.keyCode === 86 && (e.ctrlKey || e.metaKey)) ||

        // Allow: Ctrl+X

        (e.keyCode === 88 && (e.ctrlKey || e.metaKey)) ||

        // Allow: home, end, left, right

        (e.keyCode >= 35 && e.keyCode <= 39)) {

        // let it happen, don't do anything

        return;

      }

      // Ensure that it is a number and stop the keypress

      if ((e.shiftKey || (e.keyCode < 48 || e.keyCode > 57)) && (e.keyCode < 96 || e.keyCode > 105)) {

        e.preventDefault();

      }    

  }  



  @HostListener('focus', ['$event.target.value'])

  onFocus(value: any) {

    this.ctrl.setValue(this.currencyPipe.convertToNumber(value));    

  }



  @HostListener('blur', ['$event.target.value'])

  onBlur(value: any) {

    this.ctrl.setValue(this.currencyPipe.transform(value, this.decimalNumber, this.symbol));

  }



  get ctrl() {    

    return this.ngControl.control;

  }

}

My solution is something with a set interval in ngOnInit...

ngOnInit() {
        let m = window.setInterval(() => {
        console.log("Upao sam");
        console.log(this.ctrl.value);
        if (this.ctrl.value) {
          console.log(this.ctrl.value);
          if (seted) {
            window.clearInterval(m);
          } else {
            seted = true;
            this.ctrl.setValue(this.currencyPipe.transform(this.ctrl.value, this.decimalNumber, this.symbol));
          }
        }
      }, 500);
}

Does anyone have any idea which HostListener I can use for it, to try to avoid using window.setInterval(). Or if anyone has any idea how can I resolve this problem?

UPDATE

ngOnChanges() isn't raised every time, so the selected duplicate question can't resolve my problem.

GlacialMan
  • 579
  • 2
  • 5
  • 20

4 Answers4

2

I would use the whole value accessor thing. Thats what I use for all my inputs. You can write something like this

@Component({
  selector: 'ks-input',
  template: `<input [(ngModel)]="value" />`,
  styleUrls: ['./whatever.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputComponent),
      multi: true
    }
  ]
})

export class InputComponent implements ControlValueAccessor {

  @Input('value') _value = '';
  onChange: any = () => {
  };
  onTouched: any = () => {
  };

  get value() {
    return this._value;
  }

  set value(val) {
    this._value = val;
    this.onChange(val);
    this.onTouched();
  }

  registerOnChange(fn) {
    this.onChange = fn;
  }

  registerOnTouched(fn) {
    this.onTouched = fn;
  }

  writeValue(value) {
    this.value = value;
  }
}

When you pass a variable to this component it will first pass through the writeValue function where you can do any initial formatting. you can also do what ever in the set value function which is called everytime the value/input ngModel variable changes.

Matt
  • 451
  • 2
  • 7
  • Hmm, nice solution it can be. I'll definitely try it if I don't find a better solution. – GlacialMan Jul 17 '19 at 08:47
  • @GlacialMan Yes, all of my form inputs are much more complex than the very simple example I showed with things like curency format, phone number format, ect. and I use that and it works wonders. With angular ive found thats the only way to have full control over your form controls. I think that was actually designed for your specific use case. – Matt Jul 17 '19 at 08:51
  • I'll give you a bounty because I tried all other solutions. Your solution must work but it's much more complex. – GlacialMan Jul 22 '19 at 13:47
  • https://stackoverflow.com/questions/51474527/format-input-as-number-with-locale-in-angular-6 – GlacialMan Jul 22 '19 at 13:48
1

Here is what you can do to resolve this issue :

<input [ngModel]="item | customCurrencyPipe:'USD':'symbol':'2.2'" name="inputField" type="text" 
(ngModelChange)="item = $event||0.00" [ngModelOptions]="{updateOn:'blur'}"/>

1) Split the binding into a one-way binding and an event binding.

2) With Event-binding "ngModelChange" restore value with the input provided.

3) Update Value on 'blur', so any text which is not a number cannot be served // optional based on requirement

4) customCurrencyPipe : This will have the default features of currency pipe, but will not escalate if a number is not provided instead will return the default value or won't allow other than numbers // based on requirement

With above steps, you will be able to achieve desired results with less hack and a promising solution.

yanky_cranky
  • 1,303
  • 1
  • 11
  • 16
  • Which event I can use for updateOn if I want to format numbers on keyPressed. I tried with keydown but it doesn't work. For some reason, I don't get $ only formatted numbers and only one-time debugger shows me that it's in my function transform of customCurrencyPipe. Another time I don't know where it goes. – GlacialMan Jul 17 '19 at 08:11
  • you can use : [ngModelOptions]="{ debounce:200,updateOn: 'change' }" – yanky_cranky Jul 17 '19 at 09:24
  • How I can move the vertical line... I can enter 1-2 numbers but all others going in the decimal part of number – GlacialMan Jul 17 '19 at 11:53
  • This you have to address in your custom pipe, wherein you are passing '2.2' as second param. So, basic logic would be to get the number and filter it with your pipe to prepend it with the symbol. Now, the debounce time helps you to wait till user stops typing – yanky_cranky Jul 17 '19 at 12:22
  • hmm, I can use your solution but I want to format numbers on focus and on blur. On focus, I would like to use directive, on blur I would like to use a pipe. For example $123,123.00 - on focus will be 123123.00, when user change number on blur it will get $ and , separator for thousands. Is it possible? – GlacialMan Jul 22 '19 at 07:17
  • Ideally the separator should corresponds to the region, for that you need to set [locale](https://angular.io/guide/i18n#setting-up-the-locale-of-your-app) , however if you want separator for thousands only, you can write your regex in your pipe itself and feed it to the output – yanky_cranky Jul 22 '19 at 07:45
  • yeah i have locale, but how it can be used in input? – GlacialMan Jul 22 '19 at 13:12
1

The solution is format the value of input inside ngDoCheck() instead of ngOnChanges().

ngDoCheck() is a guranteed life cycle hook that will run every time Angular trigger change detection.

A small working example showing that ngDoCheck() hook of the directive will always be triggered when there is change.


For understanding why ngOnChanges() does not raise all the time and why use ngDoCheck() instead:

Whenever a binding Input property from a directive changes, it trigger Angular change detection. And how Angular detect if that Input is changed ? It compare the Simple Change object represent that Input's old and new values, if there is differences between those, then Angular trigger ngOnChanges() life cycle hook.

So there is case that you did entered something but the value compared doesnt change( for example you change an Input by mutate and Object 's property, so actually the old and new value of the Simple Change is the same because Angular is comparing the reference, not the value), in this case it does not raise the ngOnChange() hook.


But ngDoChek() is something different. I will use this informations from a very good article regarding ngDoCheck():

Suppose we have the following components/directive tree:

Component A
    Component B
        Component C

So when Angular runs change detection the order of operations is the following:

Checking A component:
  - update B input bindings
  - call NgDoCheck on the B component
  - update DOM interpolations for component A
 
 Checking B component:
    - update C input bindings
    - call NgDoCheck on the C component
    - update DOM interpolations for component B
 
   Checking C component:
      - update DOM interpolations for component C

As you seeing it, whenever there is change detection run, ngDoCheck() is always be called. Beside that, It also be called when the component/directive is intialized (reset the my blitzstack example to see that). So:

Format the value of input inside ngDoCheck() will solve your problem

Community
  • 1
  • 1
Ethan Vu
  • 2,911
  • 9
  • 25
  • Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'model: undefined'. Current value: 'model: null'. – GlacialMan Jul 22 '19 at 13:11
  • and it shows me an error every time when I enter the number by number... – GlacialMan Jul 22 '19 at 13:44
1

I had the exact same problem as you. I was able to solve it using NgControl hook to access the form control from the directive.

I added NgControl to the directive constructor:

constructor(
  private el: ElementRef, 
  private decimalPipe: DecimalPipe,
  @Self() private ngControl: NgControl) { }

This way, I was able to subscribe to the form control value changes observable. The observable triggers with both user and code changes, so to avoid formatting while the user is typing I validate that the control is pristine:

ngOnInit(): void {
  this.ngControl.valueChanges
    .subscribe(value => {
      if (this.ngControl.pristine) {
        this.el.nativeElement.value = 
          this.decimalPipe.transform(value, `1.2-2`);
      }
    });

I hope it can be useful for someone else. I got the idea from this article https://netbasal.com/attribute-directives-angular-forms-b40503643089