From d2c11ca50be5d0e0a9391014b1a7af2ac8eb08b7 Mon Sep 17 00:00:00 2001 From: tinayuangao Date: Tue, 28 Nov 2017 11:48:28 -0800 Subject: [PATCH] fix(chip-list): fix error state changes in chip list (#8425) --- src/lib/chips/chip-list.spec.ts | 203 ++++++++++++++++--- src/lib/chips/chip-list.ts | 57 ++++-- src/lib/chips/chips-module.ts | 4 +- src/lib/core/common-behaviors/error-state.ts | 65 ++++++ src/lib/core/common-behaviors/index.ts | 1 + src/lib/core/common-behaviors/tabindex.ts | 1 - src/lib/input/input.ts | 42 ++-- src/lib/select/select.ts | 48 ++--- 8 files changed, 312 insertions(+), 109 deletions(-) create mode 100644 src/lib/core/common-behaviors/error-state.ts diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts index 5f006e427ac..ab2fa74c7ce 100644 --- a/src/lib/chips/chip-list.spec.ts +++ b/src/lib/chips/chip-list.spec.ts @@ -4,7 +4,7 @@ import {BACKSPACE, DELETE, ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@an import {createKeyboardEvent, dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing'; import {Component, DebugElement, QueryList, ViewChild, ViewChildren} from '@angular/core'; import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; -import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms'; import {MatFormFieldModule} from '@angular/material/form-field'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; @@ -35,6 +35,7 @@ describe('MatChipList', () => { NoopAnimationsModule ], declarations: [ + ChipListWithFormErrorMessages, StandardChipList, FormFieldChipList, BasicChipList, @@ -864,6 +865,121 @@ describe('MatChipList', () => { }); }); + describe('error messages', () => { + let errorTestComponent: ChipListWithFormErrorMessages; + let containerEl: HTMLElement; + let chipListEl: HTMLElement; + + beforeEach(() => { + fixture = TestBed.createComponent(ChipListWithFormErrorMessages); + fixture.detectChanges(); + errorTestComponent = fixture.componentInstance; + containerEl = fixture.debugElement.query(By.css('mat-form-field')).nativeElement; + chipListEl = fixture.debugElement.query(By.css('mat-chip-list')).nativeElement; + }); + + it('should not show any errors if the user has not interacted', () => { + expect(errorTestComponent.formControl.untouched) + .toBe(true, 'Expected untouched form control'); + expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message'); + expect(chipListEl.getAttribute('aria-invalid')) + .toBe('false', 'Expected aria-invalid to be set to "false".'); + }); + + it('should display an error message when the chip list is touched and invalid', async(() => { + expect(errorTestComponent.formControl.invalid) + .toBe(true, 'Expected form control to be invalid'); + expect(containerEl.querySelectorAll('mat-error').length) + .toBe(0, 'Expected no error message'); + + errorTestComponent.formControl.markAsTouched(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(containerEl.classList) + .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.'); + expect(containerEl.querySelectorAll('mat-error').length) + .toBe(1, 'Expected one error message to have been rendered.'); + expect(chipListEl.getAttribute('aria-invalid')) + .toBe('true', 'Expected aria-invalid to be set to "true".'); + }); + })); + + it('should display an error message when the parent form is submitted', fakeAsync(() => { + expect(errorTestComponent.form.submitted) + .toBe(false, 'Expected form not to have been submitted'); + expect(errorTestComponent.formControl.invalid) + .toBe(true, 'Expected form control to be invalid'); + expect(containerEl.querySelectorAll('mat-error').length).toBe(0, 'Expected no error message'); + + dispatchFakeEvent(fixture.debugElement.query(By.css('form')).nativeElement, 'submit'); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(errorTestComponent.form.submitted) + .toBe(true, 'Expected form to have been submitted'); + expect(containerEl.classList) + .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.'); + expect(containerEl.querySelectorAll('mat-error').length) + .toBe(1, 'Expected one error message to have been rendered.'); + expect(chipListEl.getAttribute('aria-invalid')) + .toBe('true', 'Expected aria-invalid to be set to "true".'); + }); + })); + + it('should hide the errors and show the hints once the chip list becomes valid', + fakeAsync(() => { + errorTestComponent.formControl.markAsTouched(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(containerEl.classList) + .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.'); + expect(containerEl.querySelectorAll('mat-error').length) + .toBe(1, 'Expected one error message to have been rendered.'); + expect(containerEl.querySelectorAll('mat-hint').length) + .toBe(0, 'Expected no hints to be shown.'); + + errorTestComponent.formControl.setValue('something'); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + expect(containerEl.classList).not.toContain('mat-form-field-invalid', + 'Expected container not to have the invalid class when valid.'); + expect(containerEl.querySelectorAll('mat-error').length) + .toBe(0, 'Expected no error messages when the input is valid.'); + expect(containerEl.querySelectorAll('mat-hint').length) + .toBe(1, 'Expected one hint to be shown once the input is valid.'); + }); + }); + })); + + it('should set the proper role on the error messages', () => { + errorTestComponent.formControl.markAsTouched(); + fixture.detectChanges(); + + expect(containerEl.querySelector('mat-error')!.getAttribute('role')).toBe('alert'); + }); + + it('sets the aria-describedby to reference errors when in error state', () => { + let hintId = fixture.debugElement.query(By.css('.mat-hint')).nativeElement.getAttribute('id'); + let describedBy = chipListEl.getAttribute('aria-describedby'); + + expect(hintId).toBeTruthy('hint should be shown'); + expect(describedBy).toBe(hintId); + + fixture.componentInstance.formControl.markAsTouched(); + fixture.detectChanges(); + + let errorIds = fixture.debugElement.queryAll(By.css('.mat-error')) + .map(el => el.nativeElement.getAttribute('id')).join(' '); + describedBy = chipListEl.getAttribute('aria-describedby'); + + expect(errorIds).toBeTruthy('errors should be shown'); + expect(describedBy).toBe(errorIds); + }); + }); + function setupStandardList() { fixture = TestBed.createComponent(StandardChipList); fixture.detectChanges(); @@ -940,14 +1056,14 @@ class FormFieldChipList { }) class BasicChipList { foods: any[] = [ - { value: 'steak-0', viewValue: 'Steak' }, - { value: 'pizza-1', viewValue: 'Pizza' }, - { value: 'tacos-2', viewValue: 'Tacos', disabled: true }, - { value: 'sandwich-3', viewValue: 'Sandwich' }, - { value: 'chips-4', viewValue: 'Chips' }, - { value: 'eggs-5', viewValue: 'Eggs' }, - { value: 'pasta-6', viewValue: 'Pasta' }, - { value: 'sushi-7', viewValue: 'Sushi' }, + {value: 'steak-0', viewValue: 'Steak'}, + {value: 'pizza-1', viewValue: 'Pizza'}, + {value: 'tacos-2', viewValue: 'Tacos', disabled: true}, + {value: 'sandwich-3', viewValue: 'Sandwich'}, + {value: 'chips-4', viewValue: 'Chips'}, + {value: 'eggs-5', viewValue: 'Eggs'}, + {value: 'pasta-6', viewValue: 'Pasta'}, + {value: 'sushi-7', viewValue: 'Sushi'}, ]; control = new FormControl(); isRequired: boolean; @@ -975,14 +1091,14 @@ class BasicChipList { }) class MultiSelectionChipList { foods: any[] = [ - { value: 'steak-0', viewValue: 'Steak' }, - { value: 'pizza-1', viewValue: 'Pizza' }, - { value: 'tacos-2', viewValue: 'Tacos', disabled: true }, - { value: 'sandwich-3', viewValue: 'Sandwich' }, - { value: 'chips-4', viewValue: 'Chips' }, - { value: 'eggs-5', viewValue: 'Eggs' }, - { value: 'pasta-6', viewValue: 'Pasta' }, - { value: 'sushi-7', viewValue: 'Sushi' }, + {value: 'steak-0', viewValue: 'Steak'}, + {value: 'pizza-1', viewValue: 'Pizza'}, + {value: 'tacos-2', viewValue: 'Tacos', disabled: true}, + {value: 'sandwich-3', viewValue: 'Sandwich'}, + {value: 'chips-4', viewValue: 'Chips'}, + {value: 'eggs-5', viewValue: 'Eggs'}, + {value: 'pasta-6', viewValue: 'Pasta'}, + {value: 'sushi-7', viewValue: 'Sushi'}, ]; control = new FormControl(); isRequired: boolean; @@ -1013,14 +1129,14 @@ class MultiSelectionChipList { }) class InputChipList { foods: any[] = [ - { value: 'steak-0', viewValue: 'Steak' }, - { value: 'pizza-1', viewValue: 'Pizza' }, - { value: 'tacos-2', viewValue: 'Tacos', disabled: true }, - { value: 'sandwich-3', viewValue: 'Sandwich' }, - { value: 'chips-4', viewValue: 'Chips' }, - { value: 'eggs-5', viewValue: 'Eggs' }, - { value: 'pasta-6', viewValue: 'Pasta' }, - { value: 'sushi-7', viewValue: 'Sushi' }, + {value: 'steak-0', viewValue: 'Steak'}, + {value: 'pizza-1', viewValue: 'Pizza'}, + {value: 'tacos-2', viewValue: 'Tacos', disabled: true}, + {value: 'sandwich-3', viewValue: 'Sandwich'}, + {value: 'chips-4', viewValue: 'Chips'}, + {value: 'eggs-5', viewValue: 'Eggs'}, + {value: 'pasta-6', viewValue: 'Pasta'}, + {value: 'sushi-7', viewValue: 'Sushi'}, ]; control = new FormControl(); @@ -1061,8 +1177,8 @@ class InputChipList { }) class FalsyValueChipList { foods: any[] = [ - { value: 0, viewValue: 'Steak' }, - { value: 1, viewValue: 'Pizza' }, + {value: 0, viewValue: 'Steak'}, + {value: 1, viewValue: 'Pizza'}, ]; control = new FormControl(); @ViewChildren(MatChip) chips: QueryList; @@ -1079,9 +1195,36 @@ class FalsyValueChipList { }) class SelectedChipList { foods: any[] = [ - { value: 0, viewValue: 'Steak', selected: true }, - { value: 1, viewValue: 'Pizza', selected: false }, - { value: 2, viewValue: 'Pasta', selected: true }, + {value: 0, viewValue: 'Steak', selected: true}, + {value: 1, viewValue: 'Pizza', selected: false}, + {value: 2, viewValue: 'Pasta', selected: true}, ]; @ViewChildren(MatChip) chips: QueryList; } + +@Component({ + template: ` +
+ + + + {{food.viewValue}} + + + Please select a chip, or type to add a new chip + Should have value + +
+ ` +}) +class ChipListWithFormErrorMessages { + foods: any[] = [ + {value: 0, viewValue: 'Steak', selected: true}, + {value: 1, viewValue: 'Pizza', selected: false}, + {value: 2, viewValue: 'Pasta', selected: true}, + ]; + @ViewChildren(MatChip) chips: QueryList; + + @ViewChild('form') form: NgForm; + formControl = new FormControl('', Validators.required); +} diff --git a/src/lib/chips/chip-list.ts b/src/lib/chips/chip-list.ts index c5e0451731a..b9031baadf6 100644 --- a/src/lib/chips/chip-list.ts +++ b/src/lib/chips/chip-list.ts @@ -18,6 +18,7 @@ import { ChangeDetectorRef, Component, ContentChildren, + DoCheck, ElementRef, EventEmitter, Input, @@ -29,15 +30,31 @@ import { Self, ViewEncapsulation, } from '@angular/core'; -import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; +import { + ControlValueAccessor, + FormGroupDirective, + NgControl, + NgForm +} from '@angular/forms'; +import {ErrorStateMatcher, mixinErrorState, CanUpdateErrorState} from '@angular/material/core'; import {MatFormFieldControl} from '@angular/material/form-field'; import {Observable} from 'rxjs/Observable'; import {merge} from 'rxjs/observable/merge'; -import {Subject} from 'rxjs/Subject'; import {Subscription} from 'rxjs/Subscription'; import {MatChip, MatChipEvent, MatChipSelectionChange} from './chip'; import {MatChipInput} from './chip-input'; +// Boilerplate for applying mixins to MatChipList. +/** @docs-private */ +export class MatChipListBase { + constructor(public _defaultErrorStateMatcher: ErrorStateMatcher, + public _parentForm: NgForm, + public _parentFormGroup: FormGroupDirective, + public ngControl: NgControl) {} +} +export const _MatChipListMixinBase = mixinErrorState(MatChipListBase); + + // Increasing integer for generating unique ids for chip-list components. let nextUniqueId = 0; @@ -78,16 +95,10 @@ export class MatChipListChange { preserveWhitespaces: false, changeDetection: ChangeDetectionStrategy.OnPush }) -export class MatChipList implements MatFormFieldControl, ControlValueAccessor, - AfterContentInit, OnInit, OnDestroy { +export class MatChipList extends _MatChipListMixinBase implements MatFormFieldControl, + ControlValueAccessor, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState { readonly controlType = 'mat-chip-list'; - /** - * Stream that emits whenever the state of the input changes such that the wrapping `MatFormField` - * needs to run change detection. - */ - stateChanges = new Subject(); - /** When a chip is destroyed, we track the index so we can focus the appropriate next chip. */ protected _lastDestroyedIndex: number|null = null; @@ -173,6 +184,9 @@ export class MatChipList implements MatFormFieldControl, ControlValueAccess return this.empty ? null : 'listbox'; } + /** An object used to control when error messages are shown. */ + @Input() errorStateMatcher: ErrorStateMatcher; + /** Whether the user should be allowed to select multiple chips. */ @Input() get multiple(): boolean { return this._multiple; } @@ -251,14 +265,6 @@ export class MatChipList implements MatFormFieldControl, ControlValueAccess get disabled() { return this.ngControl ? this.ngControl.disabled : this._disabled; } set disabled(value: any) { this._disabled = coerceBooleanProperty(value); } - /** Whether the chip list is in an error state. */ - get errorState(): boolean { - const isInvalid = this.ngControl && this.ngControl.invalid; - const isTouched = this.ngControl && this.ngControl.touched; - const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) || - (this._parentForm && this._parentForm.submitted); - return !!(isInvalid && (isTouched || isSubmitted)); - } /** Orientation of the chip list. */ @Input('aria-orientation') ariaOrientation: 'horizontal' | 'vertical' = 'horizontal'; @@ -313,9 +319,11 @@ export class MatChipList implements MatFormFieldControl, ControlValueAccess constructor(protected _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef, @Optional() private _dir: Directionality, - @Optional() private _parentForm: NgForm, - @Optional() private _parentFormGroup: FormGroupDirective, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, + _defaultErrorStateMatcher: ErrorStateMatcher, @Optional() @Self() public ngControl: NgControl) { + super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); if (this.ngControl) { this.ngControl.valueAccessor = this; } @@ -352,6 +360,15 @@ export class MatChipList implements MatFormFieldControl, ControlValueAccess this.stateChanges.next(); } + ngDoCheck() { + if (this.ngControl) { + // We need to re-evaluate this on every change detection cycle, because there are some + // error triggers that we can't subscribe to (e.g. parent form submissions). This means + // that whatever logic is in here has to be super lean or we risk destroying the performance. + this.updateErrorState(); + } + } + ngOnDestroy(): void { this._tabOutSubscription.unsubscribe(); diff --git a/src/lib/chips/chips-module.ts b/src/lib/chips/chips-module.ts index 1d29f093c2e..3a8ca64e0f8 100644 --- a/src/lib/chips/chips-module.ts +++ b/src/lib/chips/chips-module.ts @@ -7,6 +7,7 @@ */ import {NgModule} from '@angular/core'; +import {ErrorStateMatcher} from '@angular/material/core'; import {MatChipList} from './chip-list'; import {MatBasicChip, MatChip, MatChipRemove} from './chip'; import {MatChipInput} from './chip-input'; @@ -15,6 +16,7 @@ import {MatChipInput} from './chip-input'; @NgModule({ imports: [], exports: [MatChipList, MatChip, MatChipInput, MatChipRemove, MatChipRemove, MatBasicChip], - declarations: [MatChipList, MatChip, MatChipInput, MatChipRemove, MatChipRemove, MatBasicChip] + declarations: [MatChipList, MatChip, MatChipInput, MatChipRemove, MatChipRemove, MatBasicChip], + providers: [ErrorStateMatcher] }) export class MatChipsModule {} diff --git a/src/lib/core/common-behaviors/error-state.ts b/src/lib/core/common-behaviors/error-state.ts new file mode 100644 index 00000000000..5f0850b2ed3 --- /dev/null +++ b/src/lib/core/common-behaviors/error-state.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright Google LLC 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 {Constructor} from './constructor'; +import {ErrorStateMatcher} from '../error/error-options'; +import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; +import {Subject} from 'rxjs/Subject'; + +/** @docs-private */ +export interface CanUpdateErrorState { + updateErrorState(); + stateChanges: Subject; + errorState: boolean; + errorStateMatcher: ErrorStateMatcher; +} + +/** @docs-private */ +export interface HasErrorState { + _parentFormGroup: FormGroupDirective; + _parentForm: NgForm; + _defaultErrorStateMatcher: ErrorStateMatcher; + ngControl: NgControl; +} + +/** + * Mixin to augment a directive with updateErrorState method. + * For component with `errorState` and need to update `errorState`. + */ +export function mixinErrorState>(base: T) +: Constructor & T { + return class extends base { + /** Whether the component is in an error state. */ + errorState: boolean = false; + + /** + * Stream that emits whenever the state of the input changes such that the wrapping + * `MatFormField needs to run change detection. + */ + stateChanges = new Subject(); + + errorStateMatcher: ErrorStateMatcher; + + updateErrorState() { + const oldState = this.errorState; + const parent = this._parentFormGroup || this._parentForm; + const matcher = this.errorStateMatcher || this._defaultErrorStateMatcher; + const control = this.ngControl ? this.ngControl.control as FormControl : null; + const newState = matcher.isErrorState(control, parent); + + if (newState !== oldState) { + this.errorState = newState; + this.stateChanges.next(); + } + } + + constructor(...args: any[]) { + super(...args); + } + }; +} diff --git a/src/lib/core/common-behaviors/index.ts b/src/lib/core/common-behaviors/index.ts index b4f74da4e4e..bfc3317731b 100644 --- a/src/lib/core/common-behaviors/index.ts +++ b/src/lib/core/common-behaviors/index.ts @@ -11,3 +11,4 @@ export {CanDisable, mixinDisabled} from './disabled'; export {CanColor, mixinColor, ThemePalette} from './color'; export {CanDisableRipple, mixinDisableRipple} from './disable-ripple'; export {HasTabIndex, mixinTabIndex} from './tabindex'; +export {CanUpdateErrorState, mixinErrorState} from './error-state'; diff --git a/src/lib/core/common-behaviors/tabindex.ts b/src/lib/core/common-behaviors/tabindex.ts index 8ed2c209fdf..5ea836a1eba 100644 --- a/src/lib/core/common-behaviors/tabindex.ts +++ b/src/lib/core/common-behaviors/tabindex.ts @@ -31,4 +31,3 @@ export function mixinTabIndex>(base: T, defaul } }; } - diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 3491133ab33..94504ca8997 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -19,8 +19,8 @@ import { Optional, Self, } from '@angular/core'; -import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; -import {ErrorStateMatcher} from '@angular/material/core'; +import {FormGroupDirective, NgControl, NgForm} from '@angular/forms'; +import {ErrorStateMatcher, mixinErrorState, CanUpdateErrorState} from '@angular/material/core'; import {MatFormFieldControl} from '@angular/material/form-field'; import {Subject} from 'rxjs/Subject'; import {getMatInputUnsupportedTypeError} from './input-errors'; @@ -43,6 +43,15 @@ const MAT_INPUT_INVALID_TYPES = [ let nextUniqueId = 0; +// Boilerplate for applying mixins to MatInput. +/** @docs-private */ +export class MatInputBase { + constructor(public _defaultErrorStateMatcher: ErrorStateMatcher, + public _parentForm: NgForm, + public _parentFormGroup: FormGroupDirective, + public ngControl: NgControl) {} +} +export const _MatInputMixinBase = mixinErrorState(MatInputBase); /** Directive that allows a native input to work inside a `MatFormField`. */ @Directive({ @@ -67,7 +76,8 @@ let nextUniqueId = 0; }, providers: [{provide: MatFormFieldControl, useExisting: MatInput}], }) -export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, DoCheck { +export class MatInput extends _MatInputMixinBase implements MatFormFieldControl, OnChanges, + OnDestroy, DoCheck, CanUpdateErrorState { /** Variables used as cache for getters and setters. */ protected _type = 'text'; protected _disabled = false; @@ -81,9 +91,6 @@ export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, /** Whether the input is focused. */ focused = false; - /** Whether the input is in an error state. */ - errorState = false; - /** The aria-describedby attribute on the input for improved a11y. */ _ariaDescribedby: string; @@ -162,10 +169,11 @@ export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, constructor(protected _elementRef: ElementRef, protected _platform: Platform, @Optional() @Self() public ngControl: NgControl, - @Optional() protected _parentForm: NgForm, - @Optional() protected _parentFormGroup: FormGroupDirective, - private _defaultErrorStateMatcher: ErrorStateMatcher, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, + _defaultErrorStateMatcher: ErrorStateMatcher, @Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any) { + super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl); // If no input value accessor was explicitly specified, use the element as the input value // accessor. this._inputValueAccessor = inputValueAccessor || this._elementRef.nativeElement; @@ -207,7 +215,7 @@ export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, // We need to re-evaluate this on every change detection cycle, because there are some // error triggers that we can't subscribe to (e.g. parent form submissions). This means // that whatever logic is in here has to be super lean or we risk destroying the performance. - this._updateErrorState(); + this.updateErrorState(); } else { // When the input isn't used together with `@angular/forms`, we need to check manually for // changes to the native `value` property in order to update the floating label. @@ -235,20 +243,6 @@ export class MatInput implements MatFormFieldControl, OnChanges, OnDestroy, // FormsModule or ReactiveFormsModule, because Angular forms also listens to input events. } - /** Re-evaluates the error state. This is only relevant with @angular/forms. */ - protected _updateErrorState() { - const oldState = this.errorState; - const parent = this._parentFormGroup || this._parentForm; - const matcher = this.errorStateMatcher || this._defaultErrorStateMatcher; - const control = this.ngControl ? this.ngControl.control as FormControl : null; - const newState = matcher.isErrorState(control, parent); - - if (newState !== oldState) { - this.errorState = newState; - this.stateChanges.next(); - } - } - /** Does some manual dirty checking on the native input `value` property. */ protected _dirtyCheckNativeValue() { const newValue = this.value; diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 1e652f7f94e..bf2cf9dff61 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -53,7 +53,6 @@ import { } from '@angular/core'; import { ControlValueAccessor, - FormControl, FormGroupDirective, NgControl, NgForm @@ -61,6 +60,8 @@ import { import { CanDisable, ErrorStateMatcher, + CanUpdateErrorState, + mixinErrorState, HasTabIndex, MatOptgroup, MatOption, @@ -142,9 +143,13 @@ export class MatSelectChange { // Boilerplate for applying mixins to MatSelect. /** @docs-private */ export class MatSelectBase { - constructor(public _elementRef: ElementRef) {} + constructor(public _elementRef: ElementRef, + public _defaultErrorStateMatcher: ErrorStateMatcher, + public _parentForm: NgForm, + public _parentFormGroup: FormGroupDirective, + public ngControl: NgControl) {} } -export const _MatSelectMixinBase = mixinTabIndex(mixinDisabled(MatSelectBase)); +export const _MatSelectMixinBase = mixinTabIndex(mixinDisabled(mixinErrorState(MatSelectBase))); /** @@ -198,7 +203,7 @@ export class MatSelectTrigger {} }) export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor, CanDisable, HasTabIndex, - MatFormFieldControl { + MatFormFieldControl, CanUpdateErrorState { /** Whether or not the overlay panel is open. */ private _panelOpen = false; @@ -284,12 +289,6 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, }, ]; - /** - * Stream that emits whenever the state of the select changes such that the wrapping - * `MatFormField` needs to run change detection. - */ - stateChanges = new Subject(); - /** Whether the select is focused. */ focused = false; @@ -451,17 +450,17 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, private _viewportRuler: ViewportRuler, private _changeDetectorRef: ChangeDetectorRef, private _ngZone: NgZone, - private _defaultErrorStateMatcher: ErrorStateMatcher, + _defaultErrorStateMatcher: ErrorStateMatcher, elementRef: ElementRef, @Optional() private _dir: Directionality, - @Optional() private _parentForm: NgForm, - @Optional() private _parentFormGroup: FormGroupDirective, + @Optional() _parentForm: NgForm, + @Optional() _parentFormGroup: FormGroupDirective, @Optional() private _parentFormField: MatFormField, @Self() @Optional() public ngControl: NgControl, @Attribute('tabindex') tabIndex: string, @Inject(MAT_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory) { - - super(elementRef); + super(elementRef, _defaultErrorStateMatcher, _parentForm, + _parentFormGroup, ngControl); if (this.ngControl) { this.ngControl.valueAccessor = this; @@ -489,7 +488,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, ngDoCheck() { if (this.ngControl) { - this._updateErrorState(); + this.updateErrorState(); } } @@ -729,9 +728,6 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, return !this._selectionModel || this._selectionModel.isEmpty(); } - /** Whether the select is in an error state. */ - errorState: boolean; - private _initializeSelection(): void { // Defer setting the value in order to avoid the "Expression // has changed after it was checked" errors from Angular. @@ -1188,20 +1184,6 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, return this._triggerFontSize * SELECT_ITEM_HEIGHT_EM; } - /** Updates the select's error state. Only relevant when used with @angular/forms. */ - private _updateErrorState() { - const oldState = this.errorState; - const parent = this._parentFormGroup || this._parentForm; - const matcher = this.errorStateMatcher || this._defaultErrorStateMatcher; - const control = this.ngControl ? this.ngControl.control as FormControl : null; - const newState = matcher.isErrorState(control, parent); - - if (newState !== oldState) { - this.errorState = newState; - this.stateChanges.next(); - } - } - // Implemented as part of MatFormFieldControl. setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' ');