Search code examples
angularangular-forms

Format and Validate within one directive


I am trying to build a directive to work on my forms which deal with durations. I want the form to appear with a textbox which has a format like 1:24, but a value of total minutes - so 144 in this case (this will ease adding lots of times up later on!).

Here is my current code:

@Directive({
...
)
export class DurationFormatDirective implements ControlValueAccessor {
  @HostBinding('value') inputValue;

  mins: number;
  onChange;
  onTouched;

  constructor() {}

  @HostListener('blur', ['$event.target.value'])
  onBlur(value: string) {
    let hrs: number, mins: number;

    if (value === '' || value === '0') {
      hrs = 0;
      mins = 0;
    } else {
      if (value.indexOf(':') === -1) { // there is no colon
        if (value.length <= 2) {
          hrs = 0;
          mins = +value;
        } else {
          hrs = +value.substring(0, value.length - 2);
          mins = +value.substring(value.length - 2);
        }
      } else { // There is a colon
        const arr = value.split(':');
        hrs = +arr[0];
        mins = +arr[1];
      }
    }

    this.mins = hrs * 60 + mins;
    this.inputValue = `${hrs}:${mins < 10 ? '0' : ''}${mins}`;
    this.onChange(this.mins);
    this.onTouched();
  }

  writeValue(val) {
    this.mins = val;

    const hrs = Math.floor(val / 60);
    const mins = val % 60;

    this.inputValue = `${hrs}:${mins < 10 ? '0' : ''}${mins}`;
  }

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

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

Generally this is working well, but if the user enters 2:zw for example - it breaks, because zw is not a number. If an invalid (IE Not 0-9 or :) is entered, it should declare the field invalid and not attempt to format it or update the value. Can I also make this directive change the valid property at the same time. If it makes a difference I am using reactive forms.

Thanks


Solution

  • So I actually solved this in a different way.

    I added a RegExp test to look for letters and an if statement to look for minutes > 60. In either case the value is set to NaN.

    I then created a very simple validator to check for NaN on the parent form.

    This also allows me to do another validation later, where I check that the entered duration is no greater than the overall duration from another field.

    Complete code for directive:

    @Directive({
      selector: '[appTimeFormat]',
    })
    export class TimeFormatDirective implements ControlValueAccessor {
      @HostBinding('value') inputValue;
    
      onChange;
      onTouched;
    
      private regex = /^\d{1,2}:?\d{0,2}$/;
    
      constructor() {}
    
      @HostListener('blur', ['$event.target.value'])
      onBlur(value: string) {
        this.onTouched();
    
        if (!this.regex.test(value)) {
          this.onChange(NaN);
          return;
        }
    
        let hrs: number, mins: number;
    
        if (value === '' || value === '0') {
          hrs = 0;
          mins = 0;
        } else {
          if (value.indexOf(':') === -1) {
            // There is no colon
            if (value.length <= 2) {
              hrs = 0;
              mins = +value;
            } else {
              hrs = +value.substring(0, value.length - 2);
              mins = +value.substring(value.length - 2);
            }
          } else {
            // There is a colon
            const arr = value.split(':');
            hrs = +arr[0];
            mins = +arr[1];
          }
        }
    
        this.inputValue = `${hrs}:${mins < 10 ? '0' : ''}${mins}`;
    
        if (mins > 60) {
          this.onChange(NaN);
          return;
        }
    
        this.onChange(hrs * 60 + mins);
      }
    
      writeValue(val) {
        if (val) {
          const hrs = Math.floor(val / 60);
          const mins = val % 60;
    
          this.inputValue = `${hrs}:${mins < 10 ? '0' : ''}${mins}`;
        } else {
          this.inputValue = '';
        }
      }
    
      registerOnChange(fn) {
        this.onChange = fn;
      }
    
      registerOnTouched(fn) {
        this.onTouched = fn;
      }
    }
    

    Validator:

      nanValidator(control: FormControl) {
        if (isNaN(control.value)) {
          return { invalidNumber: true };
        }
        return null;
      }