1
votes

I tried to create a custom directive to allow users to type only numbers in a text field and those numbers will be formatted using the locale format (thousands separators, decimal separator)

I have been able to create a directive that use a custom pipe to do so, however, I can't set the default value using the correct format.

I've made a Plunker so that you know exactly what I'm talking about :

https://next.plnkr.co/edit/ToxPEEooR5lvCJOm?preview

I don't know why but I can't make the demo app on Plunker work. Probably a small issue.

Here is my directive

import { Directive, HostListener, ElementRef, OnInit, Input } from '@angular/core';
import { NumberMaskPipe } from './number-mask.pipe';

@Directive({
  selector: 'input[type=text][numberMask]'
})
export class NumberMaskDirective implements OnInit {

  private el: HTMLInputElement;
  // tslint:disable-next-line:no-input-rename
  @Input('ngModel') private initialValue: any;

  constructor(
    private elementRef: ElementRef,
    private numberMaskPipe: NumberMaskPipe
  ) {
    this.el = this.elementRef.nativeElement;
  }

  public ngOnInit() {
    console.log('ngOnInit', this.initialValue); // LOG ngOnInit 1005698
    console.log(this.numberMaskPipe.transform(this.initialValue)); // LOG 1,005,698.00
    console.log('this.el', this.el); // LOG this.el <input _ng2content-c2 class="form-control...
    console.log('this.el.value', this.el.value); // this.el.value is empty on init. Why ??
    this.el.value = this.numberMaskPipe.transform(this.initialValue); // Does not change the input value. Why ??
  }

  @HostListener('focus', ['$event.target.value'])
  onFocus(value) {
    this.el.value = this.numberMaskPipe.parse(value); // opposite of transform
  }

  @HostListener('blur', ['$event.target.value'])
  onBlur(value) {
    this.el.value = this.numberMaskPipe.transform(value);
  }

  @HostListener('keydown', ['$event']) onKeyDown(event) {
    const e = < KeyboardEvent > event;
    if ([46, 8, 9, 27, 13, 110, 190].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();
    }
  }
}

Could you please tell me how to initialize the input field with its value formatted using my directive ?

Thanks in advance

1

1 Answers

0
votes

The data for the DOM element is not yet bound when ngOnInit() fires for any attached directives so the value is bound afterwards and any changes made in the directives' ngOnInit() are lost. The only way I have found you can fire a function after the data has been bound in a directive is to implement DoCheck.

So you would want to do something like this:

export class NumberMaskDirective implements DoCheck {
private firstRun = true;

public ngDoCheck() {
    if (firstRun) {
        this.el.value = this.numberMaskPipe.transform(this.initialValue);
        firstRun = false;
    }
}

The reason for the flag is that ngDoCheck is called every time the data model is updated. You could also use ngAfterContentChecked or ngAfterViewChecked but they have the same problem as ngDoCheck of firing on every update so I'm just going with ngDoCheck because it fires first of all three of these hooks. I am not sure if there is a better lifecycle hook to use that does not require a flag to ensure it only runs once, but I have not been able to find one.