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
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
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