Skip to content

Commit d06612a

Browse files
committed
Add ability to globally configure errorStateMatcher
- Also adds shortcut functionality to show the error when dirty (instead of touched) - Also passes parent form group and parent form to errorStateMatcher
1 parent c34f350 commit d06612a

File tree

5 files changed

+143
-8
lines changed

5 files changed

+143
-8
lines changed

src/lib/core/core.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ export {
110110
MD_PLACEHOLDER_GLOBAL_OPTIONS
111111
} from './placeholder/placeholder-options';
112112

113+
// Error
114+
export {
115+
ErrorStateMatcherType,
116+
ErrorOptions,
117+
MD_ERROR_GLOBAL_OPTIONS
118+
} from './error/error-options';
119+
113120
@NgModule({
114121
imports: [
115122
MdLineModule,

src/lib/core/error/error-options.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {InjectionToken} from '@angular/core';
2+
import {NgControl, FormGroupDirective, NgForm} from '@angular/forms';
3+
4+
/** Injection token that can be used to specify the global error options. */
5+
export const MD_ERROR_GLOBAL_OPTIONS =
6+
new InjectionToken<() => boolean>('md-error-global-options');
7+
8+
export type ErrorStateMatcherType =
9+
(control: NgControl, parentFormGroup: FormGroupDirective, parentForm: NgForm) => boolean;
10+
11+
export interface ErrorOptions {
12+
errorStateMatcher?: ErrorStateMatcherType;
13+
showOnDirty?: boolean;
14+
}

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

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getMdInputContainerPlaceholderConflictError
2525
} from './input-container-errors';
2626
import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options';
27+
import {MD_ERROR_GLOBAL_OPTIONS} from '../core/error/error-options';
2728

2829

2930
describe('MdInputContainer', function () {
@@ -738,6 +739,86 @@ describe('MdInputContainer', function () {
738739
});
739740
}));
740741

742+
it('should display an error message when global error matcher returns true', () => {
743+
744+
// Global error state matcher that will always cause errors to show
745+
function globalErrorStateMatcher() {
746+
return true;
747+
}
748+
749+
TestBed.resetTestingModule();
750+
TestBed.configureTestingModule({
751+
imports: [
752+
FormsModule,
753+
MdInputModule,
754+
NoopAnimationsModule,
755+
ReactiveFormsModule,
756+
],
757+
declarations: [
758+
MdInputContainerWithFormErrorMessages
759+
],
760+
providers: [
761+
{
762+
provide: MD_ERROR_GLOBAL_OPTIONS,
763+
useValue: { errorStateMatcher: globalErrorStateMatcher } }
764+
]
765+
});
766+
767+
let customFixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
768+
customFixture.detectChanges();
769+
770+
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
771+
testComponent = customFixture.componentInstance;
772+
773+
// Expect the control to still be untouched but the error to show due to the global setting
774+
expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
775+
expect(containerEl.querySelectorAll('md-error').length).toBe(1, 'Expected an error message');
776+
});
777+
778+
it('should display an error message when global setting shows errors on dirty', async() => {
779+
TestBed.resetTestingModule();
780+
TestBed.configureTestingModule({
781+
imports: [
782+
FormsModule,
783+
MdInputModule,
784+
NoopAnimationsModule,
785+
ReactiveFormsModule,
786+
],
787+
declarations: [
788+
MdInputContainerWithFormErrorMessages
789+
],
790+
providers: [
791+
{ provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { showOnDirty: true } }
792+
]
793+
});
794+
795+
let customFixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
796+
customFixture.detectChanges();
797+
798+
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
799+
testComponent = customFixture.componentInstance;
800+
801+
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
802+
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
803+
804+
testComponent.formControl.markAsTouched();
805+
customFixture.detectChanges();
806+
807+
customFixture.whenStable().then(() => {
808+
expect(containerEl.querySelectorAll('md-error').length)
809+
.toBe(0, 'Expected no error messages when touched');
810+
811+
testComponent.formControl.markAsDirty();
812+
customFixture.detectChanges();
813+
814+
customFixture.whenStable().then(() => {
815+
expect(containerEl.querySelectorAll('md-error').length)
816+
.toBe(1, 'Expected one error message when dirty');
817+
});
818+
});
819+
820+
});
821+
741822
it('should hide the errors and show the hints once the input becomes valid', async(() => {
742823
testComponent.formControl.markAsTouched();
743824
fixture.detectChanges();
@@ -1069,7 +1150,7 @@ class MdInputContainerWithCustomErrorStateMatcher {
10691150
formControl = new FormControl('', Validators.required);
10701151
errorState = false;
10711152

1072-
customErrorStateMatcher(c: NgControl): boolean {
1153+
customErrorStateMatcher(): boolean {
10731154
return this.errorState;
10741155
}
10751156
}

src/lib/input/input-container.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ import {
3434
PlaceholderOptions,
3535
MD_PLACEHOLDER_GLOBAL_OPTIONS
3636
} from '../core/placeholder/placeholder-options';
37+
import {
38+
ErrorStateMatcherType,
39+
ErrorOptions,
40+
MD_ERROR_GLOBAL_OPTIONS
41+
} from '../core/error/error-options';
3742

3843
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
3944
const MD_INPUT_INVALID_TYPES = [
@@ -129,6 +134,7 @@ export class MdInputDirective {
129134
private _required = false;
130135
private _id: string;
131136
private _cachedUid: string;
137+
private _errorOptions: ErrorOptions;
132138

133139
/** Whether the element is focused or not. */
134140
focused = false;
@@ -182,7 +188,7 @@ export class MdInputDirective {
182188
}
183189

184190
/** A function used to control when error messages are shown. */
185-
@Input() errorStateMatcher: (control: NgControl) => boolean;
191+
@Input() errorStateMatcher: ErrorStateMatcherType;
186192

187193
/** The input element's value. */
188194
get value() { return this._elementRef.nativeElement.value; }
@@ -218,10 +224,14 @@ export class MdInputDirective {
218224
private _renderer: Renderer2,
219225
@Optional() @Self() public _ngControl: NgControl,
220226
@Optional() private _parentForm: NgForm,
221-
@Optional() private _parentFormGroup: FormGroupDirective) {
227+
@Optional() private _parentFormGroup: FormGroupDirective,
228+
@Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
222229

223230
// Force setter to be called in case id was not specified.
224231
this.id = this.id;
232+
233+
this._errorOptions = errorOptions ? errorOptions : {};
234+
this.errorStateMatcher = this._errorOptions.errorStateMatcher || undefined;
225235
}
226236

227237
/** Focuses the input element. */
@@ -245,17 +255,22 @@ export class MdInputDirective {
245255
_isErrorState(): boolean {
246256
const control = this._ngControl;
247257
return this.errorStateMatcher
248-
? this.errorStateMatcher(control)
258+
? this.errorStateMatcher(control, this._parentFormGroup, this._parentForm)
249259
: this._defaultErrorStateMatcher(control);
250260
}
251261

252262
/** Default error state calculation */
253263
private _defaultErrorStateMatcher(control: NgControl): boolean {
254264
const isInvalid = control && control.invalid;
255265
const isTouched = control && control.touched;
266+
const isDirty = control && control.dirty;
256267
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
257268
(this._parentForm && this._parentForm.submitted);
258269

270+
if (this._errorOptions.showOnDirty) {
271+
return !!(isInvalid && (isDirty || isSubmitted));
272+
}
273+
259274
return !!(isInvalid && (isTouched || isSubmitted));
260275
}
261276

src/lib/input/input.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ By default, error messages are shown when the control is invalid and the user ha
108108
(touched) the element or the parent form has been submitted. If you wish to customize this
109109
behavior (e.g. to show the error as soon as the invalid control is dirty), you can use the
110110
`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.
111+
your component class that returns a boolean. A result of `true` will display the error messages.
113112

114113
```html
115114
<md-input-container>
@@ -119,7 +118,26 @@ display the error messages.
119118
```
120119

121120
```ts
122-
function myErrorStateMatcher(control: NgControl): boolean {
121+
function myErrorStateMatcher(control: NgControl, parentFg: FormGroupDirective, parentForm: NgForm): boolean {
123122
return control.invalid && control.dirty;
124123
}
125-
```
124+
```
125+
126+
A global error state matcher can be specified by setting the `MD_ERROR_GLOBAL_OPTIONS` provider. This applies
127+
to all inputs.
128+
129+
```ts
130+
@NgModule({
131+
providers: [
132+
{provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { errorStateMatcher: myErrorStateMatcher }}
133+
]
134+
})
135+
```
136+
137+
Here are the available global options:
138+
139+
140+
| Name | Type | Description |
141+
| ----------------- | -------- | ----------- |
142+
| errorStateMatcher | Function | Returns a boolean specifying if the error should be shown |
143+
| showOnDirty | boolean | If true, the error will show when the control is dirty, not touched. |P

0 commit comments

Comments
 (0)