@@ -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,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
344391export 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