-
Notifications
You must be signed in to change notification settings - Fork 6.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(input): add custom error state matcher #4750
Changes from 4 commits
3ff8d87
b0d69cc
ab870d1
9bc13bb
6f92cdb
ea477cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/** | ||
* @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; | ||
} | ||
|
||
export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) { | ||
const isInvalid = control.invalid; | ||
const isTouched = control.touched; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: these const assignments seem to be adding bloat, given how short const isSubmitted = form && form.submitted;
return !!(control.invalid && (control.touched || isSubmitted)); |
||
const isSubmitted = form && form.submitted; | ||
|
||
return !!(isInvalid && (isTouched || isSubmitted)); | ||
} | ||
|
||
export function showOnDirtyErrorStateMatcher(control: FormControl, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docs |
||
form: FormGroupDirective | NgForm) { | ||
const isInvalid = control.invalid; | ||
const isDirty = control.dirty; | ||
const isSubmitted = form && form.submitted; | ||
|
||
return !!(isInvalid && (isDirty || isSubmitted)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,121 @@ describe('MdInputContainer', function () { | |
|
||
}); | ||
|
||
describe('custom error behavior', () => { | ||
it('should display an error message when a custom error matcher returns true', async(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that we're just using reactive form directives now, these tests don't need to be async. You should be able to safely remove the |
||
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(); | ||
|
||
fixture.whenStable().then(() => { | ||
expect(containerEl.querySelectorAll('md-error').length) | ||
.toBe(0, 'Expected no error messages after being touched.'); | ||
|
||
component.errorState = true; | ||
fixture.detectChanges(); | ||
|
||
fixture.whenStable().then(() => { | ||
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(); | ||
|
||
fixture.whenStable().then(() => { | ||
expect(containerEl.querySelectorAll('md-error').length) | ||
.toBe(0, 'Expected no error messages when touched'); | ||
|
||
testComponent.formControl.markAsDirty(); | ||
fixture.detectChanges(); | ||
|
||
fixture.whenStable().then(() => { | ||
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 +1135,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> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs