Skip to content

Commit

Permalink
feat(input): Add custom error state matcher
Browse files Browse the repository at this point in the history
  • Loading branch information
willshowell committed Jun 1, 2017
1 parent 945aa43 commit f22a77a
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 1 deletion.
11 changes: 11 additions & 0 deletions src/demo-app/input/input-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ <h4>Inside a form</h4>

<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>

Expand Down
10 changes: 9 additions & 1 deletion src/demo-app/input/input-demo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
import {FormControl, Validators, NgControl} from '@angular/forms';


let max = 5;
Expand All @@ -23,6 +23,7 @@ export class InputDemo {
errorMessageExample1: string;
errorMessageExample2: string;
errorMessageExample3: string;
errorMessageExample4: string;
dividerColorExample1: string;
dividerColorExample2: string;
dividerColorExample3: string;
Expand All @@ -43,4 +44,11 @@ export class InputDemo {
this.items.push({ value: ++max });
}
}

customErrorStateMatcher(c: NgControl): boolean {
const isDirty = c.dirty;
const isInvalid = c.invalid;

return isDirty && isInvalid;
}
}
55 changes: 55 additions & 0 deletions src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
FormGroupDirective,
FormsModule,
NgForm,
NgControl,
ReactiveFormsModule,
Validators
} from '@angular/forms';
Expand Down Expand Up @@ -56,6 +57,7 @@ describe('MdInputContainer', function () {
MdInputContainerWithDynamicPlaceholder,
MdInputContainerWithFormControl,
MdInputContainerWithFormErrorMessages,
MdInputContainerWithCustomErrorStateMatcher,
MdInputContainerWithFormGroupErrorMessages,
MdInputContainerWithId,
MdInputContainerWithPrefixAndSuffix,
Expand Down Expand Up @@ -682,6 +684,36 @@ describe('MdInputContainer', function () {
});
}));

it('should display an error message when a custom error matcher returns true', async(() => {
fixture.destroy();

let customFixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
let component: MdInputContainerWithCustomErrorStateMatcher;

customFixture.detectChanges();
component = customFixture.componentInstance;
containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;

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

component.formControl.markAsTouched();
customFixture.detectChanges();

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

component.errorState = true;
customFixture.detectChanges();

customFixture.whenStable().then(() => {
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error messages to have been rendered.');
});
});
}));

it('should hide the errors and show the hints once the input becomes valid', async(() => {
testComponent.formControl.markAsTouched();
fixture.detectChanges();
Expand Down Expand Up @@ -995,6 +1027,29 @@ class MdInputContainerWithFormErrorMessages {
renderError = true;
}

@Component({
template: `
<form #form="ngForm" novalidate>
<md-input-container>
<input mdInput
[formControl]="formControl"
[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 {
@ViewChild('form') form: NgForm;
formControl = new FormControl('', Validators.required);
errorState = false;

customErrorStateMatcher(c: NgControl): boolean {
return this.errorState;
}
}

@Component({
template: `
<form [formGroup]="formGroup" novalidate>
Expand Down
10 changes: 10 additions & 0 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ export class MdInputDirective {
}
}

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

/** The input element's value. */
get value() { return this._elementRef.nativeElement.value; }
set value(value: string) { this._elementRef.nativeElement.value = value; }
Expand Down Expand Up @@ -239,6 +242,13 @@ export class MdInputDirective {
/** Whether the input is in an error state. */
_isErrorState(): boolean {
const control = this._ngControl;
return this.errorStateMatcher
? this.errorStateMatcher(control)
: this._defaultErrorStateMatcher(control);
}

/** Default error state calculation */
private _defaultErrorStateMatcher(control: NgControl): boolean {
const isInvalid = control && control.invalid;
const isTouched = control && control.touched;
const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) ||
Expand Down
22 changes: 22 additions & 0 deletions src/lib/input/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,25 @@ 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 the user has interacted with
(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.

```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: NgControl): boolean {
return control.invalid && control.dirty;
}
```

0 comments on commit f22a77a

Please sign in to comment.