Skip to content

Commit f22a77a

Browse files
committed
feat(input): Add custom error state matcher
1 parent 945aa43 commit f22a77a

File tree

5 files changed

+107
-1
lines changed

5 files changed

+107
-1
lines changed

src/demo-app/input/input-demo.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ <h4>Inside a form</h4>
9494

9595
<button color="primary" md-raised-button>Submit</button>
9696
</form>
97+
98+
<h4>With a custom error function</h4>
99+
<md-input-container>
100+
<input mdInput
101+
placeholder="example"
102+
[(ngModel)]="errorMessageExample4"
103+
[errorStateMatcher]="customErrorStateMatcher"
104+
required>
105+
<md-error>This field is required</md-error>
106+
</md-input-container>
107+
97108
</md-card-content>
98109
</md-card>
99110

src/demo-app/input/input-demo.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component} from '@angular/core';
2-
import {FormControl, Validators} from '@angular/forms';
2+
import {FormControl, Validators, NgControl} from '@angular/forms';
33

44

55
let max = 5;
@@ -23,6 +23,7 @@ export class InputDemo {
2323
errorMessageExample1: string;
2424
errorMessageExample2: string;
2525
errorMessageExample3: string;
26+
errorMessageExample4: string;
2627
dividerColorExample1: string;
2728
dividerColorExample2: string;
2829
dividerColorExample3: string;
@@ -43,4 +44,11 @@ export class InputDemo {
4344
this.items.push({ value: ++max });
4445
}
4546
}
47+
48+
customErrorStateMatcher(c: NgControl): boolean {
49+
const isDirty = c.dirty;
50+
const isInvalid = c.invalid;
51+
52+
return isDirty && isInvalid;
53+
}
4654
}

src/lib/input/input-container.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
FormGroupDirective,
77
FormsModule,
88
NgForm,
9+
NgControl,
910
ReactiveFormsModule,
1011
Validators
1112
} from '@angular/forms';
@@ -56,6 +57,7 @@ describe('MdInputContainer', function () {
5657
MdInputContainerWithDynamicPlaceholder,
5758
MdInputContainerWithFormControl,
5859
MdInputContainerWithFormErrorMessages,
60+
MdInputContainerWithCustomErrorStateMatcher,
5961
MdInputContainerWithFormGroupErrorMessages,
6062
MdInputContainerWithId,
6163
MdInputContainerWithPrefixAndSuffix,
@@ -682,6 +684,36 @@ describe('MdInputContainer', function () {
682684
});
683685
}));
684686

687+
it('should display an error message when a custom error matcher returns true', async(() => {
688+
fixture.destroy();
689+
690+
let customFixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
691+
let component: MdInputContainerWithCustomErrorStateMatcher;
692+
693+
customFixture.detectChanges();
694+
component = customFixture.componentInstance;
695+
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
696+
697+
expect(component.formControl.invalid).toBe(true, 'Expected form control to be invalid');
698+
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
699+
700+
component.formControl.markAsTouched();
701+
customFixture.detectChanges();
702+
703+
customFixture.whenStable().then(() => {
704+
expect(containerEl.querySelectorAll('md-error').length)
705+
.toBe(0, 'Expected no error messages after being touched.');
706+
707+
component.errorState = true;
708+
customFixture.detectChanges();
709+
710+
customFixture.whenStable().then(() => {
711+
expect(containerEl.querySelectorAll('md-error').length)
712+
.toBe(1, 'Expected one error messages to have been rendered.');
713+
});
714+
});
715+
}));
716+
685717
it('should hide the errors and show the hints once the input becomes valid', async(() => {
686718
testComponent.formControl.markAsTouched();
687719
fixture.detectChanges();
@@ -995,6 +1027,29 @@ class MdInputContainerWithFormErrorMessages {
9951027
renderError = true;
9961028
}
9971029

1030+
@Component({
1031+
template: `
1032+
<form #form="ngForm" novalidate>
1033+
<md-input-container>
1034+
<input mdInput
1035+
[formControl]="formControl"
1036+
[errorStateMatcher]="customErrorStateMatcher.bind(this)">
1037+
<md-hint>Please type something</md-hint>
1038+
<md-error>This field is required</md-error>
1039+
</md-input-container>
1040+
</form>
1041+
`
1042+
})
1043+
class MdInputContainerWithCustomErrorStateMatcher {
1044+
@ViewChild('form') form: NgForm;
1045+
formControl = new FormControl('', Validators.required);
1046+
errorState = false;
1047+
1048+
customErrorStateMatcher(c: NgControl): boolean {
1049+
return this.errorState;
1050+
}
1051+
}
1052+
9981053
@Component({
9991054
template: `
10001055
<form [formGroup]="formGroup" novalidate>

src/lib/input/input-container.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ export class MdInputDirective {
179179
}
180180
}
181181

182+
/** A function used to control when error messages are shown. */
183+
@Input() errorStateMatcher: (control: NgControl) => boolean;
184+
182185
/** The input element's value. */
183186
get value() { return this._elementRef.nativeElement.value; }
184187
set value(value: string) { this._elementRef.nativeElement.value = value; }
@@ -239,6 +242,13 @@ export class MdInputDirective {
239242
/** Whether the input is in an error state. */
240243
_isErrorState(): boolean {
241244
const control = this._ngControl;
245+
return this.errorStateMatcher
246+
? this.errorStateMatcher(control)
247+
: this._defaultErrorStateMatcher(control);
248+
}
249+
250+
/** Default error state calculation */
251+
private _defaultErrorStateMatcher(control: NgControl): boolean {
242252
const isInvalid = control && control.invalid;
243253
const isTouched = control && control.touched;
244254
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||

src/lib/input/input.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,25 @@ The underline (line under the `input` content) color can be changed by using the
8585
attribute of `md-input-container`. A value of `primary` is the default and will correspond to the
8686
theme primary color. Alternatively, `accent` or `warn` can be specified to use the theme's accent or
8787
warn color.
88+
89+
### Custom Error Matcher
90+
91+
By default, error messages are shown when the control is invalid and the user has interacted with
92+
(touched) the element or the parent form has been submitted. If you wish to customize this
93+
behavior (e.g. to show the error as soon as the invalid control is dirty), you can use the
94+
`errorStateMatcher` property of the `mdInput`. To use this property, create a function in
95+
your component class that accepts an `NgControl` and returns a boolean. A result of `true` will
96+
display the error messages.
97+
98+
```html
99+
<md-input-container>
100+
<input mdInput [(ngModel)]="myInput" required [errorStateMatcher]="myErrorStateMatcher">
101+
<md-error>This field is required</md-error>
102+
</md-input-container>
103+
```
104+
105+
```ts
106+
function myErrorStateMatcher(control: NgControl): boolean {
107+
return control.invalid && control.dirty;
108+
}
109+
```

0 commit comments

Comments
 (0)