mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
chore(input): switch to OnPush change detection (#5692)
* chore(input): switch to OnPush change detection Switches the `MdInputContainer` to `OnPush` change detection and sorts out the various change detection-related issues. Relates to #5035. * chore: linter errors
This commit is contained in:
committed by
Andrew Seguin
parent
89e9621158
commit
940f1a2dbe
@@ -16,16 +16,18 @@ import {
|
||||
ContentChildren,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Optional,
|
||||
Output,
|
||||
QueryList,
|
||||
Renderer2,
|
||||
Self,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
Inject
|
||||
Inject,
|
||||
ChangeDetectionStrategy,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
DoCheck,
|
||||
} from '@angular/core';
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {coerceBooleanProperty, Platform} from '../core';
|
||||
@@ -48,6 +50,7 @@ import {
|
||||
ErrorOptions,
|
||||
MD_ERROR_GLOBAL_OPTIONS
|
||||
} from '../core/error/error-options';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
|
||||
const MD_INPUT_INVALID_TYPES = [
|
||||
@@ -128,13 +131,13 @@ export class MdSuffix {}
|
||||
'[disabled]': 'disabled',
|
||||
'[required]': 'required',
|
||||
'[attr.aria-describedby]': 'ariaDescribedby || null',
|
||||
'[attr.aria-invalid]': '_isErrorState()',
|
||||
'(blur)': '_onBlur()',
|
||||
'(focus)': '_onFocus()',
|
||||
'[attr.aria-invalid]': '_isErrorState',
|
||||
'(blur)': '_focusChanged(false)',
|
||||
'(focus)': '_focusChanged(true)',
|
||||
'(input)': '_onInput()',
|
||||
}
|
||||
})
|
||||
export class MdInputDirective {
|
||||
export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {
|
||||
|
||||
/** Variables used as cache for getters and setters. */
|
||||
private _type = 'text';
|
||||
@@ -143,8 +146,12 @@ export class MdInputDirective {
|
||||
private _required = false;
|
||||
private _readonly = false;
|
||||
private _id: string;
|
||||
private _cachedUid: string;
|
||||
private _uid = `md-input-${nextUniqueId++}`;
|
||||
private _errorOptions: ErrorOptions;
|
||||
private _previousNativeValue = this.value;
|
||||
|
||||
/** Whether the input is in an error state. */
|
||||
_isErrorState = false;
|
||||
|
||||
/** Whether the element is focused or not. */
|
||||
focused = false;
|
||||
@@ -152,30 +159,24 @@ export class MdInputDirective {
|
||||
/** Sets the aria-describedby attribute on the input for improved a11y. */
|
||||
ariaDescribedby: string;
|
||||
|
||||
/**
|
||||
* Stream that emits whenever the state of the input changes. This allows for other components
|
||||
* (mostly `md-input-container`) that depend on the properties of `mdInput` to update their view.
|
||||
*/
|
||||
_stateChanges = new Subject<void>();
|
||||
|
||||
/** Whether the element is disabled. */
|
||||
@Input()
|
||||
get disabled() {
|
||||
return this._ngControl ? this._ngControl.disabled : this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value: any) {
|
||||
this._disabled = coerceBooleanProperty(value);
|
||||
}
|
||||
get disabled() { return this._ngControl ? this._ngControl.disabled : this._disabled; }
|
||||
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
|
||||
|
||||
/** Unique id of the element. */
|
||||
@Input()
|
||||
get id() { return this._id; }
|
||||
set id(value: string) {this._id = value || this._uid; }
|
||||
set id(value: string) { this._id = value || this._uid; }
|
||||
|
||||
/** Placeholder attribute of the element. */
|
||||
@Input()
|
||||
get placeholder() { return this._placeholder; }
|
||||
set placeholder(value: string) {
|
||||
if (this._placeholder !== value) {
|
||||
this._placeholder = value;
|
||||
this._placeholderChange.emit(this._placeholder);
|
||||
}
|
||||
}
|
||||
@Input() placeholder: string = '';
|
||||
|
||||
/** Whether the element is required. */
|
||||
@Input()
|
||||
@@ -209,11 +210,6 @@ export class MdInputDirective {
|
||||
get value() { return this._elementRef.nativeElement.value; }
|
||||
set value(value: string) { this._elementRef.nativeElement.value = value; }
|
||||
|
||||
/**
|
||||
* Emits an event when the placeholder changes so that the `md-input-container` can re-validate.
|
||||
*/
|
||||
@Output() _placeholderChange = new EventEmitter<string>();
|
||||
|
||||
/** Whether the input is empty. */
|
||||
get empty() {
|
||||
return !this._isNeverEmpty() &&
|
||||
@@ -224,8 +220,6 @@ export class MdInputDirective {
|
||||
!this._isBadInput();
|
||||
}
|
||||
|
||||
private get _uid() { return this._cachedUid = this._cachedUid || `md-input-${nextUniqueId++}`; }
|
||||
|
||||
private _neverEmptyInputTypes = [
|
||||
'date',
|
||||
'datetime',
|
||||
@@ -238,6 +232,7 @@ export class MdInputDirective {
|
||||
constructor(private _elementRef: ElementRef,
|
||||
private _renderer: Renderer2,
|
||||
private _platform: Platform,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Optional() @Self() public _ngControl: NgControl,
|
||||
@Optional() private _parentForm: NgForm,
|
||||
@Optional() private _parentFormGroup: FormGroupDirective,
|
||||
@@ -245,13 +240,30 @@ export class MdInputDirective {
|
||||
|
||||
// Force setter to be called in case id was not specified.
|
||||
this.id = this.id;
|
||||
|
||||
this._errorOptions = errorOptions ? errorOptions : {};
|
||||
this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
|
||||
}
|
||||
|
||||
/** Focuses the input element. */
|
||||
focus() { this._elementRef.nativeElement.focus(); }
|
||||
ngOnChanges() {
|
||||
this._stateChanges.next();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._stateChanges.complete();
|
||||
}
|
||||
|
||||
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();
|
||||
} 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.
|
||||
this._dirtyCheckNativeValue();
|
||||
}
|
||||
}
|
||||
|
||||
_onFocus() {
|
||||
if (!this._readonly) {
|
||||
@@ -259,7 +271,18 @@ export class MdInputDirective {
|
||||
}
|
||||
}
|
||||
|
||||
_onBlur() { this.focused = false; }
|
||||
/** Focuses the input element. */
|
||||
focus() {
|
||||
this._elementRef.nativeElement.focus();
|
||||
}
|
||||
|
||||
/** Callback for the cases where the focused state of the input changes. */
|
||||
_focusChanged(isFocused: boolean) {
|
||||
if (isFocused !== this.focused) {
|
||||
this.focused = isFocused;
|
||||
this._stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
_onInput() {
|
||||
// This is a noop function and is used to let Angular know whenever the value changes.
|
||||
@@ -271,22 +294,42 @@ export class MdInputDirective {
|
||||
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
|
||||
}
|
||||
|
||||
/** Whether the input is in an error state. */
|
||||
_isErrorState(): boolean {
|
||||
/** Re-evaluates the error state. This is only relevant with @angular/forms. */
|
||||
private _updateErrorState() {
|
||||
const oldState = this._isErrorState;
|
||||
const control = this._ngControl;
|
||||
const form = this._parentFormGroup || this._parentForm;
|
||||
return control && this.errorStateMatcher(control.control as FormControl, form);
|
||||
const parent = this._parentFormGroup || this._parentForm;
|
||||
const newState = control && this.errorStateMatcher(control.control as FormControl, parent);
|
||||
|
||||
if (newState !== oldState) {
|
||||
this._isErrorState = newState;
|
||||
this._stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
/** Does some manual dirty checking on the native input `value` property. */
|
||||
private _dirtyCheckNativeValue() {
|
||||
const newValue = this.value;
|
||||
|
||||
if (this._previousNativeValue !== newValue) {
|
||||
this._previousNativeValue = newValue;
|
||||
this._stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
/** Make sure the input is a supported type. */
|
||||
private _validateType() {
|
||||
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) {
|
||||
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) > -1) {
|
||||
throw getMdInputContainerUnsupportedTypeError(this._type);
|
||||
}
|
||||
}
|
||||
|
||||
private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) !== -1; }
|
||||
/** Checks whether the input type isn't one of the types that are never empty. */
|
||||
private _isNeverEmpty() {
|
||||
return this._neverEmptyInputTypes.indexOf(this._type) > -1;
|
||||
}
|
||||
|
||||
/** Checks whether the input is invalid based on the native validation. */
|
||||
private _isBadInput() {
|
||||
// The `validity` property won't be present on platform-server.
|
||||
let validity = (this._elementRef.nativeElement as HTMLInputElement).validity;
|
||||
@@ -327,7 +370,7 @@ export class MdInputDirective {
|
||||
// Remove align attribute to prevent it from interfering with layout.
|
||||
'[attr.align]': 'null',
|
||||
'class': 'mat-input-container',
|
||||
'[class.mat-input-invalid]': '_mdInputChild._isErrorState()',
|
||||
'[class.mat-input-invalid]': '_mdInputChild._isErrorState',
|
||||
'[class.mat-focused]': '_mdInputChild.focused',
|
||||
'[class.ng-untouched]': '_shouldForward("untouched")',
|
||||
'[class.ng-touched]': '_shouldForward("touched")',
|
||||
@@ -339,6 +382,7 @@ export class MdInputDirective {
|
||||
'(click)': '_focusInput()',
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
|
||||
export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterContentChecked {
|
||||
@@ -347,7 +391,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
|
||||
/** Color of the input divider, based on the theme. */
|
||||
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
|
||||
|
||||
/** @deprecated Use color instead. */
|
||||
/** @deprecated Use `color` instead. */
|
||||
@Input()
|
||||
get dividerColor() { return this.color; }
|
||||
set dividerColor(value) { this.color = value; }
|
||||
@@ -391,17 +435,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
|
||||
|
||||
/** Reference to the input's underline element. */
|
||||
@ViewChild('underline') underlineRef: ElementRef;
|
||||
|
||||
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
|
||||
|
||||
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
|
||||
|
||||
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
|
||||
|
||||
@ContentChildren(MdHint) _hintChildren: QueryList<MdHint>;
|
||||
|
||||
@ContentChildren(MdPrefix) _prefixChildren: QueryList<MdPrefix>;
|
||||
|
||||
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;
|
||||
|
||||
constructor(
|
||||
@@ -417,15 +455,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
|
||||
this._processHints();
|
||||
this._validatePlaceholders();
|
||||
|
||||
// Re-validate when things change.
|
||||
this._hintChildren.changes.subscribe(() => this._processHints());
|
||||
this._mdInputChild._placeholderChange.subscribe(() => this._validatePlaceholders());
|
||||
// Subscribe to changes in the child input state in order to update the container UI.
|
||||
this._mdInputChild._stateChanges.subscribe(() => {
|
||||
this._validatePlaceholders();
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
// Mark for check when the input's value changes to recalculate whether input is empty
|
||||
const control = this._mdInputChild._ngControl;
|
||||
if (control && control.valueChanges) {
|
||||
control.valueChanges.subscribe(() => this._changeDetectorRef.markForCheck());
|
||||
if (this._mdInputChild._ngControl && this._mdInputChild._ngControl.valueChanges) {
|
||||
this._mdInputChild._ngControl.valueChanges.subscribe(() => {
|
||||
this._changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
// Re-validate when the amount of hints changes.
|
||||
this._hintChildren.changes.subscribe(() => this._processHints());
|
||||
}
|
||||
|
||||
ngAfterContentChecked() {
|
||||
@@ -445,15 +488,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
|
||||
}
|
||||
|
||||
/** Whether the input has a placeholder. */
|
||||
_hasPlaceholder() { return !!(this._mdInputChild.placeholder || this._placeholderChild); }
|
||||
_hasPlaceholder() {
|
||||
return !!(this._mdInputChild.placeholder || this._placeholderChild);
|
||||
}
|
||||
|
||||
/** Focuses the underlying input. */
|
||||
_focusInput() { this._mdInputChild.focus(); }
|
||||
_focusInput() {
|
||||
this._mdInputChild.focus();
|
||||
}
|
||||
|
||||
/** Determines whether to display hints or errors. */
|
||||
_getDisplayedMessages(): 'error' | 'hint' {
|
||||
let input = this._mdInputChild;
|
||||
return (this._errorChildren.length > 0 && input._isErrorState()) ? 'error' : 'hint';
|
||||
return (this._errorChildren.length > 0 && input._isErrorState) ? 'error' : 'hint';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user