Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(select): add support for custom errorStateMatcher #6147

Closed
wants to merge 7 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/lib/core/_core.scss
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@
@import 'option/option-theme';
@import 'option/optgroup';
@import 'option/optgroup-theme';
@import 'error/error';
@import 'error/error-theme';
@import 'selection/pseudo-checkbox/pseudo-checkbox-theme';
@import 'typography/all-typography';

@@ -25,6 +27,7 @@
@include mat-ripple();
@include mat-option();
@include mat-optgroup();
@include mat-error();
@include cdk-a11y();
@include cdk-overlay();
}
@@ -35,6 +38,7 @@
@include mat-option-theme($theme);
@include mat-optgroup-theme($theme);
@include mat-pseudo-checkbox-theme($theme);
@include mat-error-theme($theme);

// Wrapper element that provides the theme background when the
// user's content isn't inside of a `md-sidenav-container`.
12 changes: 7 additions & 5 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ import {OverlayModule} from './overlay/index';
import {A11yModule} from './a11y/index';
import {MdSelectionModule} from './selection/index';
import {MdRippleModule} from './ripple/index';
import {MdErrorModule} from './error/index';

// Re-exports of the CDK to avoid breaking changes.
export {
@@ -123,12 +124,11 @@ export {

// Error
export {
MdErrorModule,
MdError,
ErrorStateMatcher,
ErrorOptions,
MD_ERROR_GLOBAL_OPTIONS,
defaultErrorStateMatcher,
showOnDirtyErrorStateMatcher
} from './error/error-options';
ShowOnDirtyErrorStateMatcher,
} from './error/index';

@NgModule({
imports: [
@@ -141,6 +141,7 @@ export {
A11yModule,
MdOptionModule,
MdSelectionModule,
MdErrorModule,
],
exports: [
MdLineModule,
@@ -152,6 +153,7 @@ export {
A11yModule,
MdOptionModule,
MdSelectionModule,
MdErrorModule,
],
})
export class MdCoreModule {}
9 changes: 9 additions & 0 deletions src/lib/core/error/_error-theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import '../theming/palette';
@import '../theming/theming';


@mixin mat-error-theme($theme) {
.mat-error {
color: mat-color(map-get($theme, warn));
}
}
5 changes: 5 additions & 0 deletions src/lib/core/error/_error.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@mixin mat-error {
.mat-error {
display: block;
}
}
35 changes: 14 additions & 21 deletions src/lib/core/error/error-options.ts
Original file line number Diff line number Diff line change
@@ -6,28 +6,21 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InjectionToken} from '@angular/core';
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
import {Injectable} from '@angular/core';
import {FormGroupDirective, NgForm, NgControl} from '@angular/forms';

/** Injection token that can be used to specify the global error options. */
export const MD_ERROR_GLOBAL_OPTIONS = new InjectionToken<ErrorOptions>('md-error-global-options');

export type ErrorStateMatcher =
(control: FormControl, form: FormGroupDirective | NgForm) => boolean;

export interface ErrorOptions {
errorStateMatcher?: ErrorStateMatcher;
}

/** Returns whether control is invalid and is either touched or is a part of a submitted form. */
export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) {
const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.touched || isSubmitted));
/** Error state matcher that matches when a control is invalid and dirty. */
@Injectable()
export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false;
}
}

/** Returns whether control is invalid and is either dirty or is a part of a submitted form. */
export function showOnDirtyErrorStateMatcher(control: FormControl,
form: FormGroupDirective | NgForm) {
const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.dirty || isSubmitted));
/** Provider that defines how form controls behave with regards to displaying error messages. */
@Injectable()
export class ErrorStateMatcher {
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false;
}
}
24 changes: 24 additions & 0 deletions src/lib/core/error/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Input} from '@angular/core';

let nextUniqueId = 0;

/** Single error message to be shown underneath a form control. */
@Directive({
selector: 'md-error, mat-error',
host: {
'class': 'mat-error',
'role': 'alert',
'[attr.id]': 'id',
}
})
export class MdError {
@Input() id: string = `md-input-error-${nextUniqueId++}`;
}
23 changes: 23 additions & 0 deletions src/lib/core/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/


import {NgModule} from '@angular/core';
import {MdError} from './error';
import {ErrorStateMatcher} from './error-options';

@NgModule({
declarations: [MdError],
exports: [MdError],
providers: [ErrorStateMatcher],
})
export class MdErrorModule {}


export * from './error';
export * from './error-options';
4 changes: 0 additions & 4 deletions src/lib/input/_input-theme.scss
Original file line number Diff line number Diff line change
@@ -94,10 +94,6 @@
background-color: $input-underline-color-warn;
}
}

.mat-input-error {
color: $input-underline-color-warn;
}
}

// Applies a floating placeholder above the input itself.
6 changes: 3 additions & 3 deletions src/lib/input/index.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,6 @@

import {NgModule} from '@angular/core';
import {
MdErrorDirective,
MdHint,
MdInputContainer,
MdInputDirective,
@@ -19,11 +18,11 @@ import {
import {MdTextareaAutosize} from './autosize';
import {CommonModule} from '@angular/common';
import {PlatformModule} from '../core/platform/index';
import {MdErrorModule} from '../core/error/index';


@NgModule({
declarations: [
MdErrorDirective,
MdHint,
MdInputContainer,
MdInputDirective,
@@ -35,16 +34,17 @@ import {PlatformModule} from '../core/platform/index';
imports: [
CommonModule,
PlatformModule,
MdErrorModule,
],
exports: [
MdErrorDirective,
MdHint,
MdInputContainer,
MdInputDirective,
MdPlaceholder,
MdPrefix,
MdSuffix,
MdTextareaAutosize,
MdErrorModule,
],
})
export class MdInputModule {}
5 changes: 0 additions & 5 deletions src/lib/input/input-container.scss
Original file line number Diff line number Diff line change
@@ -252,8 +252,3 @@ textarea.mat-input-element {
.mat-input-hint-spacer {
flex: 1 0 $mat-input-hint-min-space;
}

// Single error message displayed beneath the input.
.mat-input-error {
display: block;
}
24 changes: 8 additions & 16 deletions src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ import {
getMdInputContainerPlaceholderConflictError
} from './input-container-errors';
import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options';
import {MD_ERROR_GLOBAL_OPTIONS, showOnDirtyErrorStateMatcher} from '../core/error/error-options';
import {ErrorStateMatcher, ShowOnDirtyErrorStateMatcher} from '../core/error/error-options';

describe('MdInputContainer without forms', function () {
beforeEach(async(() => {
@@ -840,7 +840,7 @@ describe('MdInputContainer with forms', () => {
fixture.componentInstance.formControl.markAsTouched();
fixture.detectChanges();

let errorIds = fixture.debugElement.queryAll(By.css('.mat-input-error'))
let errorIds = fixture.debugElement.queryAll(By.css('.mat-error'))
.map(el => el.nativeElement.getAttribute('id')).join(' ');
describedBy = inputEl.getAttribute('aria-describedby');

@@ -896,9 +896,7 @@ describe('MdInputContainer with forms', () => {
MdInputContainerWithFormErrorMessages
],
providers: [
{
provide: MD_ERROR_GLOBAL_OPTIONS,
useValue: { errorStateMatcher: globalErrorStateMatcher } }
{ provide: ErrorStateMatcher, useValue: { isErrorState: globalErrorStateMatcher } }
]
});

@@ -926,12 +924,7 @@ describe('MdInputContainer with forms', () => {
declarations: [
MdInputContainerWithFormErrorMessages
],
providers: [
{
provide: MD_ERROR_GLOBAL_OPTIONS,
useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }
}
]
providers: [{ provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher }]
});

let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
@@ -1247,7 +1240,7 @@ class MdInputContainerWithFormErrorMessages {
<md-input-container>
<input mdInput
formControlName="name"
[errorStateMatcher]="customErrorStateMatcher.bind(this)">
[errorStateMatcher]="customErrorStateMatcher">
<md-hint>Please type something</md-hint>
<md-error>This field is required</md-error>
</md-input-container>
@@ -1260,10 +1253,9 @@ class MdInputContainerWithCustomErrorStateMatcher {
});

errorState = false;

customErrorStateMatcher(): boolean {
return this.errorState;
}
customErrorStateMatcher: ErrorStateMatcher = {
isErrorState: () => this.errorState
};
}

@Component({
40 changes: 9 additions & 31 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ import {
} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {coerceBooleanProperty, Platform} from '../core';
import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
import {FormGroupDirective, NgControl, NgForm} from '@angular/forms';
import {getSupportedInputTypes} from '../core/platform/features';
import {
getMdInputContainerDuplicatedHintError,
@@ -44,12 +44,7 @@ import {
MD_PLACEHOLDER_GLOBAL_OPTIONS,
PlaceholderOptions
} from '../core/placeholder/placeholder-options';
import {
defaultErrorStateMatcher,
ErrorOptions,
ErrorStateMatcher,
MD_ERROR_GLOBAL_OPTIONS
} from '../core/error/error-options';
import {ErrorStateMatcher, MdError} from '../core/error/index';
import {Subject} from 'rxjs/Subject';
import {startWith} from '@angular/cdk/rxjs';

@@ -97,19 +92,6 @@ export class MdHint {
@Input() id: string = `md-input-hint-${nextUniqueId++}`;
}

/** Single error message to be shown underneath the input. */
@Directive({
selector: 'md-error, mat-error',
host: {
'class': 'mat-input-error',
'role': 'alert',
'[attr.id]': 'id',
}
})
export class MdErrorDirective {
@Input() id: string = `md-input-error-${nextUniqueId++}`;
}

/** Prefix to be placed the the front of the input. */
@Directive({
selector: '[mdPrefix], [matPrefix]'
@@ -151,7 +133,6 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {
private _readonly = false;
private _id: string;
private _uid = `md-input-${nextUniqueId++}`;
private _errorOptions: ErrorOptions;
private _previousNativeValue = this.value;

/** Whether the input is in an error state. */
@@ -207,7 +188,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {
get readonly() { return this._readonly; }
set readonly(value: any) { this._readonly = coerceBooleanProperty(value); }

/** A function used to control when error messages are shown. */
/** An object used to control when error messages are shown. */
@Input() errorStateMatcher: ErrorStateMatcher;

/** The input element's value. */
@@ -241,15 +222,13 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {
constructor(private _elementRef: ElementRef,
private _renderer: Renderer2,
private _platform: Platform,
private _globalErrorStateMatcher: ErrorStateMatcher,
@Optional() @Self() public _ngControl: NgControl,
@Optional() private _parentForm: NgForm,
@Optional() private _parentFormGroup: FormGroupDirective,
@Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
@Optional() private _parentFormGroup: FormGroupDirective) {

// Force setter to be called in case id was not specified.
this.id = this.id;
this._errorOptions = errorOptions ? errorOptions : {};
this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;

// On some versions of iOS the caret gets stuck in the wrong place when holding down the delete
// key. In order to get around this we need to "jiggle" the caret loose. Since this bug only
@@ -320,10 +299,9 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {

/** Re-evaluates the error state. This is only relevant with @angular/forms. */
private _updateErrorState() {
const oldState = this._isErrorState;
const control = this._ngControl;
const parent = this._parentFormGroup || this._parentForm;
const newState = control && this.errorStateMatcher(control.control as FormControl, parent);
let oldState = this._isErrorState;
let matcher = this.errorStateMatcher || this._globalErrorStateMatcher;
let newState = matcher.isErrorState(this._ngControl, this._parentFormGroup || this._parentForm);

if (newState !== oldState) {
this._isErrorState = newState;
@@ -464,7 +442,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
@ViewChild('underline') underlineRef: ElementRef;
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
@ContentChildren(MdError) _errorChildren: QueryList<MdError>;
@ContentChildren(MdHint) _hintChildren: QueryList<MdHint>;
@ContentChildren(MdPrefix) _prefixChildren: QueryList<MdPrefix>;
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;
29 changes: 16 additions & 13 deletions src/lib/input/input.md
Original file line number Diff line number Diff line change
@@ -62,7 +62,8 @@ A placeholder for the input can be specified in one of two ways: either using th
attribute on the `input` or `textarea`, or using an `md-placeholder` element in the
`md-input-container`. Using both will raise an error.

Global default placeholder options can be specified by setting the `MD_PLACEHOLDER_GLOBAL_OPTIONS` provider. This setting will apply to all components that support the floating placeholder.
Global default placeholder options can be specified by setting the `MD_PLACEHOLDER_GLOBAL_OPTIONS`
provider. This setting will apply to all components that support the floating placeholder.

```ts
@NgModule({
@@ -110,12 +111,12 @@ warn color.

### Custom Error Matcher

By default, error messages are shown when the control is invalid and either the user has interacted with
(touched) the element or the parent form has been submitted. If you wish to override this
By default, error messages are shown when the control is invalid and either the user has interacted
with (touched) the element or the parent form has been submitted. If you wish to override this
behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group
is invalid), you can use the `errorStateMatcher` property of the `mdInput`. To use this property,
create a function in your component class that returns a boolean. A result of `true` will display
the error messages.
create an `ErrorStateMatcher` object in your component class that has a `isErrorState` function which
returns a boolean. A result of `true` will display the error messages.

```html
<md-input-container>
@@ -125,21 +126,23 @@ the error messages.
```

```ts
function myErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm): boolean {
// Error when invalid control is dirty, touched, or submitted
const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.dirty || control.touched || isSubmitted)));
class MyErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
// Error when invalid control is dirty, touched, or submitted
const isSubmitted = form && form.submitted;
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted)));
}
}
```

A global error state matcher can be specified by setting the `MD_ERROR_GLOBAL_OPTIONS` provider. This applies
to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally cause
input errors to show when the input is dirty and invalid.
A global error state matcher can be specified by setting the `ErrorStateMatcher` provider. This
applies to all inputs. For convenience, `ShowOnDirtyErrorStateMatcher` is available in order to
globally cause input errors to show when the input is dirty and invalid.

```ts
@NgModule({
providers: [
{provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }}
{provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher}
]
})
```
5 changes: 3 additions & 2 deletions src/lib/select/index.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MdSelect, MD_SELECT_SCROLL_STRATEGY_PROVIDER} from './select';
import {MdCommonModule, OverlayModule, MdOptionModule} from '../core';
import {MdCommonModule, OverlayModule, MdOptionModule, MdErrorModule} from '../core';


@NgModule({
@@ -18,8 +18,9 @@ import {MdCommonModule, OverlayModule, MdOptionModule} from '../core';
OverlayModule,
MdOptionModule,
MdCommonModule,
MdErrorModule,
],
exports: [MdSelect, MdOptionModule, MdCommonModule],
exports: [MdSelect, MdOptionModule, MdCommonModule, MdErrorModule],
declarations: [MdSelect],
providers: [MD_SELECT_SCROLL_STRATEGY_PROVIDER]
})
65 changes: 64 additions & 1 deletion src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ import {Subject} from 'rxjs/Subject';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';
import {dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage} from '@angular/cdk/testing';
import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher';
import {ErrorStateMatcher} from '../core/error/error-options';
import {
FloatPlaceholderType,
MD_PLACEHOLDER_GLOBAL_OPTIONS
@@ -74,7 +75,8 @@ describe('MdSelect', () => {
BasicSelectWithoutForms,
BasicSelectWithoutFormsPreselected,
BasicSelectWithoutFormsMultiple,
SelectInsideFormGroup
SelectInsideFormGroup,
CustomErrorBehaviorSelect
],
providers: [
{provide: OverlayContainer, useFactory: () => {
@@ -2675,6 +2677,46 @@ describe('MdSelect', () => {
.toBe('true', 'Expected aria-invalid to be set to true.');
});

it('should be able to override the error matching behavior via an @Input', () => {
fixture.destroy();

const customErrorFixture = TestBed.createComponent(CustomErrorBehaviorSelect);
const component = customErrorFixture.componentInstance;
const matcher = jasmine.createSpy('error state matcher').and.returnValue(true);

customErrorFixture.detectChanges();

expect(component.control.invalid).toBe(false);
expect(component.select._isErrorState()).toBe(false);

customErrorFixture.componentInstance.errorStateMatcher = { isErrorState: matcher };
customErrorFixture.detectChanges();

expect(component.select._isErrorState()).toBe(true);
expect(matcher).toHaveBeenCalled();
});

it('should be able to override the error matching behavior via the injection token', () => {
const errorStateMatcher: ErrorStateMatcher = {
isErrorState: jasmine.createSpy('error state matcher').and.returnValue(true)
};

fixture.destroy();

TestBed.resetTestingModule().configureTestingModule({
imports: [MdSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule],
declarations: [SelectInsideFormGroup],
providers: [{ provide: ErrorStateMatcher, useValue: errorStateMatcher }],
});

const errorFixture = TestBed.createComponent(SelectInsideFormGroup);
const component = errorFixture.componentInstance;

errorFixture.detectChanges();

expect(component.select._isErrorState()).toBe(true);
expect(errorStateMatcher.isErrorState).toHaveBeenCalled();
});
});

});
@@ -3147,6 +3189,7 @@ class InvalidSelectInForm {
})
class SelectInsideFormGroup {
@ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective;
@ViewChild(MdSelect) select: MdSelect;
formControl = new FormControl('', Validators.required);
formGroup = new FormGroup({
food: this.formControl
@@ -3212,3 +3255,23 @@ class BasicSelectWithoutFormsMultiple {

@ViewChild(MdSelect) select: MdSelect;
}

@Component({
template: `
<md-select placeholder="Food" [formControl]="control" [errorStateMatcher]="errorStateMatcher">
<md-option *ngFor="let food of foods" [value]="food.value">
{{ food.viewValue }}
</md-option>
</md-select>
`
})
class CustomErrorBehaviorSelect {
@ViewChild(MdSelect) select: MdSelect;
control = new FormControl();
foods: any[] = [
{ value: 'steak-0', viewValue: 'Steak' },
{ value: 'pizza-1', viewValue: 'Pizza' },
];
errorStateMatcher: ErrorStateMatcher;
}

13 changes: 7 additions & 6 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ import {
// tslint:disable-next-line:no-unused-variable
import {ScrollStrategy, RepositionScrollStrategy} from '../core/overlay/scroll';
import {Platform} from '@angular/cdk/platform';
import {ErrorStateMatcher} from '../core/error/error-options';

/**
* The following style constants are necessary to save here in order
@@ -360,6 +361,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
/** Input that can be used to specify the `aria-labelledby` attribute. */
@Input('aria-labelledby') ariaLabelledby: string = '';

/** An object used to control when error messages are shown. */
@Input() errorStateMatcher: ErrorStateMatcher;

/** Combined stream of all of the child options' change events. */
get optionSelectionChanges(): Observable<MdOptionSelectionChange> {
return merge(...this.options.map(option => option.onSelectionChange));
@@ -386,6 +390,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
private _changeDetectorRef: ChangeDetectorRef,
private _overlay: Overlay,
private _platform: Platform,
private _globalErrorStateMatcher: ErrorStateMatcher,
renderer: Renderer2,
elementRef: ElementRef,
@Optional() private _dir: Directionality,
@@ -633,12 +638,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On

/** Whether the select is in an error state. */
_isErrorState(): boolean {
const isInvalid = this._control && this._control.invalid;
const isTouched = this._control && this._control.touched;
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
(this._parentForm && this._parentForm.submitted);

return !!(isInvalid && (isTouched || isSubmitted));
const matcher = this.errorStateMatcher || this._globalErrorStateMatcher;
return matcher.isErrorState(this._control, this._parentFormGroup || this._parentForm);
}

/**