mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(input): add custom error state matcher (#4750)
* feat(input): Add custom error state matcher * Address comments * Address comments pt. 2 * Use FormControl and only one of incompatible form options * Remove unnecesary async tests and const declarations * Add jsdoc comments to error state matchers
This commit is contained in:
@@ -94,6 +94,17 @@
|
||||
|
||||
<button color="primary" md-raised-button>Submit</button>
|
||||
</form>
|
||||
|
||||
<h4>With a custom error function</h4>
|
||||
<md-input-container>
|
||||
<input mdInput
|
||||
placeholder="example"
|
||||
[(ngModel)]="errorMessageExample4"
|
||||
[errorStateMatcher]="customErrorStateMatcher"
|
||||
required>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export class InputDemo {
|
||||
errorMessageExample1: string;
|
||||
errorMessageExample2: string;
|
||||
errorMessageExample3: string;
|
||||
errorMessageExample4: string;
|
||||
dividerColorExample1: string;
|
||||
dividerColorExample2: string;
|
||||
dividerColorExample3: string;
|
||||
@@ -44,4 +45,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,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,
|
||||
|
||||
@@ -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<ErrorOptions>('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));
|
||||
}
|
||||
@@ -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: `
|
||||
<form [formGroup]="formGroup">
|
||||
<md-input-container>
|
||||
<input mdInput
|
||||
formControlName="name"
|
||||
[errorStateMatcher]="customErrorStateMatcher.bind(this)">
|
||||
<md-hint>Please type something</md-hint>
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
class MdInputContainerWithCustomErrorStateMatcher {
|
||||
formGroup = new FormGroup({
|
||||
name: new FormControl('', Validators.required)
|
||||
});
|
||||
|
||||
errorState = false;
|
||||
|
||||
customErrorStateMatcher(): boolean {
|
||||
return this.errorState;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form [formGroup]="formGroup" novalidate>
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {coerceBooleanProperty, Platform} from '../core';
|
||||
import {FormGroupDirective, NgControl, NgForm} from '@angular/forms';
|
||||
import {FormGroupDirective, NgControl, NgForm, FormControl} from '@angular/forms';
|
||||
import {getSupportedInputTypes} from '../core/platform/features';
|
||||
import {
|
||||
getMdInputContainerDuplicatedHintError,
|
||||
@@ -42,6 +42,12 @@ import {
|
||||
PlaceholderOptions,
|
||||
MD_PLACEHOLDER_GLOBAL_OPTIONS
|
||||
} from '../core/placeholder/placeholder-options';
|
||||
import {
|
||||
defaultErrorStateMatcher,
|
||||
ErrorStateMatcher,
|
||||
ErrorOptions,
|
||||
MD_ERROR_GLOBAL_OPTIONS
|
||||
} from '../core/error/error-options';
|
||||
|
||||
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
|
||||
const MD_INPUT_INVALID_TYPES = [
|
||||
@@ -137,6 +143,7 @@ export class MdInputDirective {
|
||||
private _required = false;
|
||||
private _id: string;
|
||||
private _cachedUid: string;
|
||||
private _errorOptions: ErrorOptions;
|
||||
|
||||
/** Whether the element is focused or not. */
|
||||
focused = false;
|
||||
@@ -189,6 +196,9 @@ export class MdInputDirective {
|
||||
}
|
||||
}
|
||||
|
||||
/** A function used to control when error messages are shown. */
|
||||
@Input() errorStateMatcher: ErrorStateMatcher;
|
||||
|
||||
/** The input element's value. */
|
||||
get value() { return this._elementRef.nativeElement.value; }
|
||||
set value(value: string) { this._elementRef.nativeElement.value = value; }
|
||||
@@ -224,10 +234,14 @@ export class MdInputDirective {
|
||||
private _platform: Platform,
|
||||
@Optional() @Self() public _ngControl: NgControl,
|
||||
@Optional() private _parentForm: NgForm,
|
||||
@Optional() private _parentFormGroup: FormGroupDirective) {
|
||||
@Optional() private _parentFormGroup: FormGroupDirective,
|
||||
@Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
|
||||
|
||||
// Force setter to be called in case id was not specified.
|
||||
this.id = this.id;
|
||||
|
||||
this._errorOptions = errorOptions ? errorOptions : {};
|
||||
this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
|
||||
}
|
||||
|
||||
/** Focuses the input element. */
|
||||
@@ -250,12 +264,8 @@ export class MdInputDirective {
|
||||
/** Whether the input is in an error state. */
|
||||
_isErrorState(): boolean {
|
||||
const control = this._ngControl;
|
||||
const isInvalid = control && control.invalid;
|
||||
const isTouched = control && control.touched;
|
||||
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
|
||||
(this._parentForm && this._parentForm.submitted);
|
||||
|
||||
return !!(isInvalid && (isTouched || isSubmitted));
|
||||
const form = this._parentFormGroup || this._parentForm;
|
||||
return control && this.errorStateMatcher(control.control as FormControl, form);
|
||||
}
|
||||
|
||||
/** Make sure the input is a supported type. */
|
||||
|
||||
@@ -107,3 +107,45 @@ The underline (line under the `input` content) color can be changed by using the
|
||||
attribute of `md-input-container`. A value of `primary` is the default and will correspond to the
|
||||
theme primary color. Alternatively, `accent` or `warn` can be specified to use the theme's accent or
|
||||
warn color.
|
||||
|
||||
### Custom Error Matcher
|
||||
|
||||
By default, error messages are shown when the control is invalid and either the user has interacted with
|
||||
(touched) the element or the parent form has been submitted. If you wish to override this
|
||||
behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group
|
||||
is invalid), you can use the `errorStateMatcher` property of the `mdInput`. To use this property,
|
||||
create a function in your component class that returns a boolean. A result of `true` will display
|
||||
the error messages.
|
||||
|
||||
```html
|
||||
<md-input-container>
|
||||
<input mdInput [(ngModel)]="myInput" required [errorStateMatcher]="myErrorStateMatcher">
|
||||
<md-error>This field is required</md-error>
|
||||
</md-input-container>
|
||||
```
|
||||
|
||||
```ts
|
||||
function myErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm): boolean {
|
||||
// Error when invalid control is dirty, touched, or submitted
|
||||
const isSubmitted = form && form.submitted;
|
||||
return !!(control.invalid && (control.dirty || control.touched || isSubmitted)));
|
||||
}
|
||||
```
|
||||
|
||||
A global error state matcher can be specified by setting the `MD_ERROR_GLOBAL_OPTIONS` provider. This applies
|
||||
to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally cause
|
||||
input errors to show when the input is dirty and invalid.
|
||||
|
||||
```ts
|
||||
@NgModule({
|
||||
providers: [
|
||||
{provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }}
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
Here are the available global options:
|
||||
|
||||
| Name | Type | Description |
|
||||
| ----------------- | -------- | ----------- |
|
||||
| errorStateMatcher | Function | Returns a boolean specifying if the error should be shown |
|
||||
|
||||
Reference in New Issue
Block a user