-
Notifications
You must be signed in to change notification settings - Fork 6.7k
/
input-container.ts
525 lines (445 loc) · 16.5 KB
/
input-container.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
AfterContentInit,
AfterContentChecked,
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChild,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
Input,
Optional,
Output,
QueryList,
Renderer2,
Self,
ViewChild,
ViewEncapsulation,
Inject
} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {coerceBooleanProperty, Platform} from '../core';
import {FormGroupDirective, NgControl, NgForm, FormControl} from '@angular/forms';
import {getSupportedInputTypes} from '../core/platform/features';
import {
getMdInputContainerDuplicatedHintError,
getMdInputContainerMissingMdInputError,
getMdInputContainerPlaceholderConflictError,
getMdInputContainerUnsupportedTypeError
} from './input-container-errors';
import {
FloatPlaceholderType,
PlaceholderOptions,
MD_PLACEHOLDER_GLOBAL_OPTIONS
} from '../core/placeholder/placeholder-options';
import {
defaultErrorStateMatcher,
ErrorStateMatcher,
ErrorOptions,
MD_ERROR_GLOBAL_OPTIONS
} from '../core/error/error-options';
// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
const MD_INPUT_INVALID_TYPES = [
'button',
'checkbox',
'color',
'file',
'hidden',
'image',
'radio',
'range',
'reset',
'submit'
];
let nextUniqueId = 0;
/**
* The placeholder directive. The content can declare this to implement more
* complex placeholders.
*/
@Directive({
selector: 'md-placeholder, mat-placeholder'
})
export class MdPlaceholder {}
/** Hint text to be shown underneath the input. */
@Directive({
selector: 'md-hint, mat-hint',
host: {
'class': 'mat-hint',
'[class.mat-right]': 'align == "end"',
'[attr.id]': 'id',
}
})
export class MdHint {
/** Whether to align the hint label at the start or end of the line. */
@Input() align: 'start' | 'end' = 'start';
/** Unique ID for the hint. Used for the aria-describedby on the input. */
@Input() id: string = `md-input-hint-${nextUniqueId++}`;
}
/** Single error message to be shown underneath the input. */
@Directive({
selector: 'md-error, mat-error',
host: {
'class': 'mat-input-error'
}
})
export class MdErrorDirective { }
/** Prefix to be placed the the front of the input. */
@Directive({
selector: '[mdPrefix], [matPrefix]'
})
export class MdPrefix {}
/** Suffix to be placed at the end of the input. */
@Directive({
selector: '[mdSuffix], [matSuffix]'
})
export class MdSuffix {}
/** Marker for the input element that `MdInputContainer` is wrapping. */
@Directive({
selector: `input[mdInput], textarea[mdInput], input[matInput], textarea[matInput]`,
host: {
'class': 'mat-input-element',
// Native input properties that are overwritten by Angular inputs need to be synced with
// the native input element. Otherwise property bindings for those don't work.
'[id]': 'id',
'[placeholder]': 'placeholder',
'[disabled]': 'disabled',
'[required]': 'required',
'[attr.aria-describedby]': 'ariaDescribedby || null',
'[attr.aria-invalid]': '_isErrorState()',
'(blur)': '_onBlur()',
'(focus)': '_onFocus()',
'(input)': '_onInput()',
}
})
export class MdInputDirective {
/** Variables used as cache for getters and setters. */
private _type = 'text';
private _placeholder: string = '';
private _disabled = false;
private _required = false;
private _id: string;
private _cachedUid: string;
private _errorOptions: ErrorOptions;
/** Whether the element is focused or not. */
focused = false;
/** Sets the aria-describedby attribute on the input for improved a11y. */
ariaDescribedby: string;
/** Whether the element is disabled. */
@Input()
get disabled() {
return this._ngControl ? this._ngControl.disabled : this._disabled;
}
set disabled(value: any) {
this._disabled = coerceBooleanProperty(value);
}
/** Unique id of the element. */
@Input()
get id() { return this._id; }
set id(value: string) {this._id = value || this._uid; }
/** Placeholder attribute of the element. */
@Input()
get placeholder() { return this._placeholder; }
set placeholder(value: string) {
if (this._placeholder !== value) {
this._placeholder = value;
this._placeholderChange.emit(this._placeholder);
}
}
/** Whether the element is required. */
@Input()
get required() { return this._required; }
set required(value: any) { this._required = coerceBooleanProperty(value); }
/** Input type of the element. */
@Input()
get type() { return this._type; }
set type(value: string) {
this._type = value || 'text';
this._validateType();
// When using Angular inputs, developers are no longer able to set the properties on the native
// input element. To ensure that bindings for `type` work, we need to sync the setter
// with the native property. Textarea elements don't support the type property or attribute.
if (!this._isTextarea() && getSupportedInputTypes().has(this._type)) {
this._renderer.setProperty(this._elementRef.nativeElement, 'type', this._type);
}
}
/** A function used to control when error messages are shown. */
@Input() errorStateMatcher: ErrorStateMatcher;
/** The input element's value. */
get value() { return this._elementRef.nativeElement.value; }
set value(value: string) { this._elementRef.nativeElement.value = value; }
/**
* Emits an event when the placeholder changes so that the `md-input-container` can re-validate.
*/
@Output() _placeholderChange = new EventEmitter<string>();
/** Whether the input is empty. */
get empty() {
return !this._isNeverEmpty() &&
(this.value == null || this.value === '') &&
// Check if the input contains bad input. If so, we know that it only appears empty because
// the value failed to parse. From the user's perspective it is not empty.
// TODO(mmalerba): Add e2e test for bad input case.
!this._isBadInput();
}
private get _uid() { return this._cachedUid = this._cachedUid || `md-input-${nextUniqueId++}`; }
private _neverEmptyInputTypes = [
'date',
'datetime',
'datetime-local',
'month',
'time',
'week'
].filter(t => getSupportedInputTypes().has(t));
constructor(private _elementRef: ElementRef,
private _renderer: Renderer2,
private _platform: Platform,
@Optional() @Self() public _ngControl: NgControl,
@Optional() private _parentForm: NgForm,
@Optional() private _parentFormGroup: FormGroupDirective,
@Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
// Force setter to be called in case id was not specified.
this.id = this.id;
this._errorOptions = errorOptions ? errorOptions : {};
this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
}
/** Focuses the input element. */
focus() { this._elementRef.nativeElement.focus(); }
_onFocus() { this.focused = true; }
_onBlur() { this.focused = false; }
_onInput() {
// This is a noop function and is used to let Angular know whenever the value changes.
// Angular will run a new change detection each time the `input` event has been dispatched.
// It's necessary that Angular recognizes the value change, because when floatingLabel
// is set to false and Angular forms aren't used, the placeholder won't recognize the
// value changes and will not disappear.
// Listening to the input event wouldn't be necessary when the input is using the
// FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
}
/** Whether the input is in an error state. */
_isErrorState(): boolean {
const control = this._ngControl;
const form = this._parentFormGroup || this._parentForm;
return control && this.errorStateMatcher(control.control as FormControl, form);
}
/** Make sure the input is a supported type. */
private _validateType() {
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) {
throw getMdInputContainerUnsupportedTypeError(this._type);
}
}
private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) !== -1; }
private _isBadInput() {
// The `validity` property won't be present on platform-server.
let validity = (this._elementRef.nativeElement as HTMLInputElement).validity;
return validity && validity.badInput;
}
/** Determines if the component host is a textarea. If not recognizable it returns false. */
private _isTextarea() {
let nativeElement = this._elementRef.nativeElement;
// In Universal, we don't have access to `nodeName`, but the same can be achieved with `name`.
// Note that this shouldn't be necessary once Angular switches to an API that resembles the
// DOM closer.
let nodeName = this._platform.isBrowser ? nativeElement.nodeName : nativeElement.name;
return nodeName ? nodeName.toLowerCase() === 'textarea' : false;
}
}
/**
* Container for text inputs that applies Material Design styling and behavior.
*/
@Component({
moduleId: module.id,
selector: 'md-input-container, mat-input-container',
templateUrl: 'input-container.html',
styleUrls: ['input-container.css'],
animations: [
trigger('transitionMessages', [
state('enter', style({ opacity: 1, transform: 'translateY(0%)' })),
transition('void => enter', [
style({ opacity: 0, transform: 'translateY(-100%)' }),
animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')
])
])
],
host: {
// Remove align attribute to prevent it from interfering with layout.
'[attr.align]': 'null',
'class': 'mat-input-container',
'[class.mat-input-invalid]': '_mdInputChild._isErrorState()',
'[class.mat-focused]': '_mdInputChild.focused',
'[class.ng-untouched]': '_shouldForward("untouched")',
'[class.ng-touched]': '_shouldForward("touched")',
'[class.ng-pristine]': '_shouldForward("pristine")',
'[class.ng-dirty]': '_shouldForward("dirty")',
'[class.ng-valid]': '_shouldForward("valid")',
'[class.ng-invalid]': '_shouldForward("invalid")',
'[class.ng-pending]': '_shouldForward("pending")',
'(click)': '_focusInput()',
},
encapsulation: ViewEncapsulation.None,
})
export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterContentChecked {
private _placeholderOptions: PlaceholderOptions;
/** Color of the input divider, based on the theme. */
@Input() color: 'primary' | 'accent' | 'warn' = 'primary';
/** @deprecated Use color instead. */
@Input()
get dividerColor() { return this.color; }
set dividerColor(value) { this.color = value; }
/** Whether the required marker should be hidden. */
@Input()
get hideRequiredMarker() { return this._hideRequiredMarker; }
set hideRequiredMarker(value: any) {
this._hideRequiredMarker = coerceBooleanProperty(value);
}
private _hideRequiredMarker: boolean;
/** Whether the floating label should always float or not. */
get _shouldAlwaysFloat() { return this._floatPlaceholder === 'always'; }
/** Whether the placeholder can float or not. */
get _canPlaceholderFloat() { return this._floatPlaceholder !== 'never'; }
/** State of the md-hint and md-error animations. */
_subscriptAnimationState: string = '';
/** Text for the input hint. */
@Input()
get hintLabel() { return this._hintLabel; }
set hintLabel(value: string) {
this._hintLabel = value;
this._processHints();
}
private _hintLabel = '';
// Unique id for the hint label.
_hintLabelId: string = `md-input-hint-${nextUniqueId++}`;
/** Whether the placeholder should always float, never float or float as the user types. */
@Input()
get floatPlaceholder() { return this._floatPlaceholder; }
set floatPlaceholder(value: FloatPlaceholderType) {
this._floatPlaceholder = value || this._placeholderOptions.float || 'auto';
}
private _floatPlaceholder: FloatPlaceholderType;
/** Reference to the input's underline element. */
@ViewChild('underline') underlineRef: ElementRef;
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
@ContentChildren(MdErrorDirective) _errorChildren: QueryList<MdErrorDirective>;
@ContentChildren(MdHint) _hintChildren: QueryList<MdHint>;
@ContentChildren(MdPrefix) _prefixChildren: QueryList<MdPrefix>;
@ContentChildren(MdSuffix) _suffixChildren: QueryList<MdSuffix>;
constructor(
public _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions) {
this._placeholderOptions = placeholderOptions ? placeholderOptions : {};
this.floatPlaceholder = this._placeholderOptions.float || 'auto';
}
ngAfterContentInit() {
this._validateInputChild();
this._processHints();
this._validatePlaceholders();
// Re-validate when things change.
this._hintChildren.changes.subscribe(() => this._processHints());
this._mdInputChild._placeholderChange.subscribe(() => this._validatePlaceholders());
// Mark for check when the input's value changes to recalculate whether input is empty
const control = this._mdInputChild._ngControl;
if (control && control.valueChanges) {
control.valueChanges.subscribe(() => this._changeDetectorRef.markForCheck());
}
}
ngAfterContentChecked() {
this._validateInputChild();
}
ngAfterViewInit() {
// Avoid animations on load.
this._subscriptAnimationState = 'enter';
this._changeDetectorRef.detectChanges();
}
/** Determines whether a class from the NgControl should be forwarded to the host element. */
_shouldForward(prop: string): boolean {
let control = this._mdInputChild ? this._mdInputChild._ngControl : null;
return control && (control as any)[prop];
}
/** Whether the input has a placeholder. */
_hasPlaceholder() { return !!(this._mdInputChild.placeholder || this._placeholderChild); }
/** Focuses the underlying input. */
_focusInput() { this._mdInputChild.focus(); }
/** Determines whether to display hints or errors. */
_getDisplayedMessages(): 'error' | 'hint' {
let input = this._mdInputChild;
return (this._errorChildren.length > 0 && input._isErrorState()) ? 'error' : 'hint';
}
/**
* Ensure that there is only one placeholder (either `input` attribute or child element with the
* `md-placeholder` attribute.
*/
private _validatePlaceholders() {
if (this._mdInputChild.placeholder && this._placeholderChild) {
throw getMdInputContainerPlaceholderConflictError();
}
}
/**
* Does any extra processing that is required when handling the hints.
*/
private _processHints() {
this._validateHints();
this._syncAriaDescribedby();
}
/**
* Ensure that there is a maximum of one of each `<md-hint>` alignment specified, with the
* attribute being considered as `align="start"`.
*/
private _validateHints() {
if (this._hintChildren) {
let startHint: MdHint;
let endHint: MdHint;
this._hintChildren.forEach((hint: MdHint) => {
if (hint.align == 'start') {
if (startHint || this.hintLabel) {
throw getMdInputContainerDuplicatedHintError('start');
}
startHint = hint;
} else if (hint.align == 'end') {
if (endHint) {
throw getMdInputContainerDuplicatedHintError('end');
}
endHint = hint;
}
});
}
}
/**
* Sets the child input's `aria-describedby` to a space-separated list of the ids
* of the currently-specified hints, as well as a generated id for the hint label.
*/
private _syncAriaDescribedby() {
if (this._mdInputChild) {
let ids: string[] = [];
let startHint = this._hintChildren ?
this._hintChildren.find(hint => hint.align === 'start') : null;
let endHint = this._hintChildren ?
this._hintChildren.find(hint => hint.align === 'end') : null;
if (startHint) {
ids.push(startHint.id);
} else if (this._hintLabel) {
ids.push(this._hintLabelId);
}
if (endHint) {
ids.push(endHint.id);
}
this._mdInputChild.ariaDescribedby = ids.join(' ');
}
}
/**
* Throws an error if the container's input child was removed.
*/
protected _validateInputChild() {
if (!this._mdInputChild) {
throw getMdInputContainerMissingMdInputError();
}
}
}