diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html
index c0d6c7a9dc9a..c70fa0349f41 100644
--- a/src/demo-app/input/input-demo.html
+++ b/src/demo-app/input/input-demo.html
@@ -94,6 +94,17 @@
Inside a form
+
+ With a custom error function
+
+
+ This field is required
+
+
diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts
index 387e632dbee5..f0b7d9d7bbbd 100644
--- a/src/demo-app/input/input-demo.ts
+++ b/src/demo-app/input/input-demo.ts
@@ -23,6 +23,7 @@ export class InputDemo {
errorMessageExample1: string;
errorMessageExample2: string;
errorMessageExample3: string;
+ errorMessageExample4: string;
dividerColorExample1: string;
dividerColorExample2: string;
dividerColorExample3: string;
@@ -43,4 +44,11 @@ export class InputDemo {
this.items.push({ value: ++max });
}
}
+
+ customErrorStateMatcher(c: FormControl): boolean {
+ const hasInteraction = c.dirty || c.touched;
+ const isInvalid = c.invalid;
+
+ return !!(hasInteraction && isInvalid);
+ }
}
diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts
index 0be7b12636bb..96d2c009d5ac 100644
--- a/src/lib/core/core.ts
+++ b/src/lib/core/core.ts
@@ -118,6 +118,15 @@ export {
MD_PLACEHOLDER_GLOBAL_OPTIONS
} from './placeholder/placeholder-options';
+// Error
+export {
+ ErrorStateMatcher,
+ ErrorOptions,
+ MD_ERROR_GLOBAL_OPTIONS,
+ defaultErrorStateMatcher,
+ showOnDirtyErrorStateMatcher
+} from './error/error-options';
+
@NgModule({
imports: [
MdLineModule,
diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts
new file mode 100644
index 000000000000..ec1cd71e42ea
--- /dev/null
+++ b/src/lib/core/error/error-options.ts
@@ -0,0 +1,33 @@
+/**
+ * @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 {InjectionToken} from '@angular/core';
+import {FormControl, FormGroupDirective, Form, NgForm} 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));
+}
+
+/** 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));
+}
diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts
index cd10fb53dbe3..089d3b259958 100644
--- a/src/lib/input/input-container.spec.ts
+++ b/src/lib/input/input-container.spec.ts
@@ -23,6 +23,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';
describe('MdInputContainer', function () {
beforeEach(async(() => {
@@ -56,6 +57,7 @@ describe('MdInputContainer', function () {
MdInputContainerWithDynamicPlaceholder,
MdInputContainerWithFormControl,
MdInputContainerWithFormErrorMessages,
+ MdInputContainerWithCustomErrorStateMatcher,
MdInputContainerWithFormGroupErrorMessages,
MdInputContainerWithId,
MdInputContainerWithPrefixAndSuffix,
@@ -749,6 +751,113 @@ describe('MdInputContainer', function () {
});
+ describe('custom error behavior', () => {
+ it('should display an error message when a custom error matcher returns true', () => {
+ let fixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
+ fixture.detectChanges();
+
+ let component = fixture.componentInstance;
+ let containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
+
+ const control = component.formGroup.get('name')!;
+
+ expect(control.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('md-error').length)
+ .toBe(0, 'Expected no error messages');
+
+ control.markAsTouched();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('md-error').length)
+ .toBe(0, 'Expected no error messages after being touched.');
+
+ component.errorState = true;
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('md-error').length)
+ .toBe(1, 'Expected one error messages to have been rendered.');
+ });
+
+ it('should display an error message when global error matcher returns true', () => {
+
+ // Global error state matcher that will always cause errors to show
+ function globalErrorStateMatcher() {
+ return true;
+ }
+
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MdInputModule,
+ NoopAnimationsModule,
+ ReactiveFormsModule,
+ ],
+ declarations: [
+ MdInputContainerWithFormErrorMessages
+ ],
+ providers: [
+ {
+ provide: MD_ERROR_GLOBAL_OPTIONS,
+ useValue: { errorStateMatcher: globalErrorStateMatcher } }
+ ]
+ });
+
+ let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
+
+ fixture.detectChanges();
+
+ let containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
+ let testComponent = fixture.componentInstance;
+
+ // Expect the control to still be untouched but the error to show due to the global setting
+ expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
+ expect(containerEl.querySelectorAll('md-error').length).toBe(1, 'Expected an error message');
+ });
+
+ it('should display an error message when using showOnDirtyErrorStateMatcher', async(() => {
+ TestBed.resetTestingModule();
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ MdInputModule,
+ NoopAnimationsModule,
+ ReactiveFormsModule,
+ ],
+ declarations: [
+ MdInputContainerWithFormErrorMessages
+ ],
+ providers: [
+ {
+ provide: MD_ERROR_GLOBAL_OPTIONS,
+ useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }
+ }
+ ]
+ });
+
+ let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
+ fixture.detectChanges();
+
+ let containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
+ let testComponent = fixture.componentInstance;
+
+ expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
+ expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
+
+ testComponent.formControl.markAsTouched();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('md-error').length)
+ .toBe(0, 'Expected no error messages when touched');
+
+ testComponent.formControl.markAsDirty();
+ fixture.detectChanges();
+
+ expect(containerEl.querySelectorAll('md-error').length)
+ .toBe(1, 'Expected one error message when dirty');
+ }));
+ });
+
it('should not have prefix and suffix elements when none are specified', () => {
let fixture = TestBed.createComponent(MdInputContainerWithId);
fixture.detectChanges();
@@ -1018,6 +1127,31 @@ class MdInputContainerWithFormErrorMessages {
renderError = true;
}
+@Component({
+ template: `
+
+ `
+})
+class MdInputContainerWithCustomErrorStateMatcher {
+ formGroup = new FormGroup({
+ name: new FormControl('', Validators.required)
+ });
+
+ errorState = false;
+
+ customErrorStateMatcher(): boolean {
+ return this.errorState;
+ }
+}
+
@Component({
template: `