@@ -16,16 +16,18 @@ import {
1616 ContentChildren ,
1717 Directive ,
1818 ElementRef ,
19- EventEmitter ,
2019 Input ,
2120 Optional ,
22- Output ,
2321 QueryList ,
2422 Renderer2 ,
2523 Self ,
2624 ViewChild ,
2725 ViewEncapsulation ,
28- Inject
26+ Inject ,
27+ ChangeDetectionStrategy ,
28+ OnChanges ,
29+ OnDestroy ,
30+ DoCheck ,
2931} from '@angular/core' ;
3032import { animate , state , style , transition , trigger } from '@angular/animations' ;
3133import { coerceBooleanProperty , Platform } from '../core' ;
@@ -48,6 +50,7 @@ import {
4850 ErrorOptions ,
4951 MD_ERROR_GLOBAL_OPTIONS
5052} from '../core/error/error-options' ;
53+ import { Subject } from 'rxjs/Subject' ;
5154
5255// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
5356const MD_INPUT_INVALID_TYPES = [
@@ -128,13 +131,13 @@ export class MdSuffix {}
128131 '[disabled]' : 'disabled' ,
129132 '[required]' : 'required' ,
130133 '[attr.aria-describedby]' : 'ariaDescribedby || null' ,
131- '[attr.aria-invalid]' : '_isErrorState() ' ,
132- '(blur)' : '_onBlur( )' ,
133- '(focus)' : '_onFocus( )' ,
134+ '[attr.aria-invalid]' : '_isErrorState' ,
135+ '(blur)' : '_focusChanged(false )' ,
136+ '(focus)' : '_focusChanged(true )' ,
134137 '(input)' : '_onInput()' ,
135138 }
136139} )
137- export class MdInputDirective {
140+ export class MdInputDirective implements OnChanges , OnDestroy , DoCheck {
138141
139142 /** Variables used as cache for getters and setters. */
140143 private _type = 'text' ;
@@ -143,39 +146,37 @@ export class MdInputDirective {
143146 private _required = false ;
144147 private _readonly = false ;
145148 private _id : string ;
146- private _cachedUid : string ;
149+ private _uid = `md-input- ${ nextUniqueId ++ } ` ;
147150 private _errorOptions : ErrorOptions ;
151+ private _previousNativeValue = this . value ;
152+
153+ /** Whether the input is in an error state. */
154+ _isErrorState = false ;
148155
149156 /** Whether the element is focused or not. */
150157 focused = false ;
151158
152159 /** Sets the aria-describedby attribute on the input for improved a11y. */
153160 ariaDescribedby : string ;
154161
162+ /**
163+ * Stream that emits whenever the state of the input changes. This allows for other components
164+ * (mostly `md-input-container`) that depend on the properties of `mdInput` to update their view.
165+ */
166+ _stateChanges = new Subject < void > ( ) ;
167+
155168 /** Whether the element is disabled. */
156169 @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- }
170+ get disabled ( ) { return this . _ngControl ? this . _ngControl . disabled : this . _disabled ; }
171+ set disabled ( value : any ) { this . _disabled = coerceBooleanProperty ( value ) ; }
164172
165173 /** Unique id of the element. */
166174 @Input ( )
167175 get id ( ) { return this . _id ; }
168- set id ( value : string ) { this . _id = value || this . _uid ; }
176+ set id ( value : string ) { this . _id = value || this . _uid ; }
169177
170178 /** 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- }
179+ @Input ( ) placeholder : string = '' ;
179180
180181 /** Whether the element is required. */
181182 @Input ( )
@@ -209,11 +210,6 @@ export class MdInputDirective {
209210 get value ( ) { return this . _elementRef . nativeElement . value ; }
210211 set value ( value : string ) { this . _elementRef . nativeElement . value = value ; }
211212
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-
217213 /** Whether the input is empty. */
218214 get empty ( ) {
219215 return ! this . _isNeverEmpty ( ) &&
@@ -224,8 +220,6 @@ export class MdInputDirective {
224220 ! this . _isBadInput ( ) ;
225221 }
226222
227- private get _uid ( ) { return this . _cachedUid = this . _cachedUid || `md-input-${ nextUniqueId ++ } ` ; }
228-
229223 private _neverEmptyInputTypes = [
230224 'date' ,
231225 'datetime' ,
@@ -238,28 +232,57 @@ export class MdInputDirective {
238232 constructor ( private _elementRef : ElementRef ,
239233 private _renderer : Renderer2 ,
240234 private _platform : Platform ,
235+ private _changeDetectorRef : ChangeDetectorRef ,
241236 @Optional ( ) @Self ( ) public _ngControl : NgControl ,
242237 @Optional ( ) private _parentForm : NgForm ,
243238 @Optional ( ) private _parentFormGroup : FormGroupDirective ,
244239 @Optional ( ) @Inject ( MD_ERROR_GLOBAL_OPTIONS ) errorOptions : ErrorOptions ) {
245240
246241 // Force setter to be called in case id was not specified.
247242 this . id = this . id ;
248-
249243 this . _errorOptions = errorOptions ? errorOptions : { } ;
250244 this . errorStateMatcher = this . _errorOptions . errorStateMatcher || defaultErrorStateMatcher ;
251245 }
252246
253- /** Focuses the input element. */
254- focus ( ) { this . _elementRef . nativeElement . focus ( ) ; }
247+ ngOnChanges ( ) {
248+ this . _stateChanges . next ( ) ;
249+ }
250+
251+ ngOnDestroy ( ) {
252+ this . _stateChanges . complete ( ) ;
253+ }
254+
255+ ngDoCheck ( ) {
256+ if ( this . _ngControl ) {
257+ // We need to re-evaluate this on every change detection cycle, because there are some
258+ // error triggers that we can't subscribe to (e.g. parent form submissions). This means
259+ // that whatever logic is in here has to be super lean or we risk destroying the performance.
260+ this . _updateErrorState ( ) ;
261+ } else {
262+ // When the input isn't used together with `@angular/forms`, we need to check manually for
263+ // changes to the native `value` property in order to update the floating label.
264+ this . _dirtyCheckNativeValue ( ) ;
265+ }
266+ }
255267
256268 _onFocus ( ) {
257269 if ( ! this . _readonly ) {
258270 this . focused = true ;
259271 }
260272 }
261273
262- _onBlur ( ) { this . focused = false ; }
274+ /** Focuses the input element. */
275+ focus ( ) {
276+ this . _elementRef . nativeElement . focus ( ) ;
277+ }
278+
279+ /** Callback for the cases where the focused state of the input changes. */
280+ _focusChanged ( isFocused : boolean ) {
281+ if ( isFocused !== this . focused ) {
282+ this . focused = isFocused ;
283+ this . _stateChanges . next ( ) ;
284+ }
285+ }
263286
264287 _onInput ( ) {
265288 // This is a noop function and is used to let Angular know whenever the value changes.
@@ -271,22 +294,42 @@ export class MdInputDirective {
271294 // FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
272295 }
273296
274- /** Whether the input is in an error state. */
275- _isErrorState ( ) : boolean {
297+ /** Re-evaluates the error state. This is only relevant with @angular/forms. */
298+ private _updateErrorState ( ) {
299+ const oldState = this . _isErrorState ;
276300 const control = this . _ngControl ;
277- const form = this . _parentFormGroup || this . _parentForm ;
278- return control && this . errorStateMatcher ( control . control as FormControl , form ) ;
301+ const parent = this . _parentFormGroup || this . _parentForm ;
302+ const newState = control && this . errorStateMatcher ( control . control as FormControl , parent ) ;
303+
304+ if ( newState !== oldState ) {
305+ this . _isErrorState = newState ;
306+ this . _stateChanges . next ( ) ;
307+ }
308+ }
309+
310+ /** Does some manual dirty checking on the native input `value` property. */
311+ private _dirtyCheckNativeValue ( ) {
312+ const newValue = this . value ;
313+
314+ if ( this . _previousNativeValue !== newValue ) {
315+ this . _previousNativeValue = newValue ;
316+ this . _stateChanges . next ( ) ;
317+ }
279318 }
280319
281320 /** Make sure the input is a supported type. */
282321 private _validateType ( ) {
283- if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) !== - 1 ) {
322+ if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) > - 1 ) {
284323 throw getMdInputContainerUnsupportedTypeError ( this . _type ) ;
285324 }
286325 }
287326
288- private _isNeverEmpty ( ) { return this . _neverEmptyInputTypes . indexOf ( this . _type ) !== - 1 ; }
327+ /** Checks whether the input type isn't one of the types that are never empty. */
328+ private _isNeverEmpty ( ) {
329+ return this . _neverEmptyInputTypes . indexOf ( this . _type ) > - 1 ;
330+ }
289331
332+ /** Checks whether the input is invalid based on the native validation. */
290333 private _isBadInput ( ) {
291334 // The `validity` property won't be present on platform-server.
292335 let validity = ( this . _elementRef . nativeElement as HTMLInputElement ) . validity ;
@@ -327,7 +370,7 @@ export class MdInputDirective {
327370 // Remove align attribute to prevent it from interfering with layout.
328371 '[attr.align]' : 'null' ,
329372 'class' : 'mat-input-container' ,
330- '[class.mat-input-invalid]' : '_mdInputChild._isErrorState() ' ,
373+ '[class.mat-input-invalid]' : '_mdInputChild._isErrorState' ,
331374 '[class.mat-focused]' : '_mdInputChild.focused' ,
332375 '[class.ng-untouched]' : '_shouldForward("untouched")' ,
333376 '[class.ng-touched]' : '_shouldForward("touched")' ,
@@ -339,6 +382,7 @@ export class MdInputDirective {
339382 '(click)' : '_focusInput()' ,
340383 } ,
341384 encapsulation : ViewEncapsulation . None ,
385+ changeDetection : ChangeDetectionStrategy . OnPush ,
342386} )
343387
344388export class MdInputContainer implements AfterViewInit , AfterContentInit , AfterContentChecked {
@@ -347,7 +391,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
347391 /** Color of the input divider, based on the theme. */
348392 @Input ( ) color : 'primary' | 'accent' | 'warn' = 'primary' ;
349393
350- /** @deprecated Use color instead. */
394+ /** @deprecated Use ` color` instead. */
351395 @Input ( )
352396 get dividerColor ( ) { return this . color ; }
353397 set dividerColor ( value ) { this . color = value ; }
@@ -391,17 +435,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
391435
392436 /** Reference to the input's underline element. */
393437 @ViewChild ( 'underline' ) underlineRef : ElementRef ;
394-
395438 @ContentChild ( MdInputDirective ) _mdInputChild : MdInputDirective ;
396-
397439 @ContentChild ( MdPlaceholder ) _placeholderChild : MdPlaceholder ;
398-
399440 @ContentChildren ( MdErrorDirective ) _errorChildren : QueryList < MdErrorDirective > ;
400-
401441 @ContentChildren ( MdHint ) _hintChildren : QueryList < MdHint > ;
402-
403442 @ContentChildren ( MdPrefix ) _prefixChildren : QueryList < MdPrefix > ;
404-
405443 @ContentChildren ( MdSuffix ) _suffixChildren : QueryList < MdSuffix > ;
406444
407445 constructor (
@@ -417,15 +455,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
417455 this . _processHints ( ) ;
418456 this . _validatePlaceholders ( ) ;
419457
420- // Re-validate when things change.
421- this . _hintChildren . changes . subscribe ( ( ) => this . _processHints ( ) ) ;
422- this . _mdInputChild . _placeholderChange . subscribe ( ( ) => this . _validatePlaceholders ( ) ) ;
458+ // Subscribe to changes in the child input state in order to update the container UI.
459+ this . _mdInputChild . _stateChanges . subscribe ( ( ) => {
460+ this . _validatePlaceholders ( ) ;
461+ this . _changeDetectorRef . markForCheck ( ) ;
462+ } ) ;
423463
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 ( ) ) ;
464+ if ( this . _mdInputChild . _ngControl && this . _mdInputChild . _ngControl . valueChanges ) {
465+ this . _mdInputChild . _ngControl . valueChanges . subscribe ( ( ) => {
466+ this . _changeDetectorRef . markForCheck ( ) ;
467+ } ) ;
428468 }
469+
470+ // Re-validate when the amount of hints changes.
471+ this . _hintChildren . changes . subscribe ( ( ) => this . _processHints ( ) ) ;
429472 }
430473
431474 ngAfterContentChecked ( ) {
@@ -445,15 +488,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
445488 }
446489
447490 /** Whether the input has a placeholder. */
448- _hasPlaceholder ( ) { return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ; }
491+ _hasPlaceholder ( ) {
492+ return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ;
493+ }
449494
450495 /** Focuses the underlying input. */
451- _focusInput ( ) { this . _mdInputChild . focus ( ) ; }
496+ _focusInput ( ) {
497+ this . _mdInputChild . focus ( ) ;
498+ }
452499
453500 /** Determines whether to display hints or errors. */
454501 _getDisplayedMessages ( ) : 'error' | 'hint' {
455502 let input = this . _mdInputChild ;
456- return ( this . _errorChildren . length > 0 && input . _isErrorState ( ) ) ? 'error' : 'hint' ;
503+ return ( this . _errorChildren . length > 0 && input . _isErrorState ) ? 'error' : 'hint' ;
457504 }
458505
459506 /**
0 commit comments