Search code examples
angularangular-materialangular8mat-form-fieldmat-error

mat-error is not visible on a component that I created


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


Solution

  • 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