@@ -16,16 +16,18 @@ import {
16
16
ContentChildren ,
17
17
Directive ,
18
18
ElementRef ,
19
- EventEmitter ,
20
19
Input ,
21
20
Optional ,
22
- Output ,
23
21
QueryList ,
24
22
Renderer2 ,
25
23
Self ,
26
24
ViewChild ,
27
25
ViewEncapsulation ,
28
- Inject
26
+ Inject ,
27
+ ChangeDetectionStrategy ,
28
+ OnChanges ,
29
+ OnDestroy ,
30
+ DoCheck ,
29
31
} from '@angular/core' ;
30
32
import { animate , state , style , transition , trigger } from '@angular/animations' ;
31
33
import { coerceBooleanProperty , Platform } from '../core' ;
@@ -48,6 +50,7 @@ import {
48
50
ErrorOptions ,
49
51
MD_ERROR_GLOBAL_OPTIONS
50
52
} from '../core/error/error-options' ;
53
+ import { Subject } from 'rxjs/Subject' ;
51
54
52
55
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
53
56
const MD_INPUT_INVALID_TYPES = [
@@ -128,13 +131,13 @@ export class MdSuffix {}
128
131
'[disabled]' : 'disabled' ,
129
132
'[required]' : 'required' ,
130
133
'[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 )' ,
134
137
'(input)' : '_onInput()' ,
135
138
}
136
139
} )
137
- export class MdInputDirective {
140
+ export class MdInputDirective implements OnChanges , OnDestroy , DoCheck {
138
141
139
142
/** Variables used as cache for getters and setters. */
140
143
private _type = 'text' ;
@@ -143,39 +146,37 @@ export class MdInputDirective {
143
146
private _required = false ;
144
147
private _readonly = false ;
145
148
private _id : string ;
146
- private _cachedUid : string ;
149
+ private _uid = `md-input- ${ nextUniqueId ++ } ` ;
147
150
private _errorOptions : ErrorOptions ;
151
+ private _previousNativeValue = this . value ;
152
+
153
+ /** Whether the input is in an error state. */
154
+ _isErrorState = false ;
148
155
149
156
/** Whether the element is focused or not. */
150
157
focused = false ;
151
158
152
159
/** Sets the aria-describedby attribute on the input for improved a11y. */
153
160
ariaDescribedby : string ;
154
161
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
+
155
168
/** Whether the element is disabled. */
156
169
@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 ) ; }
164
172
165
173
/** Unique id of the element. */
166
174
@Input ( )
167
175
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 ; }
169
177
170
178
/** 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 = '' ;
179
180
180
181
/** Whether the element is required. */
181
182
@Input ( )
@@ -209,11 +210,6 @@ export class MdInputDirective {
209
210
get value ( ) { return this . _elementRef . nativeElement . value ; }
210
211
set value ( value : string ) { this . _elementRef . nativeElement . value = value ; }
211
212
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
213
/** Whether the input is empty. */
218
214
get empty ( ) {
219
215
return ! this . _isNeverEmpty ( ) &&
@@ -224,8 +220,6 @@ export class MdInputDirective {
224
220
! this . _isBadInput ( ) ;
225
221
}
226
222
227
- private get _uid ( ) { return this . _cachedUid = this . _cachedUid || `md-input-${ nextUniqueId ++ } ` ; }
228
-
229
223
private _neverEmptyInputTypes = [
230
224
'date' ,
231
225
'datetime' ,
@@ -238,28 +232,57 @@ export class MdInputDirective {
238
232
constructor ( private _elementRef : ElementRef ,
239
233
private _renderer : Renderer2 ,
240
234
private _platform : Platform ,
235
+ private _changeDetectorRef : ChangeDetectorRef ,
241
236
@Optional ( ) @Self ( ) public _ngControl : NgControl ,
242
237
@Optional ( ) private _parentForm : NgForm ,
243
238
@Optional ( ) private _parentFormGroup : FormGroupDirective ,
244
239
@Optional ( ) @Inject ( MD_ERROR_GLOBAL_OPTIONS ) errorOptions : ErrorOptions ) {
245
240
246
241
// Force setter to be called in case id was not specified.
247
242
this . id = this . id ;
248
-
249
243
this . _errorOptions = errorOptions ? errorOptions : { } ;
250
244
this . errorStateMatcher = this . _errorOptions . errorStateMatcher || defaultErrorStateMatcher ;
251
245
}
252
246
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
+ }
255
267
256
268
_onFocus ( ) {
257
269
if ( ! this . _readonly ) {
258
270
this . focused = true ;
259
271
}
260
272
}
261
273
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
+ }
263
286
264
287
_onInput ( ) {
265
288
// This is a noop function and is used to let Angular know whenever the value changes.
@@ -271,22 +294,42 @@ export class MdInputDirective {
271
294
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
272
295
}
273
296
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 ;
276
300
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
+ }
279
318
}
280
319
281
320
/** Make sure the input is a supported type. */
282
321
private _validateType ( ) {
283
- if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) !== - 1 ) {
322
+ if ( MD_INPUT_INVALID_TYPES . indexOf ( this . _type ) > - 1 ) {
284
323
throw getMdInputContainerUnsupportedTypeError ( this . _type ) ;
285
324
}
286
325
}
287
326
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
+ }
289
331
332
+ /** Checks whether the input is invalid based on the native validation. */
290
333
private _isBadInput ( ) {
291
334
// The `validity` property won't be present on platform-server.
292
335
let validity = ( this . _elementRef . nativeElement as HTMLInputElement ) . validity ;
@@ -327,7 +370,7 @@ export class MdInputDirective {
327
370
// Remove align attribute to prevent it from interfering with layout.
328
371
'[attr.align]' : 'null' ,
329
372
'class' : 'mat-input-container' ,
330
- '[class.mat-input-invalid]' : '_mdInputChild._isErrorState() ' ,
373
+ '[class.mat-input-invalid]' : '_mdInputChild._isErrorState' ,
331
374
'[class.mat-focused]' : '_mdInputChild.focused' ,
332
375
'[class.ng-untouched]' : '_shouldForward("untouched")' ,
333
376
'[class.ng-touched]' : '_shouldForward("touched")' ,
@@ -339,6 +382,7 @@ export class MdInputDirective {
339
382
'(click)' : '_focusInput()' ,
340
383
} ,
341
384
encapsulation : ViewEncapsulation . None ,
385
+ changeDetection : ChangeDetectionStrategy . OnPush ,
342
386
} )
343
387
344
388
export class MdInputContainer implements AfterViewInit , AfterContentInit , AfterContentChecked {
@@ -347,7 +391,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
347
391
/** Color of the input divider, based on the theme. */
348
392
@Input ( ) color : 'primary' | 'accent' | 'warn' = 'primary' ;
349
393
350
- /** @deprecated Use color instead. */
394
+ /** @deprecated Use ` color` instead. */
351
395
@Input ( )
352
396
get dividerColor ( ) { return this . color ; }
353
397
set dividerColor ( value ) { this . color = value ; }
@@ -391,17 +435,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
391
435
392
436
/** Reference to the input's underline element. */
393
437
@ViewChild ( 'underline' ) underlineRef : ElementRef ;
394
-
395
438
@ContentChild ( MdInputDirective ) _mdInputChild : MdInputDirective ;
396
-
397
439
@ContentChild ( MdPlaceholder ) _placeholderChild : MdPlaceholder ;
398
-
399
440
@ContentChildren ( MdErrorDirective ) _errorChildren : QueryList < MdErrorDirective > ;
400
-
401
441
@ContentChildren ( MdHint ) _hintChildren : QueryList < MdHint > ;
402
-
403
442
@ContentChildren ( MdPrefix ) _prefixChildren : QueryList < MdPrefix > ;
404
-
405
443
@ContentChildren ( MdSuffix ) _suffixChildren : QueryList < MdSuffix > ;
406
444
407
445
constructor (
@@ -417,15 +455,20 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
417
455
this . _processHints ( ) ;
418
456
this . _validatePlaceholders ( ) ;
419
457
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
+ } ) ;
423
463
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
+ } ) ;
428
468
}
469
+
470
+ // Re-validate when the amount of hints changes.
471
+ this . _hintChildren . changes . subscribe ( ( ) => this . _processHints ( ) ) ;
429
472
}
430
473
431
474
ngAfterContentChecked ( ) {
@@ -445,15 +488,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC
445
488
}
446
489
447
490
/** Whether the input has a placeholder. */
448
- _hasPlaceholder ( ) { return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ; }
491
+ _hasPlaceholder ( ) {
492
+ return ! ! ( this . _mdInputChild . placeholder || this . _placeholderChild ) ;
493
+ }
449
494
450
495
/** Focuses the underlying input. */
451
- _focusInput ( ) { this . _mdInputChild . focus ( ) ; }
496
+ _focusInput ( ) {
497
+ this . _mdInputChild . focus ( ) ;
498
+ }
452
499
453
500
/** Determines whether to display hints or errors. */
454
501
_getDisplayedMessages ( ) : 'error' | 'hint' {
455
502
let input = this . _mdInputChild ;
456
- return ( this . _errorChildren . length > 0 && input . _isErrorState ( ) ) ? 'error' : 'hint' ;
503
+ return ( this . _errorChildren . length > 0 && input . _isErrorState ) ? 'error' : 'hint' ;
457
504
}
458
505
459
506
/**
0 commit comments