1

I use angular 8 with angular material to create my app.

I have the following form field defined:

<mat-form-field floatLabel="always">
        <app-my-datetime-input placeholder="From" formControlName="fromDatetime"></app-my-datetime-input>
        <mat-error>{{getError('fromDatetime')}}hello</mat-error>
        <mat-hint>YYYY-MM-DD HH:MM:SS</mat-hint>
      </mat-form-field>
  

and app-my-datetime-input is a component that I created with the following code:

the html:

<div [formGroup]="parts">
  <input  matInput mask="0000-00-00 00:00:00" formControlName="datetime" (input)="_handleInput()" />
</div>

and this is the typescript:

import {Component, ElementRef, forwardRef, HostBinding, Input, OnDestroy, OnInit, Optional, Self, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgControl} from '@angular/forms';
import {MatFormFieldControl, MatInput} from '@angular/material';
import {Subject} from 'rxjs';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {FocusMonitor} from '@angular/cdk/a11y';

@Component({
  selector: 'app-my-datetime-input',
  templateUrl: './my-datetime-input.component.html',
  styleUrls: ['./my-datetime-input.component.scss'],
  providers: [{provide: MatFormFieldControl, useExisting: MyDatetimeInputComponent}],
})

export class MyDatetimeInputComponent implements ControlValueAccessor, MatFormFieldControl<string>,
              OnDestroy {

  get empty() {
    const {value: {datetime}} = this.parts;

    return !datetime;
  }
  // TODO: fix should label float
  get shouldLabelFloat() { return this.focused || !this.empty; }

  @Input()
  get placeholder(): string { return this._placeholder; }
  set placeholder(value: string) {
    this._placeholder = value;
    this.stateChanges.next();
  }

  @Input()
  get required(): boolean { return this._required; }
  set required(value: boolean) {
    this._required = coerceBooleanProperty(value);
    this.stateChanges.next();
  }

  @Input()
  get disabled(): boolean { return this._disabled; }
  set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
    this._disabled ? this.parts.disable() : this.parts.enable();
    this.stateChanges.next();
  }

  @Input()
  get value(): string {
    const {value: {datetime}} = this.parts;
    return datetime;
  }
  set value(datetime: string) {
    this.parts.setValue({datetime});
    this.stateChanges.next();
  }

  constructor(
    formBuilder: FormBuilder,
    // tslint:disable-next-line:variable-name
    private _focusMonitor: FocusMonitor,
    // tslint:disable-next-line:variable-name
    private _elementRef: ElementRef<HTMLElement>,
    @Optional() @Self() public ngControl: NgControl) {

    this.parts = formBuilder.group({
      datetime: '',
    });

    _focusMonitor.monitor(_elementRef, true).subscribe(origin => {
      if (this.focused && !origin) {
        this.onTouched();
      }
      this.focused = !!origin;
      this.stateChanges.next();
    });

    if (this.ngControl != null) {
      this.ngControl.valueAccessor = this;
    }
  }
  static nextId = 0;

  parts: FormGroup;
  stateChanges = new Subject<void>();
  focused = false;
  errorState = false;
  controlType = 'my-datetime-input';
  id = `my-datetime-input-${MyDatetimeInputComponent.nextId++}`;
  describedBy = '';
  // tslint:disable-next-line:variable-name
  private _placeholder: string;
  // tslint:disable-next-line:variable-name
  private _required = false;
  // tslint:disable-next-line:variable-name
  private _disabled = false;
  onChange = (_: any) => {};
  onTouched = () => {};

  ngOnDestroy() {
    this.stateChanges.complete();
    this._focusMonitor.stopMonitoring(this._elementRef);
  }

  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() !== 'input') {
      // tslint:disable-next-line:no-non-null-assertion
      this._elementRef.nativeElement.querySelector('input')!.focus();
    }
  }

  writeValue(val: string): void {
    this.value = val;
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  _handleInput(): void {
    this.onChange(this.parts.value.datetime);
  }

}

this is the first time that I'm creating my own form field component so I probably did something wrong there.. mat-error in not visible. as you can see I appended the word hello to the end of mat-error and I still don't see it displayed. so I'm guessing I should have implemented MatFormFieldControl is a .. less buggy way?! :) so I don't really know what I did wrong so any information regarding this issue would be greatly appreciated.

thank you

update

added (blur)="onTouched() but unfortunately the results are the same.

I have a form validation that makes sure that from date is not newer the to date. this is my validation function:

 static fromToRangeValidator(): ValidatorFn {
      return (group: FormGroup): ValidationErrors => {
        const fromDate = group.get('fromDatetime');
        const toDate = group.get('toDatetime');
        if (fromDate.value !== '' && toDate.value !== '') {
          const fromMoment = moment(fromDate.value, 'YYYYMMDDHHmmss');
          const toMoment = moment(toDate.value, 'YYYYMMDDHHmmss');
          if (toMoment.isBefore(fromMoment)) {
            fromDate.setErrors({greaterThen: true});
            toDate.setErrors({lessThen: true});
          }
        }
        return;
      };
    }

and the form doesn't submit because of the error but the error is still not shown

Community
  • 1
  • 1
ufk
  • 30,912
  • 70
  • 235
  • 386

1 Answers1

1

A mat-error only it's showed if the control is touched(*), so you need say that your control is touched when something happens. Dont miss, your input inside the custom form control is touched, but not the custom formControl itself.

You can use (blur)

<div [formGroup]="parts">
  <input matInput mask="0000-00-00 00:00:00" 
      formControlName="datetime" 
      (blur)="onTouched()"
      (input)="_handleInput()" />
</div>

Update I see that you applied the mat-error to the FormControl "fromDate". So the validator must be applied to the formControl, not to the FormGroup -else is the formGroup who is invalid-

The custom validators must be then

fromToRangeValidator(): ValidatorFn {
      //see that the argument is a FormControl
      return (control: FormControl): ValidationErrors => {
        //the formGroup is control.parent
        const group=control.parent;
        //but we must sure that is defined
        if (!group) return null;
        const fromDate = group.get('fromDatetime');
        const toDate = group.get('toDatetime');
        //...rest of your code...
        if (fromDate.value !== '' && toDate.value !== '') {
          const fromMoment = moment(fromDate.value, 'YYYYMMDDHHmmss');
          const toMoment = moment(toDate.value, 'YYYYMMDDHHmmss');
          if (toMoment.isBefore(fromMoment)) {
            fromDate.setErrors({greaterThen: true});
            toDate.setErrors({lessThen: true});
          }
        }
        return;
      };
    }
}

And, when you create the form you applied the validator to the control

form=new FormGroup({
    fromDatetime:new FormControl('',this.fromToRangeValidator()),
    toDatetime:new FormControl()
  })

(*)Really you can change this behavior using a custom ErrorStateMatcher, but it's not the question planned

Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • hi! unfortunately the results are the same. I added more info on my post. thanks – ufk Oct 06 '19 at 15:15
  • You're applied the error to the FormGroup and the mat-error to the FormControl. I updated my answer to re-write your customValidator to check the FormControl. If I has some more time, I try to make a stackblitz – Eliseo Oct 06 '19 at 17:32
  • err.. results are the same.. trying to investigate what's going on – ufk Oct 10 '19 at 14:30
  • check this using customErrorMatcher https://stackoverflow.com/questions/56887035/custom-controls-with-reactive-forms/56893298#56893298 – Eliseo Oct 10 '19 at 18:24