Skip to content

Commit 6baac11

Browse files
crisbetoandrewseguin
authored andcommitted
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.
1 parent 374aaff commit 6baac11

File tree

1 file changed

+107
-57
lines changed

1 file changed

+107
-57
lines changed

src/lib/input/input-container.ts

Lines changed: 107 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import {
2525
Self,
2626
ViewChild,
2727
ViewEncapsulation,
28-
Inject
28+
Inject,
29+
ChangeDetectionStrategy,
30+
OnChanges,
31+
OnDestroy,
32+
DoCheck,
2933
} from '@angular/core';
3034
import {animate, state, style, transition, trigger} from '@angular/animations';
3135
import {coerceBooleanProperty, Platform} from '../core';
@@ -48,6 +52,8 @@ import {
4852
ErrorOptions,
4953
MD_ERROR_GLOBAL_OPTIONS
5054
} from '../core/error/error-options';
55+
import {Subject} from 'rxjs/Subject';
56+
import {filter} from '../core/rxjs/index';
5157

5258
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
5359
const MD_INPUT_INVALID_TYPES = [
@@ -128,13 +134,13 @@ export class MdSuffix {}
128134
'[disabled]': 'disabled',
129135
'[required]': 'required',
130136
'[attr.aria-describedby]': 'ariaDescribedby || null',
131-
'[attr.aria-invalid]': '_isErrorState()',
132-
'(blur)': '_onBlur()',
133-
'(focus)': '_onFocus()',
137+
'[attr.aria-invalid]': '_isErrorState',
138+
'(blur)': '_focusChanged(false)',
139+
'(focus)': '_focusChanged(true)',
134140
'(input)': '_onInput()',
135141
}
136142
})
137-
export class MdInputDirective {
143+
export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {
138144

139145
/** Variables used as cache for getters and setters. */
140146
private _type = 'text';
@@ -143,39 +149,37 @@ export class MdInputDirective {
143149
private _required = false;
144150
private _readonly = false;
145151
private _id: string;
146-
private _cachedUid: string;
152+
private _uid = `md-input-${nextUniqueId++}`;
147153
private _errorOptions: ErrorOptions;
154+
private _previousNativeValue = this.value;
155+
156+
/** Whether the input is in an error state. */
157+
_isErrorState = false;
148158

149159
/** Whether the element is focused or not. */
150160
focused = false;
151161

152162
/** Sets the aria-describedby attribute on the input for improved a11y. */
153163
ariaDescribedby: string;
154164

165+
/**
166+
* Stream that emits whenever the state of the input changes. This allows for other components
167+
* (mostly `md-input-container`) that depend on the properties of `mdInput` to update their view.
168+
*/
169+
_stateChanges = new Subject<void>();
170+
155171
/** Whether the element is disabled. */
156172
@Input()
157-
get disabled() {
158-
return this._ngControl ? this._ngControl.disabled : this._disabled;
159-
}
160-
161-
set disabled(value: any) {
162-
this._disabled = coerceBooleanProperty(value);
163-
}
173+
get disabled() { return this._ngControl ? this._ngControl.disabled : this._disabled; }
174+
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
164175

165176
/** Unique id of the element. */
166177
@Input()
167178
get id() { return this._id; }
168-
set id(value: string) {this._id = value || this._uid; }
179+
set id(value: string) { this._id = value || this._uid; }
169180

170181
/** Placeholder attribute of the element. */
171-
@Input()
172-
get placeholder() { return this._placeholder; }
173-
set placeholder(value: string) {
174-
if (this._placeholder !== value) {
175-
this._placeholder = value;
176-
this._placeholderChange.emit(this._placeholder);
177-
}
178-
}
182+
@Input() placeholder: string = '';
179183

180184
/** Whether the element is required. */
181185
@Input()
@@ -209,11 +213,6 @@ export class MdInputDirective {
209213
get value() { return this._elementRef.nativeElement.value; }
210214
set value(value: string) { this._elementRef.nativeElement.value = value; }
211215

212-
/**
213-
* Emits an event when the placeholder changes so that the `md-input-container` can re-validate.
214-
*/
215-
@Output() _placeholderChange = new EventEmitter<string>();
216-
217216
/** Whether the input is empty. */
218217
get empty() {
219218
return !this._isNeverEmpty() &&
@@ -224,8 +223,6 @@ export class MdInputDirective {
224223
!this._isBadInput();
225224
}
226225

227-
private get _uid() { return this._cachedUid = this._cachedUid || `md-input-${nextUniqueId++}`; }
228-
229226
private _neverEmptyInputTypes = [
230227
'date',
231228
'datetime',
@@ -238,28 +235,57 @@ export class MdInputDirective {
238235
constructor(private _elementRef: ElementRef,
239236
private _renderer: Renderer2,
240237
private _platform: Platform,
238+
private _changeDetectorRef: ChangeDetectorRef,
241239
@Optional() @Self() public _ngControl: NgControl,
242240
@Optional() private _parentForm: NgForm,
243241
@Optional() private _parentFormGroup: FormGroupDirective,
244242
@Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
245243

246244
// Force setter to be called in case id was not specified.
247245
this.id = this.id;
248-
249246
this._errorOptions = errorOptions ? errorOptions : {};
250247
this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
251248
}
252249

253-
/** Focuses the input element. */
254-
focus() { this._elementRef.nativeElement.focus(); }
250+
ngOnChanges() {
251+
this._stateChanges.next();
252+
}
253+
254+
ngOnDestroy() {
255+
this._stateChanges.complete();
256+
}
257+
258+
ngDoCheck() {
259+
if (this._ngControl) {
260+
// We need to re-evaluate this on every change detection cycle, because there are some
261+
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
262+
// that whatever logic is in here has to be super lean or we risk destroying the performance.
263+
this._updateErrorState();
264+
} else {
265+
// When the input isn't used together with `@angular/forms`, we need to check manually for
266+
// changes to the native `value` property in order to update the floating label.
267+
this._dirtyCheckNativeValue();
268+
}
269+
}
255270

256271
_onFocus() {
257272
if (!this._readonly) {
258273
this.focused = true;
259274
}
260275
}
261276

262-
_onBlur() { this.focused = false; }
277+
/** Focuses the input element. */
278+
focus() {
279+
this._elementRef.nativeElement.focus();
280+
}
281+
282+
/** Callback for the cases where the focused state of the input changes. */
283+
_focusChanged(isFocused: boolean) {
284+
if (isFocused !== this.focused) {
285+
this.focused = isFocused;
286+
this._stateChanges.next();
287+
}
288+
}
263289

264290
_onInput() {
265291
// This is a noop function and is used to let Angular know whenever the value changes.
@@ -271,22 +297,42 @@ export class MdInputDirective {
271297
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
272298
}
273299

274-
/** Whether the input is in an error state. */
275-
_isErrorState(): boolean {
300+
/** Re-evaluates the error state. This is only relevant with @angular/forms. */
301+
private _updateErrorState() {
302+
const oldState = this._isErrorState;
276303
const control = this._ngControl;
277-
const form = this._parentFormGroup || this._parentForm;
278-
return control && this.errorStateMatcher(control.control as FormControl, form);
304+
const parent = this._parentFormGroup || this._parentForm;
305+
const newState = control && this.errorStateMatcher(control.control as FormControl, parent);
306+
307+
if (newState !== oldState) {
308+
this._isErrorState = newState;
309+
this._stateChanges.next();
310+
}
311+
}
312+
313+
/** Does some manual dirty checking on the native input `value` property. */
314+
private _dirtyCheckNativeValue() {
315+
const newValue = this.value;
316+
317+
if (this._previousNativeValue !== newValue) {
318+
this._previousNativeValue = newValue;
319+
this._stateChanges.next();
320+
}
279321
}
280322

281323
/** Make sure the input is a supported type. */
282324
private _validateType() {
283-
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) {
325+
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) > -1) {
284326
throw getMdInputContainerUnsupportedTypeError(this._type);
285327
}
286328
}
287329

288-
private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) !== -1; }
330+
/** Checks whether the input type isn't one of the types that are never empty. */
331+
private _isNeverEmpty() {
332+
return this._neverEmptyInputTypes.indexOf(this._type) > -1;
333+
}
289334

335+
/** Checks whether the input is invalid based on the native validation. */
290336
private _isBadInput() {
291337
// The `validity` property won't be present on platform-server.
292338
let validity = (this._elementRef.nativeElement as HTMLInputElement).validity;
@@ -327,7 +373,7 @@ export class MdInputDirective {
327373
// Remove align attribute to prevent it from interfering with layout.
328374
'[attr.align]': 'null',
329375
'class': 'mat-input-container',
330-
'[class.mat-input-invalid]': '_mdInputChild._isErrorState()',
376+
'[class.mat-input-invalid]': '_mdInputChild._isErrorState',
331377
'[class.mat-focused]': '_mdInputChild.focused',
332378
'[class.ng-untouched]': '_shouldForward("untouched")',
333379
'[class.ng-touched]': '_shouldForward("touched")',
@@ -339,6 +385,7 @@ export class MdInputDirective {
339385
'(click)': '_focusInput()',
340386
},
341387
encapsulation: ViewEncapsulation.None,
388+
changeDetection: ChangeDetectionStrategy.OnPush,
342389
})
343390

344391
export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterContentChecked {
@@ -347,7 +394,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
347394
/** Color of the input divider, based on the theme. */
348395
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
349396

350-
/** @deprecated Use color instead. */
397+
/** @deprecated Use `color` instead. */
351398
@Input()
352399
get dividerColor() { return this.color; }
353400
set dividerColor(value) { this.color = value; }
@@ -391,17 +438,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
391438

392439
/** Reference to the input's underline element. */
393440
@ViewChild('underline') underlineRef: ElementRef;
394-
395441
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
396-
397442
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
398-
399443
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
400-
401444
@ContentChildren(MdHint) _hintChildren: QueryList<MdHint>;
402-
403445
@ContentChildren(MdPrefix) _prefixChildren: QueryList<MdPrefix>;
404-
405446
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;
406447

407448
constructor(
@@ -417,15 +458,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
417458
this._processHints();
418459
this._validatePlaceholders();
419460

420-
// Re-validate when things change.
421-
this._hintChildren.changes.subscribe(() => this._processHints());
422-
this._mdInputChild._placeholderChange.subscribe(() => this._validatePlaceholders());
461+
// Subscribe to changes in the child input state in order to update the container UI.
462+
this._mdInputChild._stateChanges.subscribe(() => {
463+
this._validatePlaceholders();
464+
this._changeDetectorRef.markForCheck();
465+
});
423466

424-
// Mark for check when the input's value changes to recalculate whether input is empty
425-
const control = this._mdInputChild._ngControl;
426-
if (control && control.valueChanges) {
427-
control.valueChanges.subscribe(() => this._changeDetectorRef.markForCheck());
467+
if (this._mdInputChild._ngControl && this._mdInputChild._ngControl.valueChanges) {
468+
this._mdInputChild._ngControl.valueChanges.subscribe(() => {
469+
this._changeDetectorRef.markForCheck();
470+
});
428471
}
472+
473+
// Re-validate when the amount of hints changes.
474+
this._hintChildren.changes.subscribe(() => this._processHints());
429475
}
430476

431477
ngAfterContentChecked() {
@@ -445,15 +491,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
445491
}
446492

447493
/** Whether the input has a placeholder. */
448-
_hasPlaceholder() { return !!(this._mdInputChild.placeholder || this._placeholderChild); }
494+
_hasPlaceholder() {
495+
return !!(this._mdInputChild.placeholder || this._placeholderChild);
496+
}
449497

450498
/** Focuses the underlying input. */
451-
_focusInput() { this._mdInputChild.focus(); }
499+
_focusInput() {
500+
this._mdInputChild.focus();
501+
}
452502

453503
/** Determines whether to display hints or errors. */
454504
_getDisplayedMessages(): 'error' | 'hint' {
455505
let input = this._mdInputChild;
456-
return (this._errorChildren.length > 0 && input._isErrorState()) ? 'error' : 'hint';
506+
return (this._errorChildren.length > 0 && input._isErrorState) ? 'error' : 'hint';
457507
}
458508

459509
/**

0 commit comments

Comments
 (0)