Skip to content

Commit

Permalink
Add ability to globally configure errorStateMatcher
Browse files Browse the repository at this point in the history
- Also adds shortcut functionality to show the error when dirty (instead of touched)
- Also passes parent form group and parent form to errorStateMatcher
  • Loading branch information
willshowell committed Jun 9, 2017
1 parent c34f350 commit 1f38a53
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 8 deletions.
7 changes: 7 additions & 0 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ export {
MD_PLACEHOLDER_GLOBAL_OPTIONS
} from './placeholder/placeholder-options';

// Error
export {
ErrorStateMatcherType,
ErrorOptions,
MD_ERROR_GLOBAL_OPTIONS
} from './error/error-options';

@NgModule({
imports: [
MdLineModule,
Expand Down
14 changes: 14 additions & 0 deletions src/lib/core/error/error-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {InjectionToken} from '@angular/core';
import {NgControl, FormGroupDirective, NgForm} from '@angular/forms';

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

export type ErrorStateMatcherType =
(control: NgControl, parentFormGroup: FormGroupDirective, parentForm: NgForm) => boolean;

export interface ErrorOptions {
errorStateMatcher?: ErrorStateMatcherType;
showOnDirty?: boolean;
}
83 changes: 82 additions & 1 deletion src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getMdInputContainerPlaceholderConflictError
} from './input-container-errors';
import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options';
import {MD_ERROR_GLOBAL_OPTIONS} from '../core/error/error-options';


describe('MdInputContainer', function () {
Expand Down Expand Up @@ -738,6 +739,86 @@ describe('MdInputContainer', function () {
});
}));

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 customFixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
customFixture.detectChanges();

containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
testComponent = customFixture.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 global setting shows errors on dirty', async() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [
FormsModule,
MdInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
],
declarations: [
MdInputContainerWithFormErrorMessages
],
providers: [
{ provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { showOnDirty: true } }
]
});

let customFixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages);
customFixture.detectChanges();

containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
testComponent = customFixture.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();
customFixture.detectChanges();

customFixture.whenStable().then(() => {
expect(containerEl.querySelectorAll('md-error').length)
.toBe(0, 'Expected no error messages when touched');

testComponent.formControl.markAsDirty();
customFixture.detectChanges();

customFixture.whenStable().then(() => {
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message when dirty');
});
});

});

it('should hide the errors and show the hints once the input becomes valid', async(() => {
testComponent.formControl.markAsTouched();
fixture.detectChanges();
Expand Down Expand Up @@ -1069,7 +1150,7 @@ class MdInputContainerWithCustomErrorStateMatcher {
formControl = new FormControl('', Validators.required);
errorState = false;

customErrorStateMatcher(c: NgControl): boolean {
customErrorStateMatcher(): boolean {
return this.errorState;
}
}
Expand Down
21 changes: 18 additions & 3 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ import {
PlaceholderOptions,
MD_PLACEHOLDER_GLOBAL_OPTIONS
} from '../core/placeholder/placeholder-options';
import {
ErrorStateMatcherType,
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 = [
Expand Down Expand Up @@ -129,6 +134,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;
Expand Down Expand Up @@ -182,7 +188,7 @@ export class MdInputDirective {
}

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

/** The input element's value. */
get value() { return this._elementRef.nativeElement.value; }
Expand Down Expand Up @@ -218,10 +224,14 @@ export class MdInputDirective {
private _renderer: Renderer2,
@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 || undefined;
}

/** Focuses the input element. */
Expand All @@ -245,17 +255,22 @@ export class MdInputDirective {
_isErrorState(): boolean {
const control = this._ngControl;
return this.errorStateMatcher
? this.errorStateMatcher(control)
? this.errorStateMatcher(control, this._parentFormGroup, this._parentForm)
: this._defaultErrorStateMatcher(control);
}

/** Default error state calculation */
private _defaultErrorStateMatcher(control: NgControl): boolean {
const isInvalid = control && control.invalid;
const isTouched = control && control.touched;
const isDirty = control && control.dirty;
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
(this._parentForm && this._parentForm.submitted);

if (this._errorOptions.showOnDirty) {
return !!(isInvalid && (isDirty || isSubmitted));
}

return !!(isInvalid && (isTouched || isSubmitted));
}

Expand Down
26 changes: 22 additions & 4 deletions src/lib/input/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ By default, error messages are shown when the control is invalid and the user ha
(touched) the element or the parent form has been submitted. If you wish to customize this
behavior (e.g. to show the error as soon as the invalid control is dirty), you can use the
`errorStateMatcher` property of the `mdInput`. To use this property, create a function in
your component class that accepts an `NgControl` and returns a boolean. A result of `true` will
display the error messages.
your component class that returns a boolean. A result of `true` will display the error messages.

```html
<md-input-container>
Expand All @@ -119,7 +118,26 @@ display the error messages.
```

```ts
function myErrorStateMatcher(control: NgControl): boolean {
function myErrorStateMatcher(control: NgControl, parentFg: FormGroupDirective, parentForm: NgForm): boolean {
return control.invalid && control.dirty;
}
```
```

A global error state matcher can be specified by setting the `MD_ERROR_GLOBAL_OPTIONS` provider. This applies
to all inputs.

```ts
@NgModule({
providers: [
{provide: MD_PLACEHOLDER_GLOBAL_OPTIONS, useValue: { errorStateMatcher: myErrorStateMatcher }}
]
})
```

Here are the available global options:


| Name | Type | Description |
| ----------------- | -------- | ----------- |
| errorStateMatcher | Function | Returns a boolean specifying if the error should be shown |
| showOnDirty | boolean | If true, the error will show when the control is dirty, not touched. |P

0 comments on commit 1f38a53

Please sign in to comment.