diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index 90e9e2474732..8b194ef9c0e5 100644 --- a/src/lib/core/_core.scss +++ b/src/lib/core/_core.scss @@ -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`. diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index fdf65913c617..fe886f3d0fbf 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -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 {} diff --git a/src/lib/core/error/_error-theme.scss b/src/lib/core/error/_error-theme.scss new file mode 100644 index 000000000000..b5c2d65602f1 --- /dev/null +++ b/src/lib/core/error/_error-theme.scss @@ -0,0 +1,9 @@ +@import '../theming/palette'; +@import '../theming/theming'; + + +@mixin mat-error-theme($theme) { + .mat-error { + color: mat-color(map-get($theme, warn)); + } +} diff --git a/src/lib/core/error/_error.scss b/src/lib/core/error/_error.scss new file mode 100644 index 000000000000..698dafba6744 --- /dev/null +++ b/src/lib/core/error/_error.scss @@ -0,0 +1,5 @@ +@mixin mat-error { + .mat-error { + display: block; + } +} diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index e4d1bb4d16f2..a6c3570ac8d6 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -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('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; + } } diff --git a/src/lib/core/error/error.ts b/src/lib/core/error/error.ts new file mode 100644 index 000000000000..1ecdcd277439 --- /dev/null +++ b/src/lib/core/error/error.ts @@ -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++}`; +} diff --git a/src/lib/core/error/index.ts b/src/lib/core/error/index.ts new file mode 100644 index 000000000000..7883b967f43c --- /dev/null +++ b/src/lib/core/error/index.ts @@ -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'; diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index e4b9ea76a6d8..34a6f0665cec 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -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. diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts index b616a558408d..9571519a2ef5 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -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,9 +34,9 @@ import {PlatformModule} from '../core/platform/index'; imports: [ CommonModule, PlatformModule, + MdErrorModule, ], exports: [ - MdErrorDirective, MdHint, MdInputContainer, MdInputDirective, @@ -45,6 +44,7 @@ import {PlatformModule} from '../core/platform/index'; MdPrefix, MdSuffix, MdTextareaAutosize, + MdErrorModule, ], }) export class MdInputModule {} diff --git a/src/lib/input/input-container.scss b/src/lib/input/input-container.scss index 0154daac6635..41ec46a7f8d3 100644 --- a/src/lib/input/input-container.scss +++ b/src/lib/input/input-container.scss @@ -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; -} diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 4c1b92ed567c..d8309d2b40b9 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -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 { + [errorStateMatcher]="customErrorStateMatcher"> Please type something This field is required @@ -1260,10 +1253,9 @@ class MdInputContainerWithCustomErrorStateMatcher { }); errorState = false; - - customErrorStateMatcher(): boolean { - return this.errorState; - } + customErrorStateMatcher: ErrorStateMatcher = { + isErrorState: () => this.errorState + }; } @Component({ diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 58ec2afe5fb8..5bac1ee57d5f 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -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; + @ContentChildren(MdError) _errorChildren: QueryList; @ContentChildren(MdHint) _hintChildren: QueryList; @ContentChildren(MdPrefix) _prefixChildren: QueryList; @ContentChildren(MdSuffix) _suffixChildren: QueryList; diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 36e4bfe0e805..83e0ebd84fe2 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -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 @@ -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} ] }) ``` diff --git a/src/lib/select/index.ts b/src/lib/select/index.ts index bf5f903905fb..0665f2eee6fa 100644 --- a/src/lib/select/index.ts +++ b/src/lib/select/index.ts @@ -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] }) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index b34f502d9f71..aaf7898694c1 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -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: ` + + + {{ food.viewValue }} + + + ` +}) +class CustomErrorBehaviorSelect { + @ViewChild(MdSelect) select: MdSelect; + control = new FormControl(); + foods: any[] = [ + { value: 'steak-0', viewValue: 'Steak' }, + { value: 'pizza-1', viewValue: 'Pizza' }, + ]; + errorStateMatcher: ErrorStateMatcher; +} + diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 7a53b46b53c9..0ae51880ea56 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -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 { 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); } /**