mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(chip-list): fix error state changes in chip list (#8425)
This commit is contained in:
+173
-30
@@ -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<MatChip>;
|
||||
@@ -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<MatChip>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<form #form="ngForm" novalidate>
|
||||
<mat-form-field>
|
||||
<mat-chip-list [formControl]="formControl">
|
||||
<mat-chip *ngFor="let food of foods" [value]="food.value" [selected]="food.selected">
|
||||
{{food.viewValue}}
|
||||
</mat-chip>
|
||||
</mat-chip-list>
|
||||
<mat-hint>Please select a chip, or type to add a new chip</mat-hint>
|
||||
<mat-error>Should have value</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
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<MatChip>;
|
||||
|
||||
@ViewChild('form') form: NgForm;
|
||||
formControl = new FormControl('', Validators.required);
|
||||
}
|
||||
|
||||
+37
-20
@@ -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<any>, ControlValueAccessor,
|
||||
AfterContentInit, OnInit, OnDestroy {
|
||||
export class MatChipList extends _MatChipListMixinBase implements MatFormFieldControl<any>,
|
||||
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<void>();
|
||||
|
||||
/** 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<any>, 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<any>, 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<any>, 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<any>, 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();
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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<void>;
|
||||
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<T extends Constructor<HasErrorState>>(base: T)
|
||||
: Constructor<CanUpdateErrorState> & 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<void>();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -31,4 +31,3 @@ export function mixinTabIndex<T extends Constructor<CanDisable>>(base: T, defaul
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+18
-24
@@ -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<any>, OnChanges, OnDestroy, DoCheck {
|
||||
export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<any>, 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<any>, 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<any>, 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<any>, 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<any>, 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;
|
||||
|
||||
+15
-33
@@ -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<any> {
|
||||
MatFormFieldControl<any>, 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<void>();
|
||||
|
||||
/** 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(' ');
|
||||
|
||||
Reference in New Issue
Block a user