Skip to content

Commit c34f350

Browse files
committed
feat(input): Add custom error state matcher
1 parent f89c6db commit c34f350

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';
@@ -57,6 +58,7 @@ describe('MdInputContainer', function () {
5758
MdInputContainerWithDynamicPlaceholder,
5859
MdInputContainerWithFormControl,
5960
MdInputContainerWithFormErrorMessages,
61+
MdInputContainerWithCustomErrorStateMatcher,
6062
MdInputContainerWithFormGroupErrorMessages,
6163
MdInputContainerWithId,
6264
MdInputContainerWithPrefixAndSuffix,
@@ -706,6 +708,36 @@ describe('MdInputContainer', function () {
706708
});
707709
}));
708710

711+
it('should display an error message when a custom error matcher returns true', async(() => {
712+
fixture.destroy();
713+
714+
let customFixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
715+
let component: MdInputContainerWithCustomErrorStateMatcher;
716+
717+
customFixture.detectChanges();
718+
component = customFixture.componentInstance;
719+
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
720+
721+
expect(component.formControl.invalid).toBe(true, 'Expected form control to be invalid');
722+
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
723+
724+
component.formControl.markAsTouched();
725+
customFixture.detectChanges();
726+
727+
customFixture.whenStable().then(() => {
728+
expect(containerEl.querySelectorAll('md-error').length)
729+
.toBe(0, 'Expected no error messages after being touched.');
730+
731+
component.errorState = true;
732+
customFixture.detectChanges();
733+
734+
customFixture.whenStable().then(() => {
735+
expect(containerEl.querySelectorAll('md-error').length)
736+
.toBe(1, 'Expected one error messages to have been rendered.');
737+
});
738+
});
739+
}));
740+
709741
it('should hide the errors and show the hints once the input becomes valid', async(() => {
710742
testComponent.formControl.markAsTouched();
711743
fixture.detectChanges();
@@ -1019,6 +1051,29 @@ class MdInputContainerWithFormErrorMessages {
10191051
renderError = true;
10201052
}
10211053

1054+
@Component({
1055+
template: `
1056+
<form #form="ngForm" novalidate>
1057+
<md-input-container>
1058+
<input mdInput
1059+
[formControl]="formControl"
1060+
[errorStateMatcher]="customErrorStateMatcher.bind(this)">
1061+
<md-hint>Please type something</md-hint>
1062+
<md-error>This field is required</md-error>
1063+
</md-input-container>
1064+
</form>
1065+
`
1066+
})
1067+
class MdInputContainerWithCustomErrorStateMatcher {
1068+
@ViewChild('form') form: NgForm;
1069+
formControl = new FormControl('', Validators.required);
1070+
errorState = false;
1071+
1072+
customErrorStateMatcher(c: NgControl): boolean {
1073+
return this.errorState;
1074+
}
1075+
}
1076+
10221077
@Component({
10231078
template: `
10241079
<form [formGroup]="formGroup" novalidate>

src/lib/input/input-container.ts

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

184+
/** A function used to control when error messages are shown. */
185+
@Input() errorStateMatcher: (control: NgControl) => boolean;
186+
184187
/** The input element's value. */
185188
get value() { return this._elementRef.nativeElement.value; }
186189
set value(value: string) { this._elementRef.nativeElement.value = value; }
@@ -241,6 +244,13 @@ export class MdInputDirective {
241244
/** Whether the input is in an error state. */
242245
_isErrorState(): boolean {
243246
const control = this._ngControl;
247+
return this.errorStateMatcher
248+
? this.errorStateMatcher(control)
249+
: this._defaultErrorStateMatcher(control);
250+
}
251+
252+
/** Default error state calculation */
253+
private _defaultErrorStateMatcher(control: NgControl): boolean {
244254
const isInvalid = control && control.invalid;
245255
const isTouched = control && control.touched;
246256
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
@@ -101,3 +101,25 @@ The underline (line under the `input` content) color can be changed by using the
101101
attribute of `md-input-container`. A value of `primary` is the default and will correspond to the
102102
theme primary color. Alternatively, `accent` or `warn` can be specified to use the theme's accent or
103103
warn color.
104+
105+
### Custom Error Matcher
106+
107+
By default, error messages are shown when the control is invalid and the user has interacted with
108+
(touched) the element or the parent form has been submitted. If you wish to customize this
109+
behavior (e.g. to show the error as soon as the invalid control is dirty), you can use the
110+
`errorStateMatcher` property of the `mdInput`. To use this property, create a function in
111+
your component class that accepts an `NgControl` and returns a boolean. A result of `true` will
112+
display the error messages.
113+
114+
```html
115+
<md-input-container>
116+
<input mdInput [(ngModel)]="myInput" required [errorStateMatcher]="myErrorStateMatcher">
117+
<md-error>This field is required</md-error>
118+
</md-input-container>
119+
```
120+
121+
```ts
122+
function myErrorStateMatcher(control: NgControl): boolean {
123+
return control.invalid && control.dirty;
124+
}
125+
```

0 commit comments

Comments
 (0)