Skip to content

Commit a774688

Browse files
crisbetokara
authored andcommitted
feat(select): add support for custom error state matcher (#7443)
Fixes #7419.
1 parent b5310da commit a774688

File tree

11 files changed

+143
-114
lines changed

11 files changed

+143
-114
lines changed

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { Component, ChangeDetectionStrategy } from '@angular/core';
2-
import {FormControl, Validators} from '@angular/forms';
1+
import {Component, ChangeDetectionStrategy} from '@angular/core';
2+
import {FormControl, NgControl, Validators} from '@angular/forms';
3+
import {ErrorStateMatcher} from '@angular/material';
34

45

56
let max = 5;
@@ -52,10 +53,16 @@ export class InputDemo {
5253
}
5354
}
5455

55-
customErrorStateMatcher(c: FormControl): boolean {
56-
const hasInteraction = c.dirty || c.touched;
57-
const isInvalid = c.invalid;
56+
customErrorStateMatcher: ErrorStateMatcher = {
57+
isErrorState: (control: NgControl | null) => {
58+
if (control) {
59+
const hasInteraction = control.dirty || control.touched;
60+
const isInvalid = control.invalid;
5861

59-
return !!(hasInteraction && isInvalid);
60-
}
62+
return !!(hasInteraction && isInvalid);
63+
}
64+
65+
return false;
66+
}
67+
};
6168
}

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

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,21 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {InjectionToken} from '@angular/core';
10-
import {FormControl, FormGroupDirective, NgForm} from '@angular/forms';
9+
import {Injectable} from '@angular/core';
10+
import {FormGroupDirective, NgForm, NgControl} from '@angular/forms';
1111

12-
/** Injection token that can be used to specify the global error options. */
13-
export const MAT_ERROR_GLOBAL_OPTIONS =
14-
new InjectionToken<ErrorOptions>('mat-error-global-options');
15-
16-
export type ErrorStateMatcher =
17-
(control: FormControl, form: FormGroupDirective | NgForm) => boolean;
18-
19-
export interface ErrorOptions {
20-
errorStateMatcher?: ErrorStateMatcher;
21-
}
22-
23-
/** Returns whether control is invalid and is either touched or is a part of a submitted form. */
24-
export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) {
25-
const isSubmitted = form && form.submitted;
26-
return !!(control.invalid && (control.touched || isSubmitted));
12+
/** Error state matcher that matches when a control is invalid and dirty. */
13+
@Injectable()
14+
export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher {
15+
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
16+
return !!(control && control.invalid && (control.dirty || (form && form.submitted)));
17+
}
2718
}
2819

29-
/** Returns whether control is invalid and is either dirty or is a part of a submitted form. */
30-
export function showOnDirtyErrorStateMatcher(control: FormControl,
31-
form: FormGroupDirective | NgForm) {
32-
const isSubmitted = form && form.submitted;
33-
return !!(control.invalid && (control.dirty || isSubmitted));
20+
/** Provider that defines how form controls behave with regards to displaying error messages. */
21+
@Injectable()
22+
export class ErrorStateMatcher {
23+
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
24+
return !!(control && control.invalid && (control.touched || (form && form.submitted)));
25+
}
3426
}

src/lib/input/input-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {NgModule} from '@angular/core';
1212
import {MatFormFieldModule} from '@angular/material/form-field';
1313
import {MatTextareaAutosize} from './autosize';
1414
import {MatInput} from './input';
15+
import {ErrorStateMatcher} from '@angular/material/core';
1516

1617

1718
@NgModule({
@@ -31,5 +32,6 @@ import {MatInput} from './input';
3132
MatInput,
3233
MatTextareaAutosize,
3334
],
35+
providers: [ErrorStateMatcher],
3436
})
3537
export class MatInputModule {}

src/lib/input/input.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,12 @@ warn color.
111111

112112
### Custom Error Matcher
113113

114-
By default, error messages are shown when the control is invalid and either the user has interacted with
115-
(touched) the element or the parent form has been submitted. If you wish to override this
114+
By default, error messages are shown when the control is invalid and either the user has interacted
115+
with (touched) the element or the parent form has been submitted. If you wish to override this
116116
behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group
117117
is invalid), you can use the `errorStateMatcher` property of the `matInput`. To use this property,
118-
create a function in your component class that returns a boolean. A result of `true` will display
119-
the error messages.
118+
create an `ErrorStateMatcher` object in your component class that has a `isErrorState` function which
119+
returns a boolean. A result of `true` will display the error messages.
120120

121121
```html
122122
<mat-form-field>
@@ -126,25 +126,26 @@ the error messages.
126126
```
127127

128128
```ts
129-
function myErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm): boolean {
130-
// Error when invalid control is dirty, touched, or submitted
131-
const isSubmitted = form && form.submitted;
132-
return !!(control.invalid && (control.dirty || control.touched || isSubmitted));
129+
class MyErrorStateMatcher implements ErrorStateMatcher {
130+
isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean {
131+
// Error when invalid control is dirty, touched, or submitted
132+
const isSubmitted = form && form.submitted;
133+
return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted)));
134+
}
133135
}
134136
```
135137

136-
A global error state matcher can be specified by setting the `MAT_ERROR_GLOBAL_OPTIONS` provider. This applies
137-
to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally set
138-
input errors to show when the input is dirty and invalid.
138+
A global error state matcher can be specified by setting the `ErrorStateMatcher` provider. This
139+
applies to all inputs. For convenience, `ShowOnDirtyErrorStateMatcher` is available in order to
140+
globally cause input errors to show when the input is dirty and invalid.
139141

140142
```ts
141143
@NgModule({
142144
providers: [
143-
{provide: MAT_ERROR_GLOBAL_OPTIONS, useValue: {errorStateMatcher: showOnDirtyErrorStateMatcher}}
145+
{provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher}
144146
]
145147
})
146148
```
147-
148149
Here are the available global options:
149150

150151
| Name | Type | Description |

src/lib/input/input.spec.ts

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {
1212
Validators,
1313
} from '@angular/forms';
1414
import {
15-
MAT_ERROR_GLOBAL_OPTIONS,
1615
MAT_PLACEHOLDER_GLOBAL_OPTIONS,
17-
showOnDirtyErrorStateMatcher,
16+
ShowOnDirtyErrorStateMatcher,
17+
ErrorStateMatcher,
1818
} from '@angular/material/core';
1919
import {
2020
getMatFormFieldDuplicatedHintError,
@@ -926,12 +926,6 @@ describe('MatInput with forms', () => {
926926
});
927927

928928
it('should display an error message when global error matcher returns true', () => {
929-
930-
// Global error state matcher that will always cause errors to show
931-
function globalErrorStateMatcher() {
932-
return true;
933-
}
934-
935929
TestBed.resetTestingModule();
936930
TestBed.configureTestingModule({
937931
imports: [
@@ -944,11 +938,7 @@ describe('MatInput with forms', () => {
944938
declarations: [
945939
MatInputWithFormErrorMessages
946940
],
947-
providers: [
948-
{
949-
provide: MAT_ERROR_GLOBAL_OPTIONS,
950-
useValue: { errorStateMatcher: globalErrorStateMatcher } }
951-
]
941+
providers: [{provide: ErrorStateMatcher, useValue: {isErrorState: () => true}}]
952942
});
953943

954944
let fixture = TestBed.createComponent(MatInputWithFormErrorMessages);
@@ -963,7 +953,7 @@ describe('MatInput with forms', () => {
963953
expect(containerEl.querySelectorAll('mat-error').length).toBe(1, 'Expected an error message');
964954
});
965955

966-
it('should display an error message when using showOnDirtyErrorStateMatcher', async(() => {
956+
it('should display an error message when using ShowOnDirtyErrorStateMatcher', async(() => {
967957
TestBed.resetTestingModule();
968958
TestBed.configureTestingModule({
969959
imports: [
@@ -976,12 +966,7 @@ describe('MatInput with forms', () => {
976966
declarations: [
977967
MatInputWithFormErrorMessages
978968
],
979-
providers: [
980-
{
981-
provide: MAT_ERROR_GLOBAL_OPTIONS,
982-
useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }
983-
}
984-
]
969+
providers: [{provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher}]
985970
});
986971

987972
let fixture = TestBed.createComponent(MatInputWithFormErrorMessages);
@@ -1298,7 +1283,7 @@ class MatInputWithFormErrorMessages {
12981283
<mat-form-field>
12991284
<input matInput
13001285
formControlName="name"
1301-
[errorStateMatcher]="customErrorStateMatcher.bind(this)">
1286+
[errorStateMatcher]="customErrorStateMatcher">
13021287
<mat-hint>Please type something</mat-hint>
13031288
<mat-error>This field is required</mat-error>
13041289
</mat-form-field>
@@ -1312,9 +1297,9 @@ class MatInputWithCustomErrorStateMatcher {
13121297

13131298
errorState = false;
13141299

1315-
customErrorStateMatcher(): boolean {
1316-
return this.errorState;
1317-
}
1300+
customErrorStateMatcher = {
1301+
isErrorState: () => this.errorState
1302+
};
13181303
}
13191304

13201305
@Component({

src/lib/input/input.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
Directive,
1111
DoCheck,
1212
ElementRef,
13-
Inject,
1413
Input,
1514
OnChanges,
1615
OnDestroy,
@@ -19,15 +18,10 @@ import {
1918
Self,
2019
} from '@angular/core';
2120
import {coerceBooleanProperty} from '@angular/cdk/coercion';
22-
import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
21+
import {FormGroupDirective, NgControl, NgForm} from '@angular/forms';
2322
import {Platform, getSupportedInputTypes} from '@angular/cdk/platform';
2423
import {getMatInputUnsupportedTypeError} from './input-errors';
25-
import {
26-
defaultErrorStateMatcher,
27-
ErrorOptions,
28-
ErrorStateMatcher,
29-
MAT_ERROR_GLOBAL_OPTIONS
30-
} from '@angular/material/core';
24+
import {ErrorStateMatcher} from '@angular/material/core';
3125
import {Subject} from 'rxjs/Subject';
3226
import {MatFormFieldControl} from '@angular/material/form-field';
3327

@@ -75,7 +69,6 @@ export class MatInput implements MatFormFieldControl<any>, OnChanges, OnDestroy,
7569
protected _required = false;
7670
protected _id: string;
7771
protected _uid = `mat-input-${nextUniqueId++}`;
78-
protected _errorOptions: ErrorOptions;
7972
protected _previousNativeValue = this.value;
8073
private _readonly = false;
8174

@@ -130,7 +123,7 @@ export class MatInput implements MatFormFieldControl<any>, OnChanges, OnDestroy,
130123
}
131124
}
132125

133-
/** A function used to control when error messages are shown. */
126+
/** An object used to control when error messages are shown. */
134127
@Input() errorStateMatcher: ErrorStateMatcher;
135128

136129
/** The input element's value. */
@@ -163,12 +156,10 @@ export class MatInput implements MatFormFieldControl<any>, OnChanges, OnDestroy,
163156
@Optional() @Self() public ngControl: NgControl,
164157
@Optional() protected _parentForm: NgForm,
165158
@Optional() protected _parentFormGroup: FormGroupDirective,
166-
@Optional() @Inject(MAT_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
159+
private _defaultErrorStateMatcher: ErrorStateMatcher) {
167160

168161
// Force setter to be called in case id was not specified.
169162
this.id = this.id;
170-
this._errorOptions = errorOptions ? errorOptions : {};
171-
this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
172163

173164
// On some versions of iOS the caret gets stuck in the wrong place when holding down the delete
174165
// key. In order to get around this we need to "jiggle" the caret loose. Since this bug only
@@ -231,9 +222,9 @@ export class MatInput implements MatFormFieldControl<any>, OnChanges, OnDestroy,
231222
/** Re-evaluates the error state. This is only relevant with @angular/forms. */
232223
protected _updateErrorState() {
233224
const oldState = this.errorState;
234-
const ngControl = this.ngControl;
235225
const parent = this._parentFormGroup || this._parentForm;
236-
const newState = ngControl && this.errorStateMatcher(ngControl.control as FormControl, parent);
226+
const matcher = this.errorStateMatcher || this._defaultErrorStateMatcher;
227+
const newState = matcher.isErrorState(this.ngControl, parent);
237228

238229
if (newState !== oldState) {
239230
this.errorState = newState;

src/lib/select/select-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
98
import {NgModule} from '@angular/core';
109
import {CommonModule} from '@angular/common';
1110
import {MatSelect, MatSelectTrigger, MAT_SELECT_SCROLL_STRATEGY_PROVIDER} from './select';
1211
import {MatCommonModule, MatOptionModule} from '@angular/material/core';
1312
import {OverlayModule} from '@angular/cdk/overlay';
1413
import {MatFormFieldModule} from '@angular/material/form-field';
14+
import {ErrorStateMatcher} from '@angular/material/core';
1515

1616

1717
@NgModule({
@@ -23,6 +23,6 @@ import {MatFormFieldModule} from '@angular/material/form-field';
2323
],
2424
exports: [MatFormFieldModule, MatSelect, MatSelectTrigger, MatOptionModule, MatCommonModule],
2525
declarations: [MatSelect, MatSelectTrigger],
26-
providers: [MAT_SELECT_SCROLL_STRATEGY_PROVIDER]
26+
providers: [MAT_SELECT_SCROLL_STRATEGY_PROVIDER, ErrorStateMatcher]
2727
})
2828
export class MatSelectModule {}

0 commit comments

Comments
 (0)