Skip to content

Commit b0d69cc

Browse files
committed
Address comments
1 parent 3ff8d87 commit b0d69cc

File tree

5 files changed

+123
-98
lines changed

5 files changed

+123
-98
lines changed

src/lib/core/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export {
120120

121121
// Error
122122
export {
123-
ErrorStateMatcherType,
123+
ErrorStateMatcher,
124124
ErrorOptions,
125125
MD_ERROR_GLOBAL_OPTIONS
126126
} from './error/error-options';

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

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
19
import {InjectionToken} from '@angular/core';
210
import {NgControl, FormGroupDirective, NgForm} from '@angular/forms';
311

412
/** Injection token that can be used to specify the global error options. */
513
export const MD_ERROR_GLOBAL_OPTIONS =
6-
new InjectionToken<() => boolean>('md-error-global-options');
14+
new InjectionToken<ErrorOptions>('md-error-global-options');
715

8-
export type ErrorStateMatcherType =
16+
export type ErrorStateMatcher =
917
(control: NgControl, parentFormGroup: FormGroupDirective, parentForm: NgForm) => boolean;
1018

1119
export interface ErrorOptions {
12-
errorStateMatcher?: ErrorStateMatcherType;
13-
showOnDirty?: boolean;
20+
errorStateMatcher?: ErrorStateMatcher;
21+
}
22+
23+
export class DefaultErrorStateMatcher {
24+
25+
errorStateMatcher(control: NgControl, formGroup: FormGroupDirective, form: NgForm): boolean {
26+
const isInvalid = control && control.invalid;
27+
const isTouched = control && control.touched;
28+
const isSubmitted = (formGroup && formGroup.submitted) ||
29+
(form && form.submitted);
30+
31+
return !!(isInvalid && (isTouched || isSubmitted));
32+
}
33+
}
34+
35+
export class ShowOnDirtyErrorStateMatcher {
36+
37+
errorStateMatcher(control: NgControl, formGroup: FormGroupDirective, form: NgForm): boolean {
38+
const isInvalid = control && control.invalid;
39+
const isDirty = control && control.dirty;
40+
const isSubmitted = (formGroup && formGroup.submitted) ||
41+
(form && form.submitted);
42+
43+
return !!(isInvalid && (isDirty || isSubmitted));
44+
}
1445
}

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

Lines changed: 70 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +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';
27+
import {MD_ERROR_GLOBAL_OPTIONS, ShowOnDirtyErrorStateMatcher} from '../core/error/error-options';
2828

2929
describe('MdInputContainer', function () {
3030
beforeEach(async(() => {
@@ -708,32 +708,74 @@ describe('MdInputContainer', function () {
708708
});
709709
}));
710710

711-
it('should display an error message when a custom error matcher returns true', async(() => {
712-
fixture.destroy();
711+
it('should hide the errors and show the hints once the input becomes valid', async(() => {
712+
testComponent.formControl.markAsTouched();
713+
fixture.detectChanges();
714+
715+
fixture.whenStable().then(() => {
716+
expect(containerEl.classList)
717+
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
718+
expect(containerEl.querySelectorAll('md-error').length)
719+
.toBe(1, 'Expected one error message to have been rendered.');
720+
expect(containerEl.querySelectorAll('md-hint').length)
721+
.toBe(0, 'Expected no hints to be shown.');
722+
723+
testComponent.formControl.setValue('something');
724+
fixture.detectChanges();
725+
726+
fixture.whenStable().then(() => {
727+
expect(containerEl.classList).not.toContain('mat-input-invalid',
728+
'Expected container not to have the invalid class when valid.');
729+
expect(containerEl.querySelectorAll('md-error').length)
730+
.toBe(0, 'Expected no error messages when the input is valid.');
731+
expect(containerEl.querySelectorAll('md-hint').length)
732+
.toBe(1, 'Expected one hint to be shown once the input is valid.');
733+
});
734+
});
735+
}));
736+
737+
it('should not hide the hint if there are no error messages', async(() => {
738+
testComponent.renderError = false;
739+
fixture.detectChanges();
740+
741+
expect(containerEl.querySelectorAll('md-hint').length)
742+
.toBe(1, 'Expected one hint to be shown on load.');
743+
744+
testComponent.formControl.markAsTouched();
745+
fixture.detectChanges();
746+
747+
fixture.whenStable().then(() => {
748+
expect(containerEl.querySelectorAll('md-hint').length)
749+
.toBe(1, 'Expected one hint to still be shown.');
750+
});
751+
}));
752+
753+
});
713754

714-
let customFixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
715-
let component: MdInputContainerWithCustomErrorStateMatcher;
755+
describe('custom error behavior', () => {
756+
it('should display an error message when a custom error matcher returns true', async(() => {
757+
let fixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
758+
fixture.detectChanges();
716759

717-
customFixture.detectChanges();
718-
component = customFixture.componentInstance;
719-
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
760+
let component = fixture.componentInstance;
761+
let containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
720762

721763
expect(component.formControl.invalid).toBe(true, 'Expected form control to be invalid');
722764
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
723765

724766
component.formControl.markAsTouched();
725-
customFixture.detectChanges();
767+
fixture.detectChanges();
726768

727-
customFixture.whenStable().then(() => {
769+
fixture.whenStable().then(() => {
728770
expect(containerEl.querySelectorAll('md-error').length)
729-
.toBe(0, 'Expected no error messages after being touched.');
771+
.toBe(0, 'Expected no error messages after being touched.');
730772

731773
component.errorState = true;
732-
customFixture.detectChanges();
774+
fixture.detectChanges();
733775

734-
customFixture.whenStable().then(() => {
776+
fixture.whenStable().then(() => {
735777
expect(containerEl.querySelectorAll('md-error').length)
736-
.toBe(1, 'Expected one error messages to have been rendered.');
778+
.toBe(1, 'Expected one error messages to have been rendered.');
737779
});
738780
});
739781
}));
@@ -763,18 +805,19 @@ describe('MdInputContainer', function () {
763805
]
764806
});
765807

766-
let customFixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
767-
customFixture.detectChanges();
808+
let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
768809

769-
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
770-
testComponent = customFixture.componentInstance;
810+
fixture.detectChanges();
811+
812+
let containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
813+
let testComponent = fixture.componentInstance;
771814

772815
// Expect the control to still be untouched but the error to show due to the global setting
773816
expect(testComponent.formControl.untouched).toBe(true, 'Expected untouched form control');
774817
expect(containerEl.querySelectorAll('md-error').length).toBe(1, 'Expected an error message');
775818
});
776819

777-
it('should display an error message when global setting shows errors on dirty', async() => {
820+
it('should display an error message when using ShowOnDirtyErrorStateMatcher', async(() => {
778821
TestBed.resetTestingModule();
779822
TestBed.configureTestingModule({
780823
imports: [
@@ -787,79 +830,36 @@ describe('MdInputContainer', function () {
787830
MdInputContainerWithFormErrorMessages
788831
],
789832
providers: [
790-
{ provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { showOnDirty: true } }
833+
{ provide: MD_ERROR_GLOBAL_OPTIONS, useClass: ShowOnDirtyErrorStateMatcher }
791834
]
792835
});
793836

794-
let customFixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
795-
customFixture.detectChanges();
837+
let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
838+
fixture.detectChanges();
796839

797-
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
798-
testComponent = customFixture.componentInstance;
840+
let containerEl = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
841+
let testComponent = fixture.componentInstance;
799842

800843
expect(testComponent.formControl.invalid).toBe(true, 'Expected form control to be invalid');
801844
expect(containerEl.querySelectorAll('md-error').length).toBe(0, 'Expected no error messages');
802845

803-
testComponent.formControl.markAsTouched();
804-
customFixture.detectChanges();
805-
806-
customFixture.whenStable().then(() => {
807-
expect(containerEl.querySelectorAll('md-error').length)
808-
.toBe(0, 'Expected no error messages when touched');
809-
810-
testComponent.formControl.markAsDirty();
811-
customFixture.detectChanges();
812-
813-
customFixture.whenStable().then(() => {
814-
expect(containerEl.querySelectorAll('md-error').length)
815-
.toBe(1, 'Expected one error message when dirty');
816-
});
817-
});
818-
819-
});
820-
821-
it('should hide the errors and show the hints once the input becomes valid', async(() => {
822846
testComponent.formControl.markAsTouched();
823847
fixture.detectChanges();
824848

825849
fixture.whenStable().then(() => {
826-
expect(containerEl.classList)
827-
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
828850
expect(containerEl.querySelectorAll('md-error').length)
829-
.toBe(1, 'Expected one error message to have been rendered.');
830-
expect(containerEl.querySelectorAll('md-hint').length)
831-
.toBe(0, 'Expected no hints to be shown.');
851+
.toBe(0, 'Expected no error messages when touched');
832852

833-
testComponent.formControl.setValue('something');
853+
testComponent.formControl.markAsDirty();
834854
fixture.detectChanges();
835855

836856
fixture.whenStable().then(() => {
837-
expect(containerEl.classList).not.toContain('mat-input-invalid',
838-
'Expected container not to have the invalid class when valid.');
839857
expect(containerEl.querySelectorAll('md-error').length)
840-
.toBe(0, 'Expected no error messages when the input is valid.');
841-
expect(containerEl.querySelectorAll('md-hint').length)
842-
.toBe(1, 'Expected one hint to be shown once the input is valid.');
858+
.toBe(1, 'Expected one error message when dirty');
843859
});
844860
});
845-
}));
846-
847-
it('should not hide the hint if there are no error messages', async(() => {
848-
testComponent.renderError = false;
849-
fixture.detectChanges();
850861

851-
expect(containerEl.querySelectorAll('md-hint').length)
852-
.toBe(1, 'Expected one hint to be shown on load.');
853-
854-
testComponent.formControl.markAsTouched();
855-
fixture.detectChanges();
856-
857-
fixture.whenStable().then(() => {
858-
expect(containerEl.querySelectorAll('md-hint').length)
859-
.toBe(1, 'Expected one hint to still be shown.');
860-
});
861862
}));
862-
863863
});
864864

865865
it('should not have prefix and suffix elements when none are specified', () => {

src/lib/input/input-container.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ import {
4343
MD_PLACEHOLDER_GLOBAL_OPTIONS
4444
} from '../core/placeholder/placeholder-options';
4545
import {
46-
ErrorStateMatcherType,
46+
DefaultErrorStateMatcher,
47+
ErrorStateMatcher,
4748
ErrorOptions,
4849
MD_ERROR_GLOBAL_OPTIONS
4950
} from '../core/error/error-options';
@@ -196,7 +197,7 @@ export class MdInputDirective {
196197
}
197198

198199
/** A function used to control when error messages are shown. */
199-
@Input() errorStateMatcher: ErrorStateMatcherType;
200+
@Input() errorStateMatcher: ErrorStateMatcher;
200201

201202
/** The input element's value. */
202203
get value() { return this._elementRef.nativeElement.value; }
@@ -240,7 +241,8 @@ export class MdInputDirective {
240241
this.id = this.id;
241242

242243
this._errorOptions = errorOptions ? errorOptions : {};
243-
this.errorStateMatcher = this._errorOptions.errorStateMatcher || undefined;
244+
this.errorStateMatcher = this._errorOptions.errorStateMatcher
245+
|| new DefaultErrorStateMatcher().errorStateMatcher;
244246
}
245247

246248
/** Focuses the input element. */
@@ -263,24 +265,7 @@ export class MdInputDirective {
263265
/** Whether the input is in an error state. */
264266
_isErrorState(): boolean {
265267
const control = this._ngControl;
266-
return this.errorStateMatcher
267-
? this.errorStateMatcher(control, this._parentFormGroup, this._parentForm)
268-
: this._defaultErrorStateMatcher(control);
269-
}
270-
271-
/** Default error state calculation */
272-
private _defaultErrorStateMatcher(control: NgControl): boolean {
273-
const isInvalid = control && control.invalid;
274-
const isTouched = control && control.touched;
275-
const isDirty = control && control.dirty;
276-
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
277-
(this._parentForm && this._parentForm.submitted);
278-
279-
if (this._errorOptions.showOnDirty) {
280-
return !!(isInvalid && (isDirty || isSubmitted));
281-
}
282-
283-
return !!(isInvalid && (isTouched || isSubmitted));
268+
return this.errorStateMatcher(control, this._parentFormGroup, this._parentForm);
284269
}
285270

286271
/** Make sure the input is a supported type. */

src/lib/input/input.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,17 @@ to all inputs.
142142

143143
Here are the available global options:
144144

145-
146145
| Name | Type | Description |
147146
| ----------------- | -------- | ----------- |
148147
| errorStateMatcher | Function | Returns a boolean specifying if the error should be shown |
149-
| showOnDirty | boolean | If true, the error will show when the control is dirty, not touched. |P
148+
149+
150+
If you just wish to make all inputs behave the same as the default, but show errors when
151+
dirty instead of touched, you can use the `ShowOnDirtyErrorStateMatcher` implementation.
152+
153+
```ts
154+
@NgModule({
155+
providers: [
156+
{ provide: MD_ERROR_GLOBAL_OPTIONS, useClass: ShowOnDirtyErrorStateMatcher }
157+
]
158+
})

0 commit comments

Comments
 (0)