From 86de634e72a1692152131437a279a0e1d160b603 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 30 Jul 2017 11:52:10 +0300 Subject: [PATCH 1/7] feat(select): add support for custom errorStateMatcher * Allows for the `md-select` error behavior to be configured through an `@Input`, as well as globally through the same provider as `md-input-container`. * Simplifies the signature of some of the error option symbols. --- src/lib/core/error/error-options.ts | 19 ++++---- src/lib/input/input-container.spec.ts | 7 +-- src/lib/input/input-container.ts | 9 ++-- src/lib/select/select.spec.ts | 65 ++++++++++++++++++++++++++- src/lib/select/select.ts | 24 +++++++--- 5 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index e4d1bb4d16f2..d6ebc002e619 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -7,27 +7,24 @@ */ import {InjectionToken} from '@angular/core'; -import {FormControl, FormGroupDirective, NgForm} from '@angular/forms'; +import {FormGroupDirective, NgForm, NgControl} from '@angular/forms'; /** Injection token that can be used to specify the global error options. */ export const MD_ERROR_GLOBAL_OPTIONS = new InjectionToken('md-error-global-options'); export type ErrorStateMatcher = - (control: FormControl, form: FormGroupDirective | NgForm) => boolean; + (control: NgControl | null, form: FormGroupDirective | NgForm | null) => boolean; export interface ErrorOptions { errorStateMatcher?: ErrorStateMatcher; } /** Returns whether control is invalid and is either touched or is a part of a submitted form. */ -export function defaultErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm) { - const isSubmitted = form && form.submitted; - return !!(control.invalid && (control.touched || isSubmitted)); -} +export const defaultErrorStateMatcher: ErrorStateMatcher = (control, form) => { + return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; +}; /** Returns whether control is invalid and is either dirty or is a part of a submitted form. */ -export function showOnDirtyErrorStateMatcher(control: FormControl, - form: FormGroupDirective | NgForm) { - const isSubmitted = form && form.submitted; - return !!(control.invalid && (control.dirty || isSubmitted)); -} +export const showOnDirtyErrorStateMatcher: ErrorStateMatcher = (control, form) => { + return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; +}; diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 4c1b92ed567c..de85f166b619 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -1247,7 +1247,7 @@ class MdInputContainerWithFormErrorMessages { + [errorStateMatcher]="customErrorStateMatcher"> Please type something This field is required @@ -1260,10 +1260,7 @@ class MdInputContainerWithCustomErrorStateMatcher { }); errorState = false; - - customErrorStateMatcher(): boolean { - return this.errorState; - } + customErrorStateMatcher = () => this.errorState; } @Component({ diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 58ec2afe5fb8..35256567c161 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -31,7 +31,7 @@ import { } from '@angular/core'; import {animate, state, style, transition, trigger} from '@angular/animations'; import {coerceBooleanProperty, Platform} from '../core'; -import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; +import {FormGroupDirective, NgControl, NgForm} from '@angular/forms'; import {getSupportedInputTypes} from '../core/platform/features'; import { getMdInputContainerDuplicatedHintError, @@ -248,7 +248,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { // Force setter to be called in case id was not specified. this.id = this.id; - this._errorOptions = errorOptions ? errorOptions : {}; + this._errorOptions = errorOptions || {}; this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher; // On some versions of iOS the caret gets stuck in the wrong place when holding down the delete @@ -321,9 +321,8 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { const oldState = this._isErrorState; - const control = this._ngControl; - const parent = this._parentFormGroup || this._parentForm; - const newState = control && this.errorStateMatcher(control.control as FormControl, parent); + const newState = this.errorStateMatcher(this._ngControl, + this._parentFormGroup || this._parentForm); if (newState !== oldState) { this._isErrorState = newState; diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index b34f502d9f71..576884e1c5c2 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -31,6 +31,7 @@ import {Subject} from 'rxjs/Subject'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage} from '@angular/cdk/testing'; import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; +import {MD_ERROR_GLOBAL_OPTIONS, ErrorOptions} from '../core/error/error-options'; import { FloatPlaceholderType, MD_PLACEHOLDER_GLOBAL_OPTIONS @@ -74,7 +75,8 @@ describe('MdSelect', () => { BasicSelectWithoutForms, BasicSelectWithoutFormsPreselected, BasicSelectWithoutFormsMultiple, - SelectInsideFormGroup + SelectInsideFormGroup, + CustomErrorBehaviorSelect ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -2675,6 +2677,46 @@ describe('MdSelect', () => { .toBe('true', 'Expected aria-invalid to be set to true.'); }); + it('should be able to override the error matching behavior via an @Input', () => { + fixture.destroy(); + + const customErrorFixture = TestBed.createComponent(CustomErrorBehaviorSelect); + const component = customErrorFixture.componentInstance; + const matcher = jasmine.createSpy('error state matcher').and.returnValue(true); + + customErrorFixture.detectChanges(); + + expect(component.control.invalid).toBe(false); + expect(component.select._isErrorState()).toBe(false); + + customErrorFixture.componentInstance.errorStateMatcher = matcher; + customErrorFixture.detectChanges(); + + expect(component.select._isErrorState()).toBe(true); + expect(matcher).toHaveBeenCalled(); + }); + + it('should be able to override the error matching behavior via the injection token', () => { + const errorOptions: ErrorOptions = { + errorStateMatcher: jasmine.createSpy('error state matcher').and.returnValue(true) + }; + + fixture.destroy(); + + TestBed.resetTestingModule().configureTestingModule({ + imports: [MdSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule], + declarations: [SelectInsideFormGroup], + providers: [{ provide: MD_ERROR_GLOBAL_OPTIONS, useValue: errorOptions }], + }); + + const errorFixture = TestBed.createComponent(SelectInsideFormGroup); + const component = errorFixture.componentInstance; + + errorFixture.detectChanges(); + + expect(component.select._isErrorState()).toBe(true); + expect(errorOptions.errorStateMatcher).toHaveBeenCalled(); + }); }); }); @@ -3147,6 +3189,7 @@ class InvalidSelectInForm { }) class SelectInsideFormGroup { @ViewChild(FormGroupDirective) formGroupDirective: FormGroupDirective; + @ViewChild(MdSelect) select: MdSelect; formControl = new FormControl('', Validators.required); formGroup = new FormGroup({ food: this.formControl @@ -3212,3 +3255,23 @@ class BasicSelectWithoutFormsMultiple { @ViewChild(MdSelect) select: MdSelect; } + +@Component({ + template: ` + + + {{ food.viewValue }} + + + ` +}) +class CustomErrorBehaviorSelect { + @ViewChild(MdSelect) select: MdSelect; + control = new FormControl(); + foods: any[] = [ + { value: 'steak-0', viewValue: 'Steak' }, + { value: 'pizza-1', viewValue: 'Pizza' }, + ]; + errorStateMatcher = () => false; +} + diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 7a53b46b53c9..c8c27745dee2 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -57,6 +57,12 @@ import { // tslint:disable-next-line:no-unused-variable import {ScrollStrategy, RepositionScrollStrategy} from '../core/overlay/scroll'; import {Platform} from '@angular/cdk/platform'; +import { + defaultErrorStateMatcher, + ErrorStateMatcher, + ErrorOptions, + MD_ERROR_GLOBAL_OPTIONS +} from '../core/error/error-options'; /** * The following style constants are necessary to save here in order @@ -217,6 +223,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Deals with configuring placeholder options */ private _placeholderOptions: PlaceholderOptions; + /** Options that determine how an invalid select behaves. */ + private _errorOptions: ErrorOptions; + /** * The width of the trigger. Must be saved to set the min width of the overlay panel * and the width of the selected value. @@ -360,6 +369,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Input that can be used to specify the `aria-labelledby` attribute. */ @Input('aria-labelledby') ariaLabelledby: string = ''; + /** A function used to control when error messages are shown. */ + @Input() errorStateMatcher: ErrorStateMatcher; + /** Combined stream of all of the child options' change events. */ get optionSelectionChanges(): Observable { return merge(...this.options.map(option => option.onSelectionChange)); @@ -394,7 +406,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On @Self() @Optional() public _control: NgControl, @Attribute('tabindex') tabIndex: string, @Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions, - @Inject(MD_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory) { + @Inject(MD_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory, + @Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) { super(renderer, elementRef); @@ -405,6 +418,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On this._tabIndex = parseInt(tabIndex) || 0; this._placeholderOptions = placeholderOptions ? placeholderOptions : {}; this.floatPlaceholder = this._placeholderOptions.float || 'auto'; + this._errorOptions = errorOptions || {}; + this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher; } ngOnInit() { @@ -633,12 +648,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Whether the select is in an error state. */ _isErrorState(): boolean { - const isInvalid = this._control && this._control.invalid; - const isTouched = this._control && this._control.touched; - const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) || - (this._parentForm && this._parentForm.submitted); - - return !!(isInvalid && (isTouched || isSubmitted)); + return this.errorStateMatcher(this._control, this._parentFormGroup || this._parentForm); } /** From fe6e3d2c07074da39d0424637f50ab970e373bc2 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 2 Aug 2017 20:54:00 +0200 Subject: [PATCH 2/7] refactor: split error component into module --- src/lib/core/_core.scss | 4 ++++ src/lib/core/core.ts | 10 +++++--- src/lib/core/error/_error-theme.scss | 9 ++++++++ src/lib/core/error/_error.scss | 5 ++++ src/lib/core/error/error-options.ts | 18 ++++++++------- src/lib/core/error/error.ts | 24 +++++++++++++++++++ src/lib/core/error/index.ts | 23 +++++++++++++++++++ src/lib/input/_input-theme.scss | 4 ---- src/lib/input/index.ts | 6 ++--- src/lib/input/input-container.scss | 5 ---- src/lib/input/input-container.spec.ts | 6 ++--- src/lib/input/input-container.ts | 33 +++++---------------------- src/lib/input/input.md | 8 +++---- src/lib/select/index.ts | 5 ++-- src/lib/select/select.spec.ts | 4 ++-- src/lib/select/select.ts | 19 ++++----------- 16 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 src/lib/core/error/_error-theme.scss create mode 100644 src/lib/core/error/_error.scss create mode 100644 src/lib/core/error/error.ts create mode 100644 src/lib/core/error/index.ts diff --git a/src/lib/core/_core.scss b/src/lib/core/_core.scss index 90e9e2474732..8b194ef9c0e5 100644 --- a/src/lib/core/_core.scss +++ b/src/lib/core/_core.scss @@ -7,6 +7,8 @@ @import 'option/option-theme'; @import 'option/optgroup'; @import 'option/optgroup-theme'; +@import 'error/error'; +@import 'error/error-theme'; @import 'selection/pseudo-checkbox/pseudo-checkbox-theme'; @import 'typography/all-typography'; @@ -25,6 +27,7 @@ @include mat-ripple(); @include mat-option(); @include mat-optgroup(); + @include mat-error(); @include cdk-a11y(); @include cdk-overlay(); } @@ -35,6 +38,7 @@ @include mat-option-theme($theme); @include mat-optgroup-theme($theme); @include mat-pseudo-checkbox-theme($theme); + @include mat-error-theme($theme); // Wrapper element that provides the theme background when the // user's content isn't inside of a `md-sidenav-container`. diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index fdf65913c617..597d97c0acf3 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -16,6 +16,7 @@ import {OverlayModule} from './overlay/index'; import {A11yModule} from './a11y/index'; import {MdSelectionModule} from './selection/index'; import {MdRippleModule} from './ripple/index'; +import {MdErrorModule} from './error/index'; // Re-exports of the CDK to avoid breaking changes. export { @@ -123,12 +124,13 @@ export { // Error export { + MdErrorModule, + MdError, ErrorStateMatcher, ErrorOptions, - MD_ERROR_GLOBAL_OPTIONS, defaultErrorStateMatcher, - showOnDirtyErrorStateMatcher -} from './error/error-options'; + showOnDirtyErrorStateMatcher, +} from './error/index'; @NgModule({ imports: [ @@ -141,6 +143,7 @@ export { A11yModule, MdOptionModule, MdSelectionModule, + MdErrorModule, ], exports: [ MdLineModule, @@ -152,6 +155,7 @@ export { A11yModule, MdOptionModule, MdSelectionModule, + MdErrorModule, ], }) export class MdCoreModule {} diff --git a/src/lib/core/error/_error-theme.scss b/src/lib/core/error/_error-theme.scss new file mode 100644 index 000000000000..b5c2d65602f1 --- /dev/null +++ b/src/lib/core/error/_error-theme.scss @@ -0,0 +1,9 @@ +@import '../theming/palette'; +@import '../theming/theming'; + + +@mixin mat-error-theme($theme) { + .mat-error { + color: mat-color(map-get($theme, warn)); + } +} diff --git a/src/lib/core/error/_error.scss b/src/lib/core/error/_error.scss new file mode 100644 index 000000000000..698dafba6744 --- /dev/null +++ b/src/lib/core/error/_error.scss @@ -0,0 +1,5 @@ +@mixin mat-error { + .mat-error { + display: block; + } +} diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index d6ebc002e619..d78522b6f6db 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -6,19 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectionToken} from '@angular/core'; +import {Injectable} from '@angular/core'; import {FormGroupDirective, NgForm, NgControl} from '@angular/forms'; -/** Injection token that can be used to specify the global error options. */ -export const MD_ERROR_GLOBAL_OPTIONS = new InjectionToken('md-error-global-options'); - export type ErrorStateMatcher = (control: NgControl | null, form: FormGroupDirective | NgForm | null) => boolean; -export interface ErrorOptions { - errorStateMatcher?: ErrorStateMatcher; -} - /** Returns whether control is invalid and is either touched or is a part of a submitted form. */ export const defaultErrorStateMatcher: ErrorStateMatcher = (control, form) => { return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; @@ -28,3 +21,12 @@ export const defaultErrorStateMatcher: ErrorStateMatcher = (control, form) => { export const showOnDirtyErrorStateMatcher: ErrorStateMatcher = (control, form) => { return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; }; + +/** + * Provider that defines how form controls behave with + * regards to displaying error messages. + */ +@Injectable() +export class ErrorOptions { + errorStateMatcher: ErrorStateMatcher = defaultErrorStateMatcher; +} diff --git a/src/lib/core/error/error.ts b/src/lib/core/error/error.ts new file mode 100644 index 000000000000..1ecdcd277439 --- /dev/null +++ b/src/lib/core/error/error.ts @@ -0,0 +1,24 @@ +/** + * @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 {Directive, Input} from '@angular/core'; + +let nextUniqueId = 0; + +/** Single error message to be shown underneath a form control. */ +@Directive({ + selector: 'md-error, mat-error', + host: { + 'class': 'mat-error', + 'role': 'alert', + '[attr.id]': 'id', + } +}) +export class MdError { + @Input() id: string = `md-input-error-${nextUniqueId++}`; +} diff --git a/src/lib/core/error/index.ts b/src/lib/core/error/index.ts new file mode 100644 index 000000000000..8812516332c9 --- /dev/null +++ b/src/lib/core/error/index.ts @@ -0,0 +1,23 @@ +/** + * @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 {NgModule} from '@angular/core'; +import {MdError} from './error'; +import {ErrorOptions} from './error-options'; + +@NgModule({ + declarations: [MdError], + exports: [MdError], + providers: [ErrorOptions], +}) +export class MdErrorModule {} + + +export * from './error'; +export * from './error-options'; diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index e4b9ea76a6d8..34a6f0665cec 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -94,10 +94,6 @@ background-color: $input-underline-color-warn; } } - - .mat-input-error { - color: $input-underline-color-warn; - } } // Applies a floating placeholder above the input itself. diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts index b616a558408d..9571519a2ef5 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -8,7 +8,6 @@ import {NgModule} from '@angular/core'; import { - MdErrorDirective, MdHint, MdInputContainer, MdInputDirective, @@ -19,11 +18,11 @@ import { import {MdTextareaAutosize} from './autosize'; import {CommonModule} from '@angular/common'; import {PlatformModule} from '../core/platform/index'; +import {MdErrorModule} from '../core/error/index'; @NgModule({ declarations: [ - MdErrorDirective, MdHint, MdInputContainer, MdInputDirective, @@ -35,9 +34,9 @@ import {PlatformModule} from '../core/platform/index'; imports: [ CommonModule, PlatformModule, + MdErrorModule, ], exports: [ - MdErrorDirective, MdHint, MdInputContainer, MdInputDirective, @@ -45,6 +44,7 @@ import {PlatformModule} from '../core/platform/index'; MdPrefix, MdSuffix, MdTextareaAutosize, + MdErrorModule, ], }) export class MdInputModule {} diff --git a/src/lib/input/input-container.scss b/src/lib/input/input-container.scss index 0154daac6635..41ec46a7f8d3 100644 --- a/src/lib/input/input-container.scss +++ b/src/lib/input/input-container.scss @@ -252,8 +252,3 @@ textarea.mat-input-element { .mat-input-hint-spacer { flex: 1 0 $mat-input-hint-min-space; } - -// Single error message displayed beneath the input. -.mat-input-error { - display: block; -} diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index de85f166b619..3bee0551db3e 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -22,7 +22,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'; +import {ErrorOptions, showOnDirtyErrorStateMatcher} from '../core/error/error-options'; describe('MdInputContainer without forms', function () { beforeEach(async(() => { @@ -897,7 +897,7 @@ describe('MdInputContainer with forms', () => { ], providers: [ { - provide: MD_ERROR_GLOBAL_OPTIONS, + provide: ErrorOptions, useValue: { errorStateMatcher: globalErrorStateMatcher } } ] }); @@ -928,7 +928,7 @@ describe('MdInputContainer with forms', () => { ], providers: [ { - provide: MD_ERROR_GLOBAL_OPTIONS, + provide: ErrorOptions, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher } } ] diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 35256567c161..e3d4b1bdabcc 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -44,12 +44,7 @@ import { MD_PLACEHOLDER_GLOBAL_OPTIONS, PlaceholderOptions } from '../core/placeholder/placeholder-options'; -import { - defaultErrorStateMatcher, - ErrorOptions, - ErrorStateMatcher, - MD_ERROR_GLOBAL_OPTIONS -} from '../core/error/error-options'; +import {ErrorOptions, ErrorStateMatcher, MdError} from '../core/error/index'; import {Subject} from 'rxjs/Subject'; import {startWith} from '@angular/cdk/rxjs'; @@ -97,19 +92,6 @@ export class MdHint { @Input() id: string = `md-input-hint-${nextUniqueId++}`; } -/** Single error message to be shown underneath the input. */ -@Directive({ - selector: 'md-error, mat-error', - host: { - 'class': 'mat-input-error', - 'role': 'alert', - '[attr.id]': 'id', - } -}) -export class MdErrorDirective { - @Input() id: string = `md-input-error-${nextUniqueId++}`; -} - /** Prefix to be placed the the front of the input. */ @Directive({ selector: '[mdPrefix], [matPrefix]' @@ -151,7 +133,6 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { private _readonly = false; private _id: string; private _uid = `md-input-${nextUniqueId++}`; - private _errorOptions: ErrorOptions; private _previousNativeValue = this.value; /** Whether the input is in an error state. */ @@ -241,15 +222,13 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { constructor(private _elementRef: ElementRef, private _renderer: Renderer2, private _platform: Platform, + private _errorOptions: ErrorOptions, @Optional() @Self() public _ngControl: NgControl, @Optional() private _parentForm: NgForm, - @Optional() private _parentFormGroup: FormGroupDirective, - @Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) { + @Optional() private _parentFormGroup: FormGroupDirective) { // Force setter to be called in case id was not specified. this.id = this.id; - this._errorOptions = errorOptions || {}; - this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher; // On some versions of iOS the caret gets stuck in the wrong place when holding down the delete // key. In order to get around this we need to "jiggle" the caret loose. Since this bug only @@ -321,8 +300,8 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { const oldState = this._isErrorState; - const newState = this.errorStateMatcher(this._ngControl, - this._parentFormGroup || this._parentForm); + const errorMatcher = this.errorStateMatcher || this._errorOptions.errorStateMatcher; + const newState = errorMatcher(this._ngControl, this._parentFormGroup || this._parentForm); if (newState !== oldState) { this._isErrorState = newState; @@ -463,7 +442,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC @ViewChild('underline') underlineRef: ElementRef; @ContentChild(MdInputDirective) _mdInputChild: MdInputDirective; @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; - @ContentChildren(MdErrorDirective) _errorChildren: QueryList; + @ContentChildren(MdError) _errorChildren: QueryList; @ContentChildren(MdHint) _hintChildren: QueryList; @ContentChildren(MdPrefix) _prefixChildren: QueryList; @ContentChildren(MdSuffix) _suffixChildren: QueryList; diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 36e4bfe0e805..9d72a47cdd8a 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -132,14 +132,14 @@ function myErrorStateMatcher(control: FormControl, form: FormGroupDirective | Ng } ``` -A global error state matcher can be specified by setting the `MD_ERROR_GLOBAL_OPTIONS` provider. This applies -to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally cause -input errors to show when the input is dirty and invalid. +A global error state matcher can be specified by setting the `ErrorOptions` provider. This applies +to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally +cause input errors to show when the input is dirty and invalid. ```ts @NgModule({ providers: [ - {provide: MD_ERROR_GLOBAL_OPTIONS, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }} + {provide: ErrorOptions, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }} ] }) ``` diff --git a/src/lib/select/index.ts b/src/lib/select/index.ts index bf5f903905fb..0665f2eee6fa 100644 --- a/src/lib/select/index.ts +++ b/src/lib/select/index.ts @@ -9,7 +9,7 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {MdSelect, MD_SELECT_SCROLL_STRATEGY_PROVIDER} from './select'; -import {MdCommonModule, OverlayModule, MdOptionModule} from '../core'; +import {MdCommonModule, OverlayModule, MdOptionModule, MdErrorModule} from '../core'; @NgModule({ @@ -18,8 +18,9 @@ import {MdCommonModule, OverlayModule, MdOptionModule} from '../core'; OverlayModule, MdOptionModule, MdCommonModule, + MdErrorModule, ], - exports: [MdSelect, MdOptionModule, MdCommonModule], + exports: [MdSelect, MdOptionModule, MdCommonModule, MdErrorModule], declarations: [MdSelect], providers: [MD_SELECT_SCROLL_STRATEGY_PROVIDER] }) diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 576884e1c5c2..a5837447c043 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -31,7 +31,7 @@ import {Subject} from 'rxjs/Subject'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage} from '@angular/cdk/testing'; import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; -import {MD_ERROR_GLOBAL_OPTIONS, ErrorOptions} from '../core/error/error-options'; +import {ErrorOptions} from '../core/error/error-options'; import { FloatPlaceholderType, MD_PLACEHOLDER_GLOBAL_OPTIONS @@ -2706,7 +2706,7 @@ describe('MdSelect', () => { TestBed.resetTestingModule().configureTestingModule({ imports: [MdSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule], declarations: [SelectInsideFormGroup], - providers: [{ provide: MD_ERROR_GLOBAL_OPTIONS, useValue: errorOptions }], + providers: [{ provide: ErrorOptions, useValue: errorOptions }], }); const errorFixture = TestBed.createComponent(SelectInsideFormGroup); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index c8c27745dee2..1899d21ff820 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -57,12 +57,7 @@ import { // tslint:disable-next-line:no-unused-variable import {ScrollStrategy, RepositionScrollStrategy} from '../core/overlay/scroll'; import {Platform} from '@angular/cdk/platform'; -import { - defaultErrorStateMatcher, - ErrorStateMatcher, - ErrorOptions, - MD_ERROR_GLOBAL_OPTIONS -} from '../core/error/error-options'; +import {ErrorStateMatcher, ErrorOptions} from '../core/error/error-options'; /** * The following style constants are necessary to save here in order @@ -223,9 +218,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Deals with configuring placeholder options */ private _placeholderOptions: PlaceholderOptions; - /** Options that determine how an invalid select behaves. */ - private _errorOptions: ErrorOptions; - /** * The width of the trigger. Must be saved to set the min width of the overlay panel * and the width of the selected value. @@ -398,6 +390,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On private _changeDetectorRef: ChangeDetectorRef, private _overlay: Overlay, private _platform: Platform, + private _errorOptions: ErrorOptions, renderer: Renderer2, elementRef: ElementRef, @Optional() private _dir: Directionality, @@ -406,8 +399,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On @Self() @Optional() public _control: NgControl, @Attribute('tabindex') tabIndex: string, @Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions, - @Inject(MD_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory, - @Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) { + @Inject(MD_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory) { super(renderer, elementRef); @@ -418,8 +410,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On this._tabIndex = parseInt(tabIndex) || 0; this._placeholderOptions = placeholderOptions ? placeholderOptions : {}; this.floatPlaceholder = this._placeholderOptions.float || 'auto'; - this._errorOptions = errorOptions || {}; - this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher; } ngOnInit() { @@ -648,7 +638,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Whether the select is in an error state. */ _isErrorState(): boolean { - return this.errorStateMatcher(this._control, this._parentFormGroup || this._parentForm); + const errorMatcher = this.errorStateMatcher || this._errorOptions.errorStateMatcher; + return errorMatcher(this._control, this._parentFormGroup || this._parentForm); } /** From bbca4cbbad3032e6f908c5c93a90247751e7b9d6 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 2 Aug 2017 21:24:29 +0200 Subject: [PATCH 3/7] refactor: preserve context when invoking the error state matcher --- src/lib/core/core.ts | 1 - src/lib/core/error/error-options.ts | 9 +++------ src/lib/input/input-container.spec.ts | 4 ++-- src/lib/input/input-container.ts | 5 +++-- src/lib/input/input.md | 2 +- src/lib/select/select.spec.ts | 2 +- src/lib/select/select.ts | 8 ++++++-- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 597d97c0acf3..20e2b2bf03f8 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -128,7 +128,6 @@ export { MdError, ErrorStateMatcher, ErrorOptions, - defaultErrorStateMatcher, showOnDirtyErrorStateMatcher, } from './error/index'; diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index d78522b6f6db..8fceeccc6c71 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -12,11 +12,6 @@ import {FormGroupDirective, NgForm, NgControl} from '@angular/forms'; export type ErrorStateMatcher = (control: NgControl | null, form: FormGroupDirective | NgForm | null) => boolean; -/** Returns whether control is invalid and is either touched or is a part of a submitted form. */ -export const defaultErrorStateMatcher: ErrorStateMatcher = (control, form) => { - return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; -}; - /** Returns whether control is invalid and is either dirty or is a part of a submitted form. */ export const showOnDirtyErrorStateMatcher: ErrorStateMatcher = (control, form) => { return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; @@ -28,5 +23,7 @@ export const showOnDirtyErrorStateMatcher: ErrorStateMatcher = (control, form) = */ @Injectable() export class ErrorOptions { - errorStateMatcher: ErrorStateMatcher = defaultErrorStateMatcher; + isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; + } } diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 3bee0551db3e..24b6956704a0 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -898,7 +898,7 @@ describe('MdInputContainer with forms', () => { providers: [ { provide: ErrorOptions, - useValue: { errorStateMatcher: globalErrorStateMatcher } } + useValue: { isErrorState: globalErrorStateMatcher } } ] }); @@ -929,7 +929,7 @@ describe('MdInputContainer with forms', () => { providers: [ { provide: ErrorOptions, - useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher } + useValue: { isErrorState: showOnDirtyErrorStateMatcher } } ] }); diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index e3d4b1bdabcc..cf86be6b7107 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -300,8 +300,9 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { const oldState = this._isErrorState; - const errorMatcher = this.errorStateMatcher || this._errorOptions.errorStateMatcher; - const newState = errorMatcher(this._ngControl, this._parentFormGroup || this._parentForm); + const newState = this.errorStateMatcher ? + this.errorStateMatcher(this._ngControl, this._parentFormGroup || this._parentForm) : + this._errorOptions.isErrorState(this._ngControl, this._parentFormGroup || this._parentForm); if (newState !== oldState) { this._isErrorState = newState; diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 9d72a47cdd8a..4b3ac56867f0 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -139,7 +139,7 @@ cause input errors to show when the input is dirty and invalid. ```ts @NgModule({ providers: [ - {provide: ErrorOptions, useValue: { errorStateMatcher: showOnDirtyErrorStateMatcher }} + {provide: ErrorOptions, useValue: { isErrorState: showOnDirtyErrorStateMatcher }} ] }) ``` diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index a5837447c043..2130c20d9538 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -2698,7 +2698,7 @@ describe('MdSelect', () => { it('should be able to override the error matching behavior via the injection token', () => { const errorOptions: ErrorOptions = { - errorStateMatcher: jasmine.createSpy('error state matcher').and.returnValue(true) + isErrorState: jasmine.createSpy('error state matcher').and.returnValue(true) }; fixture.destroy(); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 1899d21ff820..92f35901409b 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -638,8 +638,12 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Whether the select is in an error state. */ _isErrorState(): boolean { - const errorMatcher = this.errorStateMatcher || this._errorOptions.errorStateMatcher; - return errorMatcher(this._control, this._parentFormGroup || this._parentForm); + if (this.errorStateMatcher) { + return this.errorStateMatcher(this._control, this._parentFormGroup || this._parentForm); + } + + return this._errorOptions.isErrorState(this._control, + this._parentFormGroup || this._parentForm); } /** From 55fb514b2d58eb6594cead816421d5dd9549158c Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 2 Aug 2017 23:06:30 +0200 Subject: [PATCH 4/7] refactor: address feedback --- src/lib/core/core.ts | 3 +-- src/lib/core/error/error-options.ts | 23 ++++++++++------------- src/lib/core/error/index.ts | 4 ++-- src/lib/input/input-container.spec.ts | 19 ++++++------------- src/lib/input/input-container.ts | 11 +++++------ src/lib/input/input.md | 14 ++++++++------ src/lib/select/select.spec.ts | 14 +++++++------- src/lib/select/select.ts | 14 +++++--------- 8 files changed, 44 insertions(+), 58 deletions(-) diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts index 20e2b2bf03f8..fe886f3d0fbf 100644 --- a/src/lib/core/core.ts +++ b/src/lib/core/core.ts @@ -127,8 +127,7 @@ export { MdErrorModule, MdError, ErrorStateMatcher, - ErrorOptions, - showOnDirtyErrorStateMatcher, + ShowOnDirtyErrorStateMatcher, } from './error/index'; @NgModule({ diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index 8fceeccc6c71..e4848c522cc1 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -9,21 +9,18 @@ import {Injectable} from '@angular/core'; import {FormGroupDirective, NgForm, NgControl} from '@angular/forms'; -export type ErrorStateMatcher = - (control: NgControl | null, form: FormGroupDirective | NgForm | null) => boolean; - -/** Returns whether control is invalid and is either dirty or is a part of a submitted form. */ -export const showOnDirtyErrorStateMatcher: ErrorStateMatcher = (control, form) => { - return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; -}; +/** Error state matcher that matches when a control is invalid and dirty. */ +@Injectable() +export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher { + match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; + } +} -/** - * Provider that defines how form controls behave with - * regards to displaying error messages. - */ +/** Provider that defines how form controls behave with regards to displaying error messages. */ @Injectable() -export class ErrorOptions { - isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { +export class ErrorStateMatcher { + match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; } } diff --git a/src/lib/core/error/index.ts b/src/lib/core/error/index.ts index 8812516332c9..7883b967f43c 100644 --- a/src/lib/core/error/index.ts +++ b/src/lib/core/error/index.ts @@ -9,12 +9,12 @@ import {NgModule} from '@angular/core'; import {MdError} from './error'; -import {ErrorOptions} from './error-options'; +import {ErrorStateMatcher} from './error-options'; @NgModule({ declarations: [MdError], exports: [MdError], - providers: [ErrorOptions], + providers: [ErrorStateMatcher], }) export class MdErrorModule {} diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 24b6956704a0..ca765480c8a7 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -22,7 +22,7 @@ import { getMdInputContainerPlaceholderConflictError } from './input-container-errors'; import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options'; -import {ErrorOptions, showOnDirtyErrorStateMatcher} from '../core/error/error-options'; +import {ErrorStateMatcher, ShowOnDirtyErrorStateMatcher} from '../core/error/error-options'; describe('MdInputContainer without forms', function () { beforeEach(async(() => { @@ -895,11 +895,7 @@ describe('MdInputContainer with forms', () => { declarations: [ MdInputContainerWithFormErrorMessages ], - providers: [ - { - provide: ErrorOptions, - useValue: { isErrorState: globalErrorStateMatcher } } - ] + providers: [{ provide: ErrorStateMatcher, useValue: { match: globalErrorStateMatcher } }] }); let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages); @@ -926,12 +922,7 @@ describe('MdInputContainer with forms', () => { declarations: [ MdInputContainerWithFormErrorMessages ], - providers: [ - { - provide: ErrorOptions, - useValue: { isErrorState: showOnDirtyErrorStateMatcher } - } - ] + providers: [{ provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher }] }); let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages); @@ -1260,7 +1251,9 @@ class MdInputContainerWithCustomErrorStateMatcher { }); errorState = false; - customErrorStateMatcher = () => this.errorState; + customErrorStateMatcher: ErrorStateMatcher = { + match: () => this.errorState + }; } @Component({ diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index cf86be6b7107..2fdb67e5e4c8 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -44,7 +44,7 @@ import { MD_PLACEHOLDER_GLOBAL_OPTIONS, PlaceholderOptions } from '../core/placeholder/placeholder-options'; -import {ErrorOptions, ErrorStateMatcher, MdError} from '../core/error/index'; +import {ErrorStateMatcher, MdError} from '../core/error/index'; import {Subject} from 'rxjs/Subject'; import {startWith} from '@angular/cdk/rxjs'; @@ -188,7 +188,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { get readonly() { return this._readonly; } set readonly(value: any) { this._readonly = coerceBooleanProperty(value); } - /** A function used to control when error messages are shown. */ + /** An object used to control when error messages are shown. */ @Input() errorStateMatcher: ErrorStateMatcher; /** The input element's value. */ @@ -222,7 +222,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { constructor(private _elementRef: ElementRef, private _renderer: Renderer2, private _platform: Platform, - private _errorOptions: ErrorOptions, + private _globalErrorStateMatcher: ErrorStateMatcher, @Optional() @Self() public _ngControl: NgControl, @Optional() private _parentForm: NgForm, @Optional() private _parentFormGroup: FormGroupDirective) { @@ -300,9 +300,8 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { const oldState = this._isErrorState; - const newState = this.errorStateMatcher ? - this.errorStateMatcher(this._ngControl, this._parentFormGroup || this._parentForm) : - this._errorOptions.isErrorState(this._ngControl, this._parentFormGroup || this._parentForm); + const matcher = this.errorStateMatcher || this._globalErrorStateMatcher; + const newState = matcher.match(this._ngControl, this._parentFormGroup || this._parentForm); if (newState !== oldState) { this._isErrorState = newState; diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 4b3ac56867f0..14b89219caea 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -125,21 +125,23 @@ the error messages. ``` ```ts -function myErrorStateMatcher(control: FormControl, form: FormGroupDirective | NgForm): boolean { - // Error when invalid control is dirty, touched, or submitted - const isSubmitted = form && form.submitted; - return !!(control.invalid && (control.dirty || control.touched || isSubmitted))); +class MyErrorStateMatcher implements ErrorStateMatcher { + match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + // Error when invalid control is dirty, touched, or submitted + const isSubmitted = form && form.submitted; + return !!(control.invalid && (control.dirty || control.touched || isSubmitted))); + } } ``` A global error state matcher can be specified by setting the `ErrorOptions` provider. This applies -to all inputs. For convenience, `showOnDirtyErrorStateMatcher` is available in order to globally +to all inputs. For convenience, `ShowOnDirtyErrorStateMatcher` is available in order to globally cause input errors to show when the input is dirty and invalid. ```ts @NgModule({ providers: [ - {provide: ErrorOptions, useValue: { isErrorState: showOnDirtyErrorStateMatcher }} + {provide: ErrorOptions, useClass: ShowOnDirtyErrorStateMatcher} ] }) ``` diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index 2130c20d9538..a9a921f14cd8 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -31,7 +31,7 @@ import {Subject} from 'rxjs/Subject'; import {ViewportRuler} from '../core/overlay/position/viewport-ruler'; import {dispatchFakeEvent, dispatchKeyboardEvent, wrappedErrorMessage} from '@angular/cdk/testing'; import {ScrollDispatcher} from '../core/overlay/scroll/scroll-dispatcher'; -import {ErrorOptions} from '../core/error/error-options'; +import {ErrorStateMatcher} from '../core/error/error-options'; import { FloatPlaceholderType, MD_PLACEHOLDER_GLOBAL_OPTIONS @@ -2689,7 +2689,7 @@ describe('MdSelect', () => { expect(component.control.invalid).toBe(false); expect(component.select._isErrorState()).toBe(false); - customErrorFixture.componentInstance.errorStateMatcher = matcher; + customErrorFixture.componentInstance.errorStateMatcher = { match: matcher }; customErrorFixture.detectChanges(); expect(component.select._isErrorState()).toBe(true); @@ -2697,8 +2697,8 @@ describe('MdSelect', () => { }); it('should be able to override the error matching behavior via the injection token', () => { - const errorOptions: ErrorOptions = { - isErrorState: jasmine.createSpy('error state matcher').and.returnValue(true) + const errorOptions: ErrorStateMatcher = { + match: jasmine.createSpy('error state matcher').and.returnValue(true) }; fixture.destroy(); @@ -2706,7 +2706,7 @@ describe('MdSelect', () => { TestBed.resetTestingModule().configureTestingModule({ imports: [MdSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule], declarations: [SelectInsideFormGroup], - providers: [{ provide: ErrorOptions, useValue: errorOptions }], + providers: [{ provide: ErrorStateMatcher, useValue: errorOptions }], }); const errorFixture = TestBed.createComponent(SelectInsideFormGroup); @@ -2715,7 +2715,7 @@ describe('MdSelect', () => { errorFixture.detectChanges(); expect(component.select._isErrorState()).toBe(true); - expect(errorOptions.errorStateMatcher).toHaveBeenCalled(); + expect(errorOptions.match).toHaveBeenCalled(); }); }); @@ -3272,6 +3272,6 @@ class CustomErrorBehaviorSelect { { value: 'steak-0', viewValue: 'Steak' }, { value: 'pizza-1', viewValue: 'Pizza' }, ]; - errorStateMatcher = () => false; + errorStateMatcher: ErrorStateMatcher; } diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 92f35901409b..1a28af1f3e4d 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -57,7 +57,7 @@ import { // tslint:disable-next-line:no-unused-variable import {ScrollStrategy, RepositionScrollStrategy} from '../core/overlay/scroll'; import {Platform} from '@angular/cdk/platform'; -import {ErrorStateMatcher, ErrorOptions} from '../core/error/error-options'; +import {ErrorStateMatcher} from '../core/error/error-options'; /** * The following style constants are necessary to save here in order @@ -361,7 +361,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Input that can be used to specify the `aria-labelledby` attribute. */ @Input('aria-labelledby') ariaLabelledby: string = ''; - /** A function used to control when error messages are shown. */ + /** An object used to control when error messages are shown. */ @Input() errorStateMatcher: ErrorStateMatcher; /** Combined stream of all of the child options' change events. */ @@ -390,7 +390,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On private _changeDetectorRef: ChangeDetectorRef, private _overlay: Overlay, private _platform: Platform, - private _errorOptions: ErrorOptions, + private _globalErrorStateMatcher: ErrorStateMatcher, renderer: Renderer2, elementRef: ElementRef, @Optional() private _dir: Directionality, @@ -638,12 +638,8 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Whether the select is in an error state. */ _isErrorState(): boolean { - if (this.errorStateMatcher) { - return this.errorStateMatcher(this._control, this._parentFormGroup || this._parentForm); - } - - return this._errorOptions.isErrorState(this._control, - this._parentFormGroup || this._parentForm); + const matcher = this.errorStateMatcher || this._globalErrorStateMatcher; + return matcher.match(this._control, this._parentFormGroup || this._parentForm); } /** From 9200c6f87f1329f10abf7dadfe99001dd7763194 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 3 Aug 2017 07:35:52 +0200 Subject: [PATCH 5/7] refactor: rename match to isErrorState --- src/lib/core/error/error-options.ts | 4 ++-- src/lib/input/input-container.spec.ts | 6 ++++-- src/lib/input/input-container.ts | 6 +++--- src/lib/input/input.md | 23 ++++++++++++----------- src/lib/select/select.spec.ts | 10 +++++----- src/lib/select/select.ts | 2 +- 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index e4848c522cc1..fb8b45131086 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -12,7 +12,7 @@ import {FormGroupDirective, NgForm, NgControl} from '@angular/forms'; /** Error state matcher that matches when a control is invalid and dirty. */ @Injectable() export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher { - match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; } } @@ -20,7 +20,7 @@ export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher { /** Provider that defines how form controls behave with regards to displaying error messages. */ @Injectable() export class ErrorStateMatcher { - match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; } } diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index ca765480c8a7..89eb3b4a3589 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -895,7 +895,9 @@ describe('MdInputContainer with forms', () => { declarations: [ MdInputContainerWithFormErrorMessages ], - providers: [{ provide: ErrorStateMatcher, useValue: { match: globalErrorStateMatcher } }] + providers: [ + { provide: ErrorStateMatcher, useValue: { isErrorSate: globalErrorStateMatcher } } + ] }); let fixture = TestBed.createComponent(MdInputContainerWithFormErrorMessages); @@ -1252,7 +1254,7 @@ class MdInputContainerWithCustomErrorStateMatcher { errorState = false; customErrorStateMatcher: ErrorStateMatcher = { - match: () => this.errorState + isErrorSate: () => this.errorState }; } diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 2fdb67e5e4c8..10c4d8ad5cdc 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -299,9 +299,9 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { - const oldState = this._isErrorState; - const matcher = this.errorStateMatcher || this._globalErrorStateMatcher; - const newState = matcher.match(this._ngControl, this._parentFormGroup || this._parentForm); + let oldState = this._isErrorState; + let matcher = this.errorStateMatcher || this._globalErrorStateMatcher; + let newState = matcher.isErrorSate(this._ngControl, this._parentFormGroup || this._parentForm); if (newState !== oldState) { this._isErrorState = newState; diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 14b89219caea..75c8f17f626c 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -62,7 +62,8 @@ A placeholder for the input can be specified in one of two ways: either using th attribute on the `input` or `textarea`, or using an `md-placeholder` element in the `md-input-container`. Using both will raise an error. -Global default placeholder options can be specified by setting the `MD_PLACEHOLDER_GLOBAL_OPTIONS` provider. This setting will apply to all components that support the floating placeholder. +Global default placeholder options can be specified by setting the `MD_PLACEHOLDER_GLOBAL_OPTIONS` +provider. This setting will apply to all components that support the floating placeholder. ```ts @NgModule({ @@ -110,12 +111,12 @@ warn color. ### Custom Error Matcher -By default, error messages are shown when the control is invalid and either the user has interacted with -(touched) the element or the parent form has been submitted. If you wish to override this +By default, error messages are shown when the control is invalid and either the user has interacted +with (touched) the element or the parent form has been submitted. If you wish to override this behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group is invalid), you can use the `errorStateMatcher` property of the `mdInput`. To use this property, -create a function in your component class that returns a boolean. A result of `true` will display -the error messages. +create an `ErrorStateMatcher` object in your component class that has a `isErrorSate` function which +returns a boolean. A result of `true` will display the error messages. ```html @@ -126,22 +127,22 @@ the error messages. ```ts class MyErrorStateMatcher implements ErrorStateMatcher { - match(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { // Error when invalid control is dirty, touched, or submitted const isSubmitted = form && form.submitted; - return !!(control.invalid && (control.dirty || control.touched || isSubmitted))); + return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted))); } } ``` -A global error state matcher can be specified by setting the `ErrorOptions` provider. This applies -to all inputs. For convenience, `ShowOnDirtyErrorStateMatcher` is available in order to globally -cause input errors to show when the input is dirty and invalid. +A global error state matcher can be specified by setting the `ErrorStateMatcher` provider. This +applies to all inputs. For convenience, `ShowOnDirtyErrorStateMatcher` is available in order to +globally cause input errors to show when the input is dirty and invalid. ```ts @NgModule({ providers: [ - {provide: ErrorOptions, useClass: ShowOnDirtyErrorStateMatcher} + {provide: ErrorStateMatcher, useClass: ShowOnDirtyErrorStateMatcher} ] }) ``` diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index a9a921f14cd8..d617c82392e5 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -2689,7 +2689,7 @@ describe('MdSelect', () => { expect(component.control.invalid).toBe(false); expect(component.select._isErrorState()).toBe(false); - customErrorFixture.componentInstance.errorStateMatcher = { match: matcher }; + customErrorFixture.componentInstance.errorStateMatcher = { isErrorSate: matcher }; customErrorFixture.detectChanges(); expect(component.select._isErrorState()).toBe(true); @@ -2697,8 +2697,8 @@ describe('MdSelect', () => { }); it('should be able to override the error matching behavior via the injection token', () => { - const errorOptions: ErrorStateMatcher = { - match: jasmine.createSpy('error state matcher').and.returnValue(true) + const errorStateMatcher: ErrorStateMatcher = { + isErrorSate: jasmine.createSpy('error state matcher').and.returnValue(true) }; fixture.destroy(); @@ -2706,7 +2706,7 @@ describe('MdSelect', () => { TestBed.resetTestingModule().configureTestingModule({ imports: [MdSelectModule, ReactiveFormsModule, FormsModule, NoopAnimationsModule], declarations: [SelectInsideFormGroup], - providers: [{ provide: ErrorStateMatcher, useValue: errorOptions }], + providers: [{ provide: ErrorStateMatcher, useValue: errorStateMatcher }], }); const errorFixture = TestBed.createComponent(SelectInsideFormGroup); @@ -2715,7 +2715,7 @@ describe('MdSelect', () => { errorFixture.detectChanges(); expect(component.select._isErrorState()).toBe(true); - expect(errorOptions.match).toHaveBeenCalled(); + expect(errorStateMatcher.isErrorSate).toHaveBeenCalled(); }); }); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 1a28af1f3e4d..694068fc861a 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -639,7 +639,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Whether the select is in an error state. */ _isErrorState(): boolean { const matcher = this.errorStateMatcher || this._globalErrorStateMatcher; - return matcher.match(this._control, this._parentFormGroup || this._parentForm); + return matcher.isErrorSate(this._control, this._parentFormGroup || this._parentForm); } /** From 32e5de8015fae25c6d1a7f02a205c66ce3e6228f Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 3 Aug 2017 20:31:12 +0200 Subject: [PATCH 6/7] chore: many typos --- src/lib/core/error/error-options.ts | 4 ++-- src/lib/input/input-container.spec.ts | 4 ++-- src/lib/input/input-container.ts | 2 +- src/lib/input/input.md | 4 ++-- src/lib/select/select.spec.ts | 6 +++--- src/lib/select/select.ts | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/core/error/error-options.ts b/src/lib/core/error/error-options.ts index fb8b45131086..a6c3570ac8d6 100644 --- a/src/lib/core/error/error-options.ts +++ b/src/lib/core/error/error-options.ts @@ -12,7 +12,7 @@ import {FormGroupDirective, NgForm, NgControl} from '@angular/forms'; /** Error state matcher that matches when a control is invalid and dirty. */ @Injectable() export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher { - isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { return control ? !!(control.invalid && (control.dirty || (form && form.submitted))) : false; } } @@ -20,7 +20,7 @@ export class ShowOnDirtyErrorStateMatcher implements ErrorStateMatcher { /** Provider that defines how form controls behave with regards to displaying error messages. */ @Injectable() export class ErrorStateMatcher { - isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { return control ? !!(control.invalid && (control.touched || (form && form.submitted))) : false; } } diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 89eb3b4a3589..9770c19583a7 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -896,7 +896,7 @@ describe('MdInputContainer with forms', () => { MdInputContainerWithFormErrorMessages ], providers: [ - { provide: ErrorStateMatcher, useValue: { isErrorSate: globalErrorStateMatcher } } + { provide: ErrorStateMatcher, useValue: { isErrorState: globalErrorStateMatcher } } ] }); @@ -1254,7 +1254,7 @@ class MdInputContainerWithCustomErrorStateMatcher { errorState = false; customErrorStateMatcher: ErrorStateMatcher = { - isErrorSate: () => this.errorState + isErrorState: () => this.errorState }; } diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 10c4d8ad5cdc..5bac1ee57d5f 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -301,7 +301,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { private _updateErrorState() { let oldState = this._isErrorState; let matcher = this.errorStateMatcher || this._globalErrorStateMatcher; - let newState = matcher.isErrorSate(this._ngControl, this._parentFormGroup || this._parentForm); + let newState = matcher.isErrorState(this._ngControl, this._parentFormGroup || this._parentForm); if (newState !== oldState) { this._isErrorState = newState; diff --git a/src/lib/input/input.md b/src/lib/input/input.md index 75c8f17f626c..83e0ebd84fe2 100644 --- a/src/lib/input/input.md +++ b/src/lib/input/input.md @@ -115,7 +115,7 @@ By default, error messages are shown when the control is invalid and either the with (touched) the element or the parent form has been submitted. If you wish to override this behavior (e.g. to show the error as soon as the invalid control is dirty or when a parent form group is invalid), you can use the `errorStateMatcher` property of the `mdInput`. To use this property, -create an `ErrorStateMatcher` object in your component class that has a `isErrorSate` function which +create an `ErrorStateMatcher` object in your component class that has a `isErrorState` function which returns a boolean. A result of `true` will display the error messages. ```html @@ -127,7 +127,7 @@ returns a boolean. A result of `true` will display the error messages. ```ts class MyErrorStateMatcher implements ErrorStateMatcher { - isErrorSate(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { + isErrorState(control: NgControl | null, form: FormGroupDirective | NgForm | null): boolean { // Error when invalid control is dirty, touched, or submitted const isSubmitted = form && form.submitted; return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted))); diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index d617c82392e5..aaf7898694c1 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -2689,7 +2689,7 @@ describe('MdSelect', () => { expect(component.control.invalid).toBe(false); expect(component.select._isErrorState()).toBe(false); - customErrorFixture.componentInstance.errorStateMatcher = { isErrorSate: matcher }; + customErrorFixture.componentInstance.errorStateMatcher = { isErrorState: matcher }; customErrorFixture.detectChanges(); expect(component.select._isErrorState()).toBe(true); @@ -2698,7 +2698,7 @@ describe('MdSelect', () => { it('should be able to override the error matching behavior via the injection token', () => { const errorStateMatcher: ErrorStateMatcher = { - isErrorSate: jasmine.createSpy('error state matcher').and.returnValue(true) + isErrorState: jasmine.createSpy('error state matcher').and.returnValue(true) }; fixture.destroy(); @@ -2715,7 +2715,7 @@ describe('MdSelect', () => { errorFixture.detectChanges(); expect(component.select._isErrorState()).toBe(true); - expect(errorStateMatcher.isErrorSate).toHaveBeenCalled(); + expect(errorStateMatcher.isErrorState).toHaveBeenCalled(); }); }); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 694068fc861a..0ae51880ea56 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -639,7 +639,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Whether the select is in an error state. */ _isErrorState(): boolean { const matcher = this.errorStateMatcher || this._globalErrorStateMatcher; - return matcher.isErrorSate(this._control, this._parentFormGroup || this._parentForm); + return matcher.isErrorState(this._control, this._parentFormGroup || this._parentForm); } /** From 6ca1928ab66bd4ca42646167f48b5468127c1c5c Mon Sep 17 00:00:00 2001 From: crisbeto Date: Sun, 6 Aug 2017 11:07:56 +0200 Subject: [PATCH 7/7] fix: update selector in unit test --- src/lib/input/input-container.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 9770c19583a7..d8309d2b40b9 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -840,7 +840,7 @@ describe('MdInputContainer with forms', () => { fixture.componentInstance.formControl.markAsTouched(); fixture.detectChanges(); - let errorIds = fixture.debugElement.queryAll(By.css('.mat-input-error')) + let errorIds = fixture.debugElement.queryAll(By.css('.mat-error')) .map(el => el.nativeElement.getAttribute('id')).join(' '); describedBy = inputEl.getAttribute('aria-describedby');