@@ -25,7 +25,11 @@ import {
25
25
Self ,
26
26
ViewChild ,
27
27
ViewEncapsulation ,
28
- Inject
28
+ Inject ,
29
+ ChangeDetectionStrategy ,
30
+ OnChanges ,
31
+ OnDestroy ,
32
+ DoCheck ,
29
33
} from '@angular/core' ;
30
34
import { animate , state , style , transition , trigger } from '@angular/animations' ;
31
35
import { coerceBooleanProperty , Platform } from '../core' ;
@@ -48,6 +52,8 @@ import {
48
52
ErrorOptions ,
49
53
MD_ERROR_GLOBAL_OPTIONS
50
54
} from '../core/error/error-options' ;
55
+ import { Subject } from 'rxjs/Subject' ;
56
+ import { filter } from '../core/rxjs/index' ;
51
57
52
58
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
53
59
const MD_INPUT_INVALID_TYPES = [
@@ -128,53 +134,51 @@ export class MdSuffix {}
128
134
'[disabled]' : 'disabled' ,
129
135
'[required]' : 'required' ,
130
136
'[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 )' ,
134
140
'(input)' : '_onInput()' ,
135
141
}
136
142
} )
137
- export class MdInputDirective {
143
+ export class MdInputDirective implements OnChanges , OnDestroy , DoCheck {
138
144
139
145
/** Variables used as cache for getters and setters. */
140
146
private _type = 'text' ;
141
147
private _placeholder : string = '' ;
142
148
private _disabled = false ;
143
149
private _required = false ;
144
150
private _id : string ;
145
- private _cachedUid : string ;
151
+ private _uid = `md-input- ${ nextUniqueId ++ } ` ;
146
152
private _errorOptions : ErrorOptions ;
153
+ private _previousNativeValue = this . value ;
154
+
155
+ /** Whether the input is in an error state. */
156
+ _isErrorState = false ;
147
157
148
158
/** Whether the element is focused or not. */
149
159
focused = false ;
150
160
151
161
/** Sets the aria-describedby attribute on the input for improved a11y. */
152
162
ariaDescribedby : string ;
153
163
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
+
154
170
/** Whether the element is disabled. */
155
171
@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 ) ; }
163
174
164
175
/** Unique id of the element. */
165
176
@Input ( )
166
177
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 ; }
168
179
169
180
/** 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 = '' ;
178
182
179
183
/** Whether the element is required. */
180
184
@Input ( )
@@ -203,11 +207,6 @@ export class MdInputDirective {
203
207
get value ( ) { return this . _elementRef . nativeElement . value ; }
204
208
set value ( value : string ) { this . _elementRef . nativeElement . value = value ; }
205
209
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
-
211
210
/** Whether the input is empty. */
212
211
get empty ( ) {
213
212
return ! this . _isNeverEmpty ( ) &&
@@ -218,8 +217,6 @@ export class MdInputDirective {
218
217
! this . _isBadInput ( ) ;
219
218
}
220
219
221
- private get _uid ( ) { return this . _cachedUid = this . _cachedUid || `md-input-${ nextUniqueId ++ } ` ; }
222
-
223
220
private _neverEmptyInputTypes = [
224
221
'date' ,
225
222
'datetime' ,
@@ -232,24 +229,51 @@ export class MdInputDirective {
232
229
constructor ( private _elementRef : ElementRef ,
233
230
private _renderer : Renderer2 ,
234
231
private _platform : Platform ,
232
+ private _changeDetectorRef : ChangeDetectorRef ,
235
233
@Optional ( ) @Self ( ) public _ngControl : NgControl ,
236
234
@Optional ( ) private _parentForm : NgForm ,
237
235
@Optional ( ) private _parentFormGroup : FormGroupDirective ,
238
236
@Optional ( ) @Inject ( MD_ERROR_GLOBAL_OPTIONS ) errorOptions : ErrorOptions ) {
239
237
240
238
// Force setter to be called in case id was not specified.
241
239
this . id = this . id ;
242
-
243
240
this . _errorOptions = errorOptions ? errorOptions : { } ;
244
241
this . errorStateMatcher = this . _errorOptions . errorStateMatcher || defaultErrorStateMatcher ;
245
242
}
246
243
247
- /** Focuses the input element. */
248
- focus ( ) { this . _elementRef . nativeElement . focus ( ) ; }
244
+ ngOnChanges ( ) {
245
+ this . _stateChanges . next ( ) ;
246
+ }
249
247
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
+ }
251
264
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
+ }
253
277
254
278
_onInput ( ) {
255
279
// This is a noop function and is used to let Angular know whenever the value changes.
@@ -261,22 +285,42 @@ export class MdInputDirective {
261
285
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
262
286
}
263
287
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 ;
266
291
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
+ }
269
309
}
270
310
271
311
/** Make sure the input is a supported type. */
272
312
private _validateType ( ) {
273
- if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) !== - 1 ) {
313
+ if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) > - 1 ) {
274
314
throw getMdInputContainerUnsupportedTypeError ( this . _type ) ;
275
315
}
276
316
}
277
317
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
+ }
279
322
323
+ /** Checks whether the input is invalid based on the native validation. */
280
324
private _isBadInput ( ) {
281
325
// The `validity` property won't be present on platform-server.
282
326
let validity = ( this . _elementRef . nativeElement as HTMLInputElement ) . validity ;
@@ -317,7 +361,7 @@ export class MdInputDirective {
317
361
// Remove align attribute to prevent it from interfering with layout.
318
362
'[attr.align]' : 'null' ,
319
363
'class' : 'mat-input-container' ,
320
- '[class.mat-input-invalid]' : '_mdInputChild._isErrorState() ' ,
364
+ '[class.mat-input-invalid]' : '_mdInputChild._isErrorState' ,
321
365
'[class.mat-focused]' : '_mdInputChild.focused' ,
322
366
'[class.ng-untouched]' : '_shouldForward("untouched")' ,
323
367
'[class.ng-touched]' : '_shouldForward("touched")' ,
@@ -329,6 +373,7 @@ export class MdInputDirective {
329
373
'(click)' : '_focusInput()' ,
330
374
} ,
331
375
encapsulation : ViewEncapsulation . None ,
376
+ changeDetection : ChangeDetectionStrategy . OnPush ,
332
377
} )
333
378
334
379
export class MdInputContainer implements AfterViewInit , AfterContentInit , AfterContentChecked {
@@ -337,7 +382,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
337
382
/** Color of the input divider, based on the theme. */
338
383
@Input ( ) color : 'primary' | 'accent' | 'warn' = 'primary' ;
339
384
340
- /** @deprecated Use color instead. */
385
+ /** @deprecated Use ` color` instead. */
341
386
@Input ( )
342
387
get dividerColor ( ) { return this . color ; }
343
388
set dividerColor ( value ) { this . color = value ; }
@@ -381,17 +426,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
381
426
382
427
/** Reference to the input's underline element. */
383
428
@ViewChild ( 'underline' ) underlineRef : ElementRef ;
384
-
385
429
@ContentChild ( MdInputDirective ) _mdInputChild : MdInputDirective ;
386
-
387
430
@ContentChild ( MdPlaceholder ) _placeholderChild : MdPlaceholder ;
388
-
389
431
@ContentChildren ( MdErrorDirective ) _errorChildren : QueryList < MdErrorDirective > ;
390
-
391
432
@ContentChildren ( MdHint ) _hintChildren : QueryList < MdHint > ;
392
-
393
433
@ContentChildren ( MdPrefix ) _prefixChildren : QueryList < MdPrefix > ;
394
-
395
434
@ContentChildren ( MdSuffix ) _suffixChildren : QueryList < MdSuffix > ;
396
435
397
436
constructor (
@@ -407,9 +446,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
407
446
this . _processHints ( ) ;
408
447
this . _validatePlaceholders ( ) ;
409
448
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.
411
462
this . _hintChildren . changes . subscribe ( ( ) => this . _processHints ( ) ) ;
412
- this . _mdInputChild . _placeholderChange . subscribe ( ( ) => this . _validatePlaceholders ( ) ) ;
413
463
}
414
464
415
465
ngAfterContentChecked ( ) {
@@ -429,15 +479,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
429
479
}
430
480
431
481
/** Whether the input has a placeholder. */
432
- _hasPlaceholder ( ) { return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ; }
482
+ _hasPlaceholder ( ) {
483
+ return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ;
484
+ }
433
485
434
486
/** Focuses the underlying input. */
435
- _focusInput ( ) { this . _mdInputChild . focus ( ) ; }
487
+ _focusInput ( ) {
488
+ this . _mdInputChild . focus ( ) ;
489
+ }
436
490
437
491
/** Determines whether to display hints or errors. */
438
492
_getDisplayedMessages ( ) : 'error' | 'hint' {
439
493
let input = this . _mdInputChild ;
440
- return ( this . _errorChildren . length > 0 && input . _isErrorState ( ) ) ? 'error' : 'hint' ;
494
+ return ( this . _errorChildren . length > 0 && input . _isErrorState ) ? 'error' : 'hint' ;
441
495
}
442
496
443
497
/**
0 commit comments