Skip to content

Commit

Permalink
Address comments
Browse files Browse the repository at this point in the history
  • Loading branch information
willshowell committed Jun 22, 2017
1 parent 3ff8d87 commit b0d69cc
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 98 deletions.
2 changes: 1 addition & 1 deletion src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export {

// Error
export {
ErrorStateMatcherType,
ErrorStateMatcher,
ErrorOptions,
MD_ERROR_GLOBAL_OPTIONS
} from './error/error-options';
Expand Down
39 changes: 35 additions & 4 deletions src/lib/core/error/error-options.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
/**
* @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 {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');
new InjectionToken<ErrorOptions>('md-error-global-options');

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

export interface ErrorOptions {
errorStateMatcher?: ErrorStateMatcherType;
showOnDirty?: boolean;
errorStateMatcher?: ErrorStateMatcher;
}

export class DefaultErrorStateMatcher {

errorStateMatcher(control: NgControl, formGroup: FormGroupDirective, form: NgForm): boolean {
const isInvalid = control && control.invalid;
const isTouched = control && control.touched;
const isSubmitted = (formGroup && formGroup.submitted) ||
(form && form.submitted);

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

export class ShowOnDirtyErrorStateMatcher {

errorStateMatcher(control: NgControl, formGroup: FormGroupDirective, form: NgForm): boolean {
const isInvalid = control && control.invalid;
const isDirty = control && control.dirty;
const isSubmitted = (formGroup && formGroup.submitted) ||
(form && form.submitted);

return !!(isInvalid && (isDirty || isSubmitted));
}
}
140 changes: 70 additions & 70 deletions src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +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';
import {MD_ERROR_GLOBAL_OPTIONS, ShowOnDirtyErrorStateMatcher} from '../core/error/error-options';

describe('MdInputContainer', function () {
beforeEach(async(() => {
Expand Down Expand Up @@ -708,32 +708,74 @@ describe('MdInputContainer', function () {
});
}));

it('should display an error message when a custom error matcher returns true', async(() => {
fixture.destroy();
it('should hide the errors and show the hints once the input becomes valid', async(() => {
testComponent.formControl.markAsTouched();
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(containerEl.classList)
.toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(containerEl.querySelectorAll('md-hint').length)
.toBe(0, 'Expected no hints to be shown.');

testComponent.formControl.setValue('something');
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(containerEl.classList).not.toContain('mat-input-invalid',
'Expected container not to have the invalid class when valid.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(0, 'Expected no error messages when the input is valid.');
expect(containerEl.querySelectorAll('md-hint').length)
.toBe(1, 'Expected one hint to be shown once the input is valid.');
});
});
}));

it('should not hide the hint if there are no error messages', async(() => {
testComponent.renderError = false;
fixture.detectChanges();

expect(containerEl.querySelectorAll('md-hint').length)
.toBe(1, 'Expected one hint to be shown on load.');

testComponent.formControl.markAsTouched();
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(containerEl.querySelectorAll('md-hint').length)
.toBe(1, 'Expected one hint to still be shown.');
});
}));

});

let customFixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
let component: MdInputContainerWithCustomErrorStateMatcher;
describe('custom error behavior', () => {
it('should display an error message when a custom error matcher returns true', async(() => {
let fixture = TestBed.createComponent(MdInputContainerWithCustomErrorStateMatcher);
fixture.detectChanges();

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

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

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

customFixture.whenStable().then(() => {
fixture.whenStable().then(() => {
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error messages to have been rendered.');
.toBe(1, 'Expected one error messages to have been rendered.');
});
});
}));
Expand Down Expand Up @@ -763,18 +805,19 @@ describe('MdInputContainer', function () {
]
});

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

containerEl = customFixture.debugElement.query(By.css('md-input-container')).nativeElement;
testComponent = customFixture.componentInstance;
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 global setting shows errors on dirty', async() => {
it('should display an error message when using ShowOnDirtyErrorStateMatcher', async(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [
Expand All @@ -787,79 +830,36 @@ describe('MdInputContainer', function () {
MdInputContainerWithFormErrorMessages
],
providers: [
{ provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { showOnDirty: true } }
{ provide: MD_ERROR_GLOBAL_OPTIONS, useClass: ShowOnDirtyErrorStateMatcher }
]
});

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

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

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

testComponent.formControl.setValue('something');
testComponent.formControl.markAsDirty();
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(containerEl.classList).not.toContain('mat-input-invalid',
'Expected container not to have the invalid class when valid.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(0, 'Expected no error messages when the input is valid.');
expect(containerEl.querySelectorAll('md-hint').length)
.toBe(1, 'Expected one hint to be shown once the input is valid.');
.toBe(1, 'Expected one error message when dirty');
});
});
}));

it('should not hide the hint if there are no error messages', async(() => {
testComponent.renderError = false;
fixture.detectChanges();

expect(containerEl.querySelectorAll('md-hint').length)
.toBe(1, 'Expected one hint to be shown on load.');

testComponent.formControl.markAsTouched();
fixture.detectChanges();

fixture.whenStable().then(() => {
expect(containerEl.querySelectorAll('md-hint').length)
.toBe(1, 'Expected one hint to still be shown.');
});
}));

});

it('should not have prefix and suffix elements when none are specified', () => {
Expand Down
27 changes: 6 additions & 21 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import {
MD_PLACEHOLDER_GLOBAL_OPTIONS
} from '../core/placeholder/placeholder-options';
import {
ErrorStateMatcherType,
DefaultErrorStateMatcher,
ErrorStateMatcher,
ErrorOptions,
MD_ERROR_GLOBAL_OPTIONS
} from '../core/error/error-options';
Expand Down Expand Up @@ -196,7 +197,7 @@ export class MdInputDirective {
}

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

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

this._errorOptions = errorOptions ? errorOptions : {};
this.errorStateMatcher = this._errorOptions.errorStateMatcher || undefined;
this.errorStateMatcher = this._errorOptions.errorStateMatcher
|| new DefaultErrorStateMatcher().errorStateMatcher;
}

/** Focuses the input element. */
Expand All @@ -263,24 +265,7 @@ export class MdInputDirective {
/** Whether the input is in an error state. */
_isErrorState(): boolean {
const control = this._ngControl;
return this.errorStateMatcher
? 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));
return this.errorStateMatcher(control, this._parentFormGroup, this._parentForm);
}

/** Make sure the input is a supported type. */
Expand Down
13 changes: 11 additions & 2 deletions src/lib/input/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,17 @@ to all inputs.

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


If you just wish to make all inputs behave the same as the default, but show errors when
dirty instead of touched, you can use the `ShowOnDirtyErrorStateMatcher` implementation.

```ts
@NgModule({
providers: [
{ provide: MD_ERROR_GLOBAL_OPTIONS, useClass: ShowOnDirtyErrorStateMatcher }
]
})

0 comments on commit b0d69cc

Please sign in to comment.