@@ -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' ;
3034import { animate , state , style , transition , trigger } from '@angular/animations' ;
3135import { 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.
5359const MD_INPUT_INVALID_TYPES = [
@@ -128,53 +134,51 @@ 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' ;
141147 private _placeholder : string = '' ;
142148 private _disabled = false ;
143149 private _required = false ;
144150 private _id : string ;
145- private _cachedUid : string ;
151+ private _uid = `md-input- ${ nextUniqueId ++ } ` ;
146152 private _errorOptions : ErrorOptions ;
153+ private _previousNativeValue = this . value ;
154+
155+ /** Whether the input is in an error state. */
156+ _isErrorState = false ;
147157
148158 /** Whether the element is focused or not. */
149159 focused = false ;
150160
151161 /** Sets the aria-describedby attribute on the input for improved a11y. */
152162 ariaDescribedby : string ;
153163
164+ /**
165+ * Stream that emits whenever the state of the input changes. This allows for other components
166+ * (mostly `md-input-container`) that depend on the properties of `mdInput` to update their view.
167+ */
168+ _stateChanges = new Subject < void > ( ) ;
169+
154170 /** Whether the element is disabled. */
155171 @Input ( )
156- get disabled ( ) {
157- return this . _ngControl ? this . _ngControl . disabled : this . _disabled ;
158- }
159-
160- set disabled ( value : any ) {
161- this . _disabled = coerceBooleanProperty ( value ) ;
162- }
172+ get disabled ( ) { return this . _ngControl ? this . _ngControl . disabled : this . _disabled ; }
173+ set disabled ( value : any ) { this . _disabled = coerceBooleanProperty ( value ) ; }
163174
164175 /** Unique id of the element. */
165176 @Input ( )
166177 get id ( ) { return this . _id ; }
167- set id ( value : string ) { this . _id = value || this . _uid ; }
178+ set id ( value : string ) { this . _id = value || this . _uid ; }
168179
169180 /** Placeholder attribute of the element. */
170- @Input ( )
171- get placeholder ( ) { return this . _placeholder ; }
172- set placeholder ( value : string ) {
173- if ( this . _placeholder !== value ) {
174- this . _placeholder = value ;
175- this . _placeholderChange . emit ( this . _placeholder ) ;
176- }
177- }
181+ @Input ( ) placeholder : string = '' ;
178182
179183 /** Whether the element is required. */
180184 @Input ( )
@@ -203,11 +207,6 @@ export class MdInputDirective {
203207 get value ( ) { return this . _elementRef . nativeElement . value ; }
204208 set value ( value : string ) { this . _elementRef . nativeElement . value = value ; }
205209
206- /**
207- * Emits an event when the placeholder changes so that the `md-input-container` can re-validate.
208- */
209- @Output ( ) _placeholderChange = new EventEmitter < string > ( ) ;
210-
211210 /** Whether the input is empty. */
212211 get empty ( ) {
213212 return ! this . _isNeverEmpty ( ) &&
@@ -218,8 +217,6 @@ export class MdInputDirective {
218217 ! this . _isBadInput ( ) ;
219218 }
220219
221- private get _uid ( ) { return this . _cachedUid = this . _cachedUid || `md-input-${ nextUniqueId ++ } ` ; }
222-
223220 private _neverEmptyInputTypes = [
224221 'date' ,
225222 'datetime' ,
@@ -232,24 +229,51 @@ export class MdInputDirective {
232229 constructor ( private _elementRef : ElementRef ,
233230 private _renderer : Renderer2 ,
234231 private _platform : Platform ,
232+ private _changeDetectorRef : ChangeDetectorRef ,
235233 @Optional ( ) @Self ( ) public _ngControl : NgControl ,
236234 @Optional ( ) private _parentForm : NgForm ,
237235 @Optional ( ) private _parentFormGroup : FormGroupDirective ,
238236 @Optional ( ) @Inject ( MD_ERROR_GLOBAL_OPTIONS ) errorOptions : ErrorOptions ) {
239237
240238 // Force setter to be called in case id was not specified.
241239 this . id = this . id ;
242-
243240 this . _errorOptions = errorOptions ? errorOptions : { } ;
244241 this . errorStateMatcher = this . _errorOptions . errorStateMatcher || defaultErrorStateMatcher ;
245242 }
246243
247- /** Focuses the input element. */
248- focus ( ) { this . _elementRef . nativeElement . focus ( ) ; }
244+ ngOnChanges ( ) {
245+ this . _stateChanges . next ( ) ;
246+ }
249247
250- _onFocus ( ) { this . focused = true ; }
248+ ngOnDestroy ( ) {
249+ this . _stateChanges . complete ( ) ;
250+ }
251+
252+ ngDoCheck ( ) {
253+ if ( this . _ngControl ) {
254+ // We need to re-evaluate this on every change detection cycle, because there are some
255+ // error triggers that we can't subscribe to (e.g. parent form submissions). This means
256+ // that whatever logic is in here has to be super lean or we risk destroying the performance.
257+ this . _updateErrorState ( ) ;
258+ } else {
259+ // When the input isn't used together with `@angular/forms`, we need to check manually for
260+ // changes to the native `value` property in order to update the floating label.
261+ this . _dirtyCheckNativeValue ( ) ;
262+ }
263+ }
251264
252- _onBlur ( ) { this . focused = false ; }
265+ /** Focuses the input element. */
266+ focus ( ) {
267+ this . _elementRef . nativeElement . focus ( ) ;
268+ }
269+
270+ /** Callback for the cases where the focused state of the input changes. */
271+ _focusChanged ( isFocused : boolean ) {
272+ if ( isFocused !== this . focused ) {
273+ this . focused = isFocused ;
274+ this . _stateChanges . next ( ) ;
275+ }
276+ }
253277
254278 _onInput ( ) {
255279 // This is a noop function and is used to let Angular know whenever the value changes.
@@ -261,22 +285,42 @@ export class MdInputDirective {
261285 // FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
262286 }
263287
264- /** Whether the input is in an error state. */
265- _isErrorState ( ) : boolean {
288+ /** Re-evaluates the error state. This is only relevant with @angular/forms. */
289+ private _updateErrorState ( ) {
290+ const oldState = this . _isErrorState ;
266291 const control = this . _ngControl ;
267- const form = this . _parentFormGroup || this . _parentForm ;
268- return control && this . errorStateMatcher ( control . control as FormControl , form ) ;
292+ const parent = this . _parentFormGroup || this . _parentForm ;
293+ const newState = control && this . errorStateMatcher ( control . control as FormControl , parent ) ;
294+
295+ if ( newState !== oldState ) {
296+ this . _isErrorState = newState ;
297+ this . _stateChanges . next ( ) ;
298+ }
299+ }
300+
301+ /** Does some manual dirty checking on the native input `value` property. */
302+ private _dirtyCheckNativeValue ( ) {
303+ const newValue = this . value ;
304+
305+ if ( this . _previousNativeValue !== newValue ) {
306+ this . _previousNativeValue = newValue ;
307+ this . _stateChanges . next ( ) ;
308+ }
269309 }
270310
271311 /** Make sure the input is a supported type. */
272312 private _validateType ( ) {
273- if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) !== - 1 ) {
313+ if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) > - 1 ) {
274314 throw getMdInputContainerUnsupportedTypeError ( this . _type ) ;
275315 }
276316 }
277317
278- private _isNeverEmpty ( ) { return this . _neverEmptyInputTypes . indexOf ( this . _type ) !== - 1 ; }
318+ /** Checks whether the input type isn't one of the types that are never empty. */
319+ private _isNeverEmpty ( ) {
320+ return this . _neverEmptyInputTypes . indexOf ( this . _type ) > - 1 ;
321+ }
279322
323+ /** Checks whether the input is invalid based on the native validation. */
280324 private _isBadInput ( ) {
281325 // The `validity` property won't be present on platform-server.
282326 let validity = ( this . _elementRef . nativeElement as HTMLInputElement ) . validity ;
@@ -317,7 +361,7 @@ export class MdInputDirective {
317361 // Remove align attribute to prevent it from interfering with layout.
318362 '[attr.align]' : 'null' ,
319363 'class' : 'mat-input-container' ,
320- '[class.mat-input-invalid]' : '_mdInputChild._isErrorState() ' ,
364+ '[class.mat-input-invalid]' : '_mdInputChild._isErrorState' ,
321365 '[class.mat-focused]' : '_mdInputChild.focused' ,
322366 '[class.ng-untouched]' : '_shouldForward("untouched")' ,
323367 '[class.ng-touched]' : '_shouldForward("touched")' ,
@@ -329,6 +373,7 @@ export class MdInputDirective {
329373 '(click)' : '_focusInput()' ,
330374 } ,
331375 encapsulation : ViewEncapsulation . None ,
376+ changeDetection : ChangeDetectionStrategy . OnPush ,
332377} )
333378
334379export class MdInputContainer implements AfterViewInit , AfterContentInit , AfterContentChecked {
@@ -337,7 +382,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
337382 /** Color of the input divider, based on the theme. */
338383 @Input ( ) color : 'primary' | 'accent' | 'warn' = 'primary' ;
339384
340- /** @deprecated Use color instead. */
385+ /** @deprecated Use ` color` instead. */
341386 @Input ( )
342387 get dividerColor ( ) { return this . color ; }
343388 set dividerColor ( value ) { this . color = value ; }
@@ -381,17 +426,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
381426
382427 /** Reference to the input's underline element. */
383428 @ViewChild ( 'underline' ) underlineRef : ElementRef ;
384-
385429 @ContentChild ( MdInputDirective ) _mdInputChild : MdInputDirective ;
386-
387430 @ContentChild ( MdPlaceholder ) _placeholderChild : MdPlaceholder ;
388-
389431 @ContentChildren ( MdErrorDirective ) _errorChildren : QueryList < MdErrorDirective > ;
390-
391432 @ContentChildren ( MdHint ) _hintChildren : QueryList < MdHint > ;
392-
393433 @ContentChildren ( MdPrefix ) _prefixChildren : QueryList < MdPrefix > ;
394-
395434 @ContentChildren ( MdSuffix ) _suffixChildren : QueryList < MdSuffix > ;
396435
397436 constructor (
@@ -407,9 +446,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
407446 this . _processHints ( ) ;
408447 this . _validatePlaceholders ( ) ;
409448
410- // Re-validate when things change.
449+ // Subscribe to changes in the child input state in order to update the container UI.
450+ this . _mdInputChild . _stateChanges . subscribe ( ( ) => {
451+ this . _validatePlaceholders ( ) ;
452+ this . _changeDetectorRef . markForCheck ( ) ;
453+ } ) ;
454+
455+ if ( this . _mdInputChild . _ngControl && this . _mdInputChild . _ngControl . valueChanges ) {
456+ this . _mdInputChild . _ngControl . valueChanges . subscribe ( ( ) => {
457+ this . _changeDetectorRef . markForCheck ( ) ;
458+ } ) ;
459+ }
460+
461+ // Re-validate when the amount of hints changes.
411462 this . _hintChildren . changes . subscribe ( ( ) => this . _processHints ( ) ) ;
412- this . _mdInputChild . _placeholderChange . subscribe ( ( ) => this . _validatePlaceholders ( ) ) ;
413463 }
414464
415465 ngAfterContentChecked ( ) {
@@ -429,15 +479,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
429479 }
430480
431481 /** Whether the input has a placeholder. */
432- _hasPlaceholder ( ) { return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ; }
482+ _hasPlaceholder ( ) {
483+ return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ;
484+ }
433485
434486 /** Focuses the underlying input. */
435- _focusInput ( ) { this . _mdInputChild . focus ( ) ; }
487+ _focusInput ( ) {
488+ this . _mdInputChild . focus ( ) ;
489+ }
436490
437491 /** Determines whether to display hints or errors. */
438492 _getDisplayedMessages ( ) : 'error' | 'hint' {
439493 let input = this . _mdInputChild ;
440- return ( this . _errorChildren . length > 0 && input . _isErrorState ( ) ) ? 'error' : 'hint' ;
494+ return ( this . _errorChildren . length > 0 && input . _isErrorState ) ? 'error' : 'hint' ;
441495 }
442496
443497 /**
0 commit comments