-
Notifications
You must be signed in to change notification settings - Fork 6.7k
/
input.ts
427 lines (373 loc) · 13.9 KB
/
input.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
/**
* @license
* Copyright Google LLC 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 {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {getSupportedInputTypes, Platform} from '@angular/cdk/platform';
import {AutofillMonitor} from '@angular/cdk/text-field';
import {
Directive,
DoCheck,
ElementRef,
Inject,
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
Optional,
Self,
} from '@angular/core';
import {FormGroupDirective, NgControl, NgForm} from '@angular/forms';
import {
CanUpdateErrorState,
CanUpdateErrorStateCtor,
ErrorStateMatcher,
mixinErrorState,
} from '@angular/material/core';
import {MatFormFieldControl} from '@angular/material/form-field';
import {Subject} from 'rxjs';
import {getMatInputUnsupportedTypeError} from './input-errors';
import {MAT_INPUT_VALUE_ACCESSOR} from './input-value-accessor';
// Invalid input type. Using one of these will throw an MatInputUnsupportedTypeError.
const MAT_INPUT_INVALID_TYPES = [
'button',
'checkbox',
'file',
'hidden',
'image',
'radio',
'range',
'reset',
'submit'
];
let nextUniqueId = 0;
// Boilerplate for applying mixins to MatInput.
/** @docs-private */
class MatInputBase {
constructor(public _defaultErrorStateMatcher: ErrorStateMatcher,
public _parentForm: NgForm,
public _parentFormGroup: FormGroupDirective,
/** @docs-private */
public ngControl: NgControl) {}
}
const _MatInputMixinBase: CanUpdateErrorStateCtor & typeof MatInputBase =
mixinErrorState(MatInputBase);
/** Directive that allows a native input to work inside a `MatFormField`. */
@Directive({
selector: `input[matInput], textarea[matInput], select[matNativeControl],
input[matNativeControl], textarea[matNativeControl]`,
exportAs: 'matInput',
host: {
/**
* @breaking-change 8.0.0 remove .mat-form-field-autofill-control in favor of AutofillMonitor.
*/
'class': 'mat-input-element mat-form-field-autofill-control',
'[class.mat-input-server]': '_isServer',
// 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.
'[attr.id]': 'id',
'[attr.placeholder]': 'placeholder',
'[disabled]': 'disabled',
'[required]': 'required',
'[attr.readonly]': 'readonly && !_isNativeSelect || null',
'[attr.aria-describedby]': '_ariaDescribedby || null',
'[attr.aria-invalid]': 'errorState',
'[attr.aria-required]': 'required.toString()',
'(blur)': '_focusChanged(false)',
'(focus)': '_focusChanged(true)',
'(input)': '_onInput()',
},
providers: [{provide: MatFormFieldControl, useExisting: MatInput}],
})
export class MatInput extends _MatInputMixinBase implements MatFormFieldControl<any>, OnChanges,
OnDestroy, OnInit, DoCheck, CanUpdateErrorState {
protected _uid = `mat-input-${nextUniqueId++}`;
protected _previousNativeValue: any;
private _inputValueAccessor: {value: any};
/** The aria-describedby attribute on the input for improved a11y. */
_ariaDescribedby: string;
/** Whether the component is being rendered on the server. */
_isServer = false;
/** Whether the component is a native html select. */
_isNativeSelect = false;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
focused: boolean = false;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
readonly stateChanges: Subject<void> = new Subject<void>();
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
controlType: string = 'mat-input';
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
autofilled = false;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
@Input()
get disabled(): boolean {
if (this.ngControl && this.ngControl.disabled !== null) {
return this.ngControl.disabled;
}
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
// Browsers may not fire the blur event if the input is disabled too quickly.
// Reset from here to ensure that the element doesn't become stuck.
if (this.focused) {
this.focused = false;
this.stateChanges.next();
}
}
protected _disabled = false;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
@Input()
get id(): string { return this._id; }
set id(value: string) { this._id = value || this._uid; }
protected _id: string;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
@Input() placeholder: string;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
@Input()
get required(): boolean { return this._required; }
set required(value: boolean) { this._required = coerceBooleanProperty(value); }
protected _required = false;
/** Input type of the element. */
@Input()
get type(): string { 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._elementRef.nativeElement as HTMLInputElement).type = this._type;
}
}
protected _type = 'text';
/** An object used to control when error messages are shown. */
@Input() errorStateMatcher: ErrorStateMatcher;
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
@Input()
get value(): string { return this._inputValueAccessor.value; }
set value(value: string) {
if (value !== this.value) {
this._inputValueAccessor.value = value;
this.stateChanges.next();
}
}
/** Whether the element is readonly. */
@Input()
get readonly(): boolean { return this._readonly; }
set readonly(value: boolean) { this._readonly = coerceBooleanProperty(value); }
private _readonly = false;
protected _neverEmptyInputTypes = [
'date',
'datetime',
'datetime-local',
'month',
'time',
'week'
].filter(t => getSupportedInputTypes().has(t));
constructor(
protected _elementRef: ElementRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
protected _platform: Platform,
/** @docs-private */
@Optional() @Self() public ngControl: NgControl,
@Optional() _parentForm: NgForm,
@Optional() _parentFormGroup: FormGroupDirective,
_defaultErrorStateMatcher: ErrorStateMatcher,
@Optional() @Self() @Inject(MAT_INPUT_VALUE_ACCESSOR) inputValueAccessor: any,
private _autofillMonitor: AutofillMonitor,
ngZone: NgZone) {
super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
const element = this._elementRef.nativeElement;
// If no input value accessor was explicitly specified, use the element as the input value
// accessor.
this._inputValueAccessor = inputValueAccessor || element;
this._previousNativeValue = this.value;
// Force setter to be called in case id was not specified.
this.id = this.id;
// On some versions of iOS the caret gets stuck in the wrong place when holding down the delete
// key. In order to get around this we need to "jiggle" the caret loose. Since this bug only
// exists on iOS, we only bother to install the listener on iOS.
if (_platform.IOS) {
ngZone.runOutsideAngular(() => {
_elementRef.nativeElement.addEventListener('keyup', (event: Event) => {
let el = event.target as HTMLInputElement;
if (!el.value && !el.selectionStart && !el.selectionEnd) {
// Note: Just setting `0, 0` doesn't fix the issue. Setting
// `1, 1` fixes it for the first time that you type text and
// then hold delete. Toggling to `1, 1` and then back to
// `0, 0` seems to completely fix it.
el.setSelectionRange(1, 1);
el.setSelectionRange(0, 0);
}
});
});
}
this._isServer = !this._platform.isBrowser;
this._isNativeSelect = element.nodeName.toLowerCase() === 'select';
if (this._isNativeSelect) {
this.controlType = (element as HTMLSelectElement).multiple ? 'mat-native-select-multiple' :
'mat-native-select';
}
}
ngOnInit() {
if (this._platform.isBrowser) {
this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(event => {
this.autofilled = event.isAutofilled;
this.stateChanges.next();
});
}
}
ngOnChanges() {
this.stateChanges.next();
}
ngOnDestroy() {
this.stateChanges.complete();
if (this._platform.isBrowser) {
this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement);
}
}
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
// that whatever logic is in here has to be super lean or we risk destroying the performance.
this.updateErrorState();
}
// We need to dirty-check the native element's value, because there are some cases where
// we won't be notified when it changes (e.g. the consumer isn't using forms or they're
// updating the value using `emitEvent: false`).
this._dirtyCheckNativeValue();
}
/** Focuses the input. */
focus(options?: FocusOptions): void {
this._elementRef.nativeElement.focus(options);
}
/** Callback for the cases where the focused state of the input changes. */
_focusChanged(isFocused: boolean) {
if (isFocused !== this.focused && (!this.readonly || !isFocused)) {
this.focused = isFocused;
this.stateChanges.next();
}
}
_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.
}
/** Does some manual dirty checking on the native input `value` property. */
protected _dirtyCheckNativeValue() {
const newValue = this._elementRef.nativeElement.value;
if (this._previousNativeValue !== newValue) {
this._previousNativeValue = newValue;
this.stateChanges.next();
}
}
/** Make sure the input is a supported type. */
protected _validateType() {
if (MAT_INPUT_INVALID_TYPES.indexOf(this._type) > -1) {
throw getMatInputUnsupportedTypeError(this._type);
}
}
/** Checks whether the input type is one of the types that are never empty. */
protected _isNeverEmpty() {
return this._neverEmptyInputTypes.indexOf(this._type) > -1;
}
/** Checks whether the input is invalid based on the native validation. */
protected _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. */
protected _isTextarea() {
return this._elementRef.nativeElement.nodeName.toLowerCase() === 'textarea';
}
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
get empty(): boolean {
return !this._isNeverEmpty() && !this._elementRef.nativeElement.value && !this._isBadInput() &&
!this.autofilled;
}
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
get shouldLabelFloat(): boolean {
if (this._isNativeSelect) {
// For a single-selection `<select>`, the label should float when the selected option has
// a non-empty display value. For a `<select multiple>`, the label *always* floats to avoid
// overlapping the label with the options.
const selectElement = this._elementRef.nativeElement as HTMLSelectElement;
const firstOption: HTMLOptionElement | undefined = selectElement.options[0];
// On most browsers the `selectedIndex` will always be 0, however on IE and Edge it'll be
// -1 if the `value` is set to something, that isn't in the list of options, at a later point.
return this.focused || selectElement.multiple || !this.empty ||
!!(selectElement.selectedIndex > -1 && firstOption && firstOption.label);
} else {
return this.focused || !this.empty;
}
}
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
setDescribedByIds(ids: string[]) {
this._ariaDescribedby = ids.join(' ');
}
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
*/
onContainerClick() {
// Do not re-focus the input element if the element is already focused. Otherwise it can happen
// that someone clicks on a time input and the cursor resets to the "hours" field while the
// "minutes" field was actually clicked. See: https://github.com/angular/components/issues/12849
if (!this.focused) {
this.focus();
}
}
static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_readonly: BooleanInput;
static ngAcceptInputType_required: BooleanInput;
// Accept `any` to avoid conflicts with other directives on `<input>` that may
// accept different types.
static ngAcceptInputType_value: any;
}