-
Notifications
You must be signed in to change notification settings - Fork 6.8k
/
input.ts
312 lines (267 loc) · 8.51 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
import {
forwardRef,
Component,
HostBinding,
Input,
Provider,
Directive,
AfterContentInit,
ContentChild,
SimpleChange,
ContentChildren,
ViewChild,
ElementRef,
QueryList,
OnChanges,
EventEmitter,
Output,
} from '@angular/core';
import {
NG_VALUE_ACCESSOR,
ControlValueAccessor,
NgModel,
} from '@angular/forms';
import {NgIf} from '@angular/common';
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
import {MdError} from '@angular2-material/core/errors/error';
import {Observable} from 'rxjs/Observable';
const noop = () => {};
export const MD_INPUT_CONTROL_VALUE_ACCESSOR = new Provider(NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => MdInput),
multi: true
});
// Invalid input type. Using one of these will throw an MdInputUnsupportedTypeError.
const MD_INPUT_INVALID_INPUT_TYPE = [
'file',
'radio',
'checkbox',
];
let nextUniqueId = 0;
export class MdInputPlaceholderConflictError extends MdError {
constructor() {
super('Placeholder attribute and child element were both specified.');
}
}
export class MdInputUnsupportedTypeError extends MdError {
constructor(type: string) {
super(`Input type "${type}" isn't supported by md-input.`);
}
}
export class MdInputDuplicatedHintError extends MdError {
constructor(align: string) {
super(`A hint was already declared for 'align="${align}"'.`);
}
}
/**
* The placeholder directive. The content can declare this to implement more
* complex placeholders.
*/
@Directive({
selector: 'md-placeholder'
})
export class MdPlaceholder {}
/** The hint directive, used to tag content as hint labels (going under the input). */
@Directive({
selector: 'md-hint',
host: {
'[class.md-right]': 'align == "end"',
'[class.md-hint]': 'true'
}
})
export class MdHint {
// Whether to align the hint label at the start or end of the line.
@Input() align: 'start' | 'end' = 'start';
}
/**
* Component that represents a text input. It encapsulates the <input> HTMLElement and
* improve on its behaviour, along with styling it according to the Material Design.
*/
@Component({
moduleId: module.id,
selector: 'md-input',
templateUrl: 'input.html',
styleUrls: ['input.css'],
providers: [MD_INPUT_CONTROL_VALUE_ACCESSOR],
directives: [NgIf, NgModel],
host: {'(click)' : 'focus()'}
})
export class MdInput implements ControlValueAccessor, AfterContentInit, OnChanges {
private _focused: boolean = false;
private _value: any = '';
/** Callback registered via registerOnTouched (ControlValueAccessor) */
private _onTouchedCallback: () => void = noop;
/** Callback registered via registerOnChange (ControlValueAccessor) */
private _onChangeCallback: (_: any) => void = noop;
/**
* Aria related inputs.
*/
@Input('aria-label') ariaLabel: string;
@Input('aria-labelledby') ariaLabelledBy: string;
@Input('aria-disabled') @BooleanFieldValue() ariaDisabled: boolean;
@Input('aria-required') @BooleanFieldValue() ariaRequired: boolean;
@Input('aria-invalid') @BooleanFieldValue() ariaInvalid: boolean;
/**
* Content directives.
*/
@ContentChild(MdPlaceholder) private _placeholderChild: MdPlaceholder;
@ContentChildren(MdHint) private _hintChildren: QueryList<MdHint>;
/** Readonly properties. */
get focused() { return this._focused; }
get empty() { return this._value == null || this._value === ''; }
get characterCount(): number {
return this.empty ? 0 : ('' + this._value).length;
}
get inputId(): string { return `${this.id}-input`; }
/**
* Bindings.
*/
@Input() align: 'start' | 'end' = 'start';
@Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary';
@Input() @BooleanFieldValue() floatingPlaceholder: boolean = true;
@Input() hintLabel: string = '';
@Input() autoComplete: string;
@Input() @BooleanFieldValue() autoFocus: boolean = false;
@Input() @BooleanFieldValue() disabled: boolean = false;
@Input() id: string = `md-input-${nextUniqueId++}`;
@Input() list: string = null;
@Input() max: string = null;
@Input() maxLength: number = null;
@Input() min: string = null;
@Input() minLength: number = null;
@Input() placeholder: string = null;
@Input() @BooleanFieldValue() readOnly: boolean = false;
@Input() @BooleanFieldValue() required: boolean = false;
@Input() @BooleanFieldValue() spellCheck: boolean = false;
@Input() step: number = null;
@Input() tabIndex: number = null;
@Input() type: string = 'text';
@Input() name: string = null;
private _blurEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
private _focusEmitter: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
@Output('blur')
get onBlur(): Observable<FocusEvent> {
return this._blurEmitter.asObservable();
}
@Output('focus')
get onFocus(): Observable<FocusEvent> {
return this._focusEmitter.asObservable();
}
get value(): any { return this._value; };
@Input() set value(v: any) {
v = this._convertValueForInputType(v);
if (v !== this._value) {
this._value = v;
this._onChangeCallback(v);
}
}
// This is to remove the `align` property of the `md-input` itself. Otherwise HTML5
// might place it as RTL when we don't want to. We still want to use `align` as an
// Input though, so we use HostBinding.
@HostBinding('attr.align') private get _align(): any { return null; }
@ViewChild('input') private _inputElement: ElementRef;
/** Set focus on input */
focus() {
this._inputElement.nativeElement.focus();
}
/** @internal */
handleFocus(event: FocusEvent) {
this._focused = true;
this._focusEmitter.emit(event);
}
/** @internal */
handleBlur(event: FocusEvent) {
this._focused = false;
this._onTouchedCallback();
this._blurEmitter.emit(event);
}
/** @internal */
handleChange(event: Event) {
this.value = (<HTMLInputElement>event.target).value;
this._onTouchedCallback();
}
/** @internal */
hasPlaceholder(): boolean {
return !!this.placeholder || this._placeholderChild != null;
}
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
*/
writeValue(value: any) {
this._value = value;
}
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
*/
registerOnChange(fn: any) {
this._onChangeCallback = fn;
}
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
*/
registerOnTouched(fn: any) {
this._onTouchedCallback = fn;
}
/** TODO: internal */
ngAfterContentInit() {
this._validateConstraints();
// Trigger validation when the hint children change.
this._hintChildren.changes.subscribe(() => {
this._validateConstraints();
});
}
/** TODO: internal */
ngOnChanges(changes: {[key: string]: SimpleChange}) {
this._validateConstraints();
}
/**
* Convert the value passed in to a value that is expected from the type of the md-input.
* This is normally performed by the *_VALUE_ACCESSOR in forms, but since the type is bound
* on our internal input it won't work locally.
* @private
*/
private _convertValueForInputType(v: any): any {
switch (this.type) {
case 'number': return parseFloat(v);
default: return v;
}
}
/**
* Ensure that all constraints defined by the API are validated, or throw errors otherwise.
* Constraints for now:
* - placeholder attribute and <md-placeholder> are mutually exclusive.
* - type attribute is not one of the forbidden types (see constant at the top).
* - Maximum one of each `<md-hint>` alignment specified, with the attribute being
* considered as align="start".
* @private
*/
private _validateConstraints() {
if (this.placeholder != '' && this.placeholder != null && this._placeholderChild != null) {
throw new MdInputPlaceholderConflictError();
}
if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this.type) != -1) {
throw new MdInputUnsupportedTypeError(this.type);
}
if (this._hintChildren) {
// Validate the hint labels.
let startHint: MdHint = null;
let endHint: MdHint = null;
this._hintChildren.forEach((hint: MdHint) => {
if (hint.align == 'start') {
if (startHint || this.hintLabel) {
throw new MdInputDuplicatedHintError('start');
}
startHint = hint;
} else if (hint.align == 'end') {
if (endHint) {
throw new MdInputDuplicatedHintError('end');
}
endHint = hint;
}
});
}
}
}
export const MD_INPUT_DIRECTIVES = [MdPlaceholder, MdInput, MdHint];