@@ -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,13 +134,13 @@ 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' ;
@@ -143,39 +149,37 @@ export class MdInputDirective {
143
149
private _required = false ;
144
150
private _readonly = false ;
145
151
private _id : string ;
146
- private _cachedUid : string ;
152
+ private _uid = `md-input- ${ nextUniqueId ++ } ` ;
147
153
private _errorOptions : ErrorOptions ;
154
+ private _previousNativeValue = this . value ;
155
+
156
+ /** Whether the input is in an error state. */
157
+ _isErrorState = false ;
148
158
149
159
/** Whether the element is focused or not. */
150
160
focused = false ;
151
161
152
162
/** Sets the aria-describedby attribute on the input for improved a11y. */
153
163
ariaDescribedby : string ;
154
164
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
+
155
171
/** Whether the element is disabled. */
156
172
@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 ) ; }
164
175
165
176
/** Unique id of the element. */
166
177
@Input ( )
167
178
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 ; }
169
180
170
181
/** 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 = '' ;
179
183
180
184
/** Whether the element is required. */
181
185
@Input ( )
@@ -209,11 +213,6 @@ export class MdInputDirective {
209
213
get value ( ) { return this . _elementRef . nativeElement . value ; }
210
214
set value ( value : string ) { this . _elementRef . nativeElement . value = value ; }
211
215
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
-
217
216
/** Whether the input is empty. */
218
217
get empty ( ) {
219
218
return ! this . _isNeverEmpty ( ) &&
@@ -224,8 +223,6 @@ export class MdInputDirective {
224
223
! this . _isBadInput ( ) ;
225
224
}
226
225
227
- private get _uid ( ) { return this . _cachedUid = this . _cachedUid || `md-input-${ nextUniqueId ++ } ` ; }
228
-
229
226
private _neverEmptyInputTypes = [
230
227
'date' ,
231
228
'datetime' ,
@@ -238,28 +235,57 @@ export class MdInputDirective {
238
235
constructor ( private _elementRef : ElementRef ,
239
236
private _renderer : Renderer2 ,
240
237
private _platform : Platform ,
238
+ private _changeDetectorRef : ChangeDetectorRef ,
241
239
@Optional ( ) @Self ( ) public _ngControl : NgControl ,
242
240
@Optional ( ) private _parentForm : NgForm ,
243
241
@Optional ( ) private _parentFormGroup : FormGroupDirective ,
244
242
@Optional ( ) @Inject ( MD_ERROR_GLOBAL_OPTIONS ) errorOptions : ErrorOptions ) {
245
243
246
244
// Force setter to be called in case id was not specified.
247
245
this . id = this . id ;
248
-
249
246
this . _errorOptions = errorOptions ? errorOptions : { } ;
250
247
this . errorStateMatcher = this . _errorOptions . errorStateMatcher || defaultErrorStateMatcher ;
251
248
}
252
249
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
+ }
255
270
256
271
_onFocus ( ) {
257
272
if ( ! this . _readonly ) {
258
273
this . focused = true ;
259
274
}
260
275
}
261
276
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
+ }
263
289
264
290
_onInput ( ) {
265
291
// This is a noop function and is used to let Angular know whenever the value changes.
@@ -271,22 +297,42 @@ export class MdInputDirective {
271
297
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
272
298
}
273
299
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 ;
276
303
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
+ }
279
321
}
280
322
281
323
/** Make sure the input is a supported type. */
282
324
private _validateType ( ) {
283
- if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) !== - 1 ) {
325
+ if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) > - 1 ) {
284
326
throw getMdInputContainerUnsupportedTypeError ( this . _type ) ;
285
327
}
286
328
}
287
329
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
+ }
289
334
335
+ /** Checks whether the input is invalid based on the native validation. */
290
336
private _isBadInput ( ) {
291
337
// The `validity` property won't be present on platform-server.
292
338
let validity = ( this . _elementRef . nativeElement as HTMLInputElement ) . validity ;
@@ -327,7 +373,7 @@ export class MdInputDirective {
327
373
// Remove align attribute to prevent it from interfering with layout.
328
374
'[attr.align]' : 'null' ,
329
375
'class' : 'mat-input-container' ,
330
- '[class.mat-input-invalid]' : '_mdInputChild._isErrorState() ' ,
376
+ '[class.mat-input-invalid]' : '_mdInputChild._isErrorState' ,
331
377
'[class.mat-focused]' : '_mdInputChild.focused' ,
332
378
'[class.ng-untouched]' : '_shouldForward("untouched")' ,
333
379
'[class.ng-touched]' : '_shouldForward("touched")' ,
@@ -339,6 +385,7 @@ export class MdInputDirective {
339
385
'(click)' : '_focusInput()' ,
340
386
} ,
341
387
encapsulation : ViewEncapsulation . None ,
388
+ changeDetection : ChangeDetectionStrategy . OnPush ,
342
389
} )
343
390
344
391
export class MdInputContainer implements AfterViewInit , AfterContentInit , AfterContentChecked {
@@ -347,7 +394,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
347
394
/** Color of the input divider, based on the theme. */
348
395
@Input ( ) color : 'primary' | 'accent' | 'warn' = 'primary' ;
349
396
350
- /** @deprecated Use color instead. */
397
+ /** @deprecated Use ` color` instead. */
351
398
@Input ( )
352
399
get dividerColor ( ) { return this . color ; }
353
400
set dividerColor ( value ) { this . color = value ; }
@@ -391,17 +438,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
391
438
392
439
/** Reference to the input's underline element. */
393
440
@ViewChild ( 'underline' ) underlineRef : ElementRef ;
394
-
395
441
@ContentChild ( MdInputDirective ) _mdInputChild : MdInputDirective ;
396
-
397
442
@ContentChild ( MdPlaceholder ) _placeholderChild : MdPlaceholder ;
398
-
399
443
@ContentChildren ( MdErrorDirective ) _errorChildren : QueryList < MdErrorDirective > ;
400
-
401
444
@ContentChildren ( MdHint ) _hintChildren : QueryList < MdHint > ;
402
-
403
445
@ContentChildren ( MdPrefix ) _prefixChildren : QueryList < MdPrefix > ;
404
-
405
446
@ContentChildren ( MdSuffix ) _suffixChildren : QueryList < MdSuffix > ;
406
447
407
448
constructor (
@@ -417,15 +458,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
417
458
this . _processHints ( ) ;
418
459
this . _validatePlaceholders ( ) ;
419
460
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
+ } ) ;
423
466
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
+ } ) ;
428
471
}
472
+
473
+ // Re-validate when the amount of hints changes.
474
+ this . _hintChildren . changes . subscribe ( ( ) => this . _processHints ( ) ) ;
429
475
}
430
476
431
477
ngAfterContentChecked ( ) {
@@ -445,15 +491,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
445
491
}
446
492
447
493
/** Whether the input has a placeholder. */
448
- _hasPlaceholder ( ) { return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ; }
494
+ _hasPlaceholder ( ) {
495
+ return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ;
496
+ }
449
497
450
498
/** Focuses the underlying input. */
451
- _focusInput ( ) { this . _mdInputChild . focus ( ) ; }
499
+ _focusInput ( ) {
500
+ this . _mdInputChild . focus ( ) ;
501
+ }
452
502
453
503
/** Determines whether to display hints or errors. */
454
504
_getDisplayedMessages ( ) : 'error' | 'hint' {
455
505
let input = this . _mdInputChild ;
456
- return ( this . _errorChildren . length > 0 && input . _isErrorState ( ) ) ? 'error' : 'hint' ;
506
+ return ( this . _errorChildren . length > 0 && input . _isErrorState ) ? 'error' : 'hint' ;
457
507
}
458
508
459
509
/**
0 commit comments