Skip to content

Commit b4b4224

Browse files
devversionkara
authored andcommitted
fix(input): ensure that property bindings work (#2431)
Fixes #2428
1 parent f2d73da commit b4b4224

File tree

2 files changed

+97
-23
lines changed

2 files changed

+97
-23
lines changed

src/lib/input/input-container.spec.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ describe('MdInputContainer', function () {
4141
MdInputContainerZeroTestController,
4242
MdTextareaWithBindings,
4343
MdInputContainerWithDisabled,
44+
MdInputContainerWithRequired,
45+
MdInputContainerWithType,
4446
MdInputContainerMissingMdInputTestController
4547
],
4648
});
@@ -236,16 +238,20 @@ describe('MdInputContainer', function () {
236238
let fixture = TestBed.createComponent(MdInputContainerPlaceholderAttrTestComponent);
237239
fixture.detectChanges();
238240

239-
let el = fixture.debugElement.query(By.css('label'));
240-
expect(el).toBeNull();
241+
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
242+
243+
expect(fixture.debugElement.query(By.css('label'))).toBeNull();
244+
expect(inputEl.placeholder).toBe('');
241245

242246
fixture.componentInstance.placeholder = 'Other placeholder';
243247
fixture.detectChanges();
244248

245-
el = fixture.debugElement.query(By.css('label'));
246-
expect(el).not.toBeNull();
247-
expect(el.nativeElement.textContent).toMatch('Other placeholder');
248-
expect(el.nativeElement.textContent).not.toMatch(/\*/g);
249+
let labelEl = fixture.debugElement.query(By.css('label'));
250+
251+
expect(inputEl.placeholder).toBe('Other placeholder');
252+
expect(labelEl).not.toBeNull();
253+
expect(labelEl.nativeElement.textContent).toMatch('Other placeholder');
254+
expect(labelEl.nativeElement.textContent).not.toMatch(/\*/g);
249255
}));
250256

251257
it('supports placeholder element', async(() => {
@@ -274,18 +280,51 @@ describe('MdInputContainer', function () {
274280
expect(el.nativeElement.textContent).toMatch(/hello\s+\*/g);
275281
});
276282

277-
it('supports the disabled attribute', async(() => {
283+
it('supports the disabled attribute as binding', async(() => {
278284
let fixture = TestBed.createComponent(MdInputContainerWithDisabled);
279285
fixture.detectChanges();
280286

281287
let underlineEl = fixture.debugElement.query(By.css('.md-input-underline')).nativeElement;
288+
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
289+
282290
expect(underlineEl.classList.contains('md-disabled')).toBe(false, 'should not be disabled');
291+
expect(inputEl.disabled).toBe(false);
283292

284293
fixture.componentInstance.disabled = true;
285294
fixture.detectChanges();
295+
296+
expect(inputEl.disabled).toBe(true);
286297
expect(underlineEl.classList.contains('md-disabled')).toBe(true, 'should be disabled');
287298
}));
288299

300+
it('supports the required attribute as binding', async(() => {
301+
let fixture = TestBed.createComponent(MdInputContainerWithRequired);
302+
fixture.detectChanges();
303+
304+
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
305+
306+
expect(inputEl.required).toBe(false);
307+
308+
fixture.componentInstance.required = true;
309+
fixture.detectChanges();
310+
311+
expect(inputEl.required).toBe(true);
312+
}));
313+
314+
it('supports the type attribute as binding', async(() => {
315+
let fixture = TestBed.createComponent(MdInputContainerWithType);
316+
fixture.detectChanges();
317+
318+
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
319+
320+
expect(inputEl.type).toBe('text');
321+
322+
fixture.componentInstance.type = 'password';
323+
fixture.detectChanges();
324+
325+
expect(inputEl.type).toBe('password');
326+
}));
327+
289328
it('supports textarea', () => {
290329
let fixture = TestBed.createComponent(MdTextareaWithBindings);
291330
fixture.detectChanges();
@@ -310,6 +349,20 @@ class MdInputContainerWithDisabled {
310349
disabled: boolean;
311350
}
312351

352+
@Component({
353+
template: `<md-input-container><input mdInput [required]="required"></md-input-container>`
354+
})
355+
class MdInputContainerWithRequired {
356+
required: boolean;
357+
}
358+
359+
@Component({
360+
template: `<md-input-container><input mdInput [type]="type"></md-input-container>`
361+
})
362+
class MdInputContainerWithType {
363+
type: string;
364+
}
365+
313366
@Component({
314367
template: `<md-input-container><input mdInput required placeholder="hello"></md-input-container>`
315368
})

src/lib/input/input-container.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,53 +75,71 @@ export class MdHint {
7575
`,
7676
host: {
7777
'class': 'md-input-element',
78+
// Native input properties that are overwritten by Angular inputs need to be synced with
79+
// the native input element. Otherwise property bindings for those don't work.
7880
'[id]': 'id',
81+
'[placeholder]': 'placeholder',
82+
'[disabled]': 'disabled',
83+
'[required]': 'required',
7984
'(blur)': '_onBlur()',
8085
'(focus)': '_onFocus()',
8186
'(input)': '_onInput()',
8287
}
8388
})
8489
export class MdInputDirective implements AfterContentInit {
90+
91+
/** Variables used as cache for getters and setters. */
92+
private _type = 'text';
93+
private _placeholder: string = '';
94+
private _disabled = false;
95+
private _required = false;
96+
private _id: string;
97+
private _cachedUid: string;
98+
99+
/** The element's value. */
100+
value: any;
101+
102+
/** Whether the element is focused or not. */
103+
focused = false;
104+
85105
/** Whether the element is disabled. */
86106
@Input()
87107
get disabled() { return this._disabled; }
88108
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
89-
private _disabled = false;
90109

91110
/** Unique id of the element. */
92111
@Input()
93112
get id() { return this._id; };
94-
set id(value: string) { this._id = value || this._uid; }
95-
private _id: string;
113+
set id(value: string) {this._id = value || this._uid; }
96114

97115
/** Placeholder attribute of the element. */
98116
@Input()
99117
get placeholder() { return this._placeholder; }
100118
set placeholder(value: string) {
101-
if (this._placeholder != value) {
119+
if (this._placeholder !== value) {
102120
this._placeholder = value;
103121
this._placeholderChange.emit(this._placeholder);
104122
}
105123
}
106-
private _placeholder = '';
107-
108124
/** Whether the element is required. */
109125
@Input()
110126
get required() { return this._required; }
111127
set required(value: any) { this._required = coerceBooleanProperty(value); }
112-
private _required = false;
113128

114129
/** Input type of the element. */
115130
@Input()
116131
get type() { return this._type; }
117132
set type(value: string) {
118133
this._type = value || 'text';
119134
this._validateType();
120-
}
121-
private _type = 'text';
122135

123-
/** The element's value. */
124-
value: any;
136+
// When using Angular inputs, developers are no longer able to set the properties on the native
137+
// input element. To ensure that bindings for `type` work, we need to sync the setter
138+
// with the native property. Textarea elements don't support the type property or attribute.
139+
if (!this._isTextarea() && getSupportedInputTypes().has(this._type)) {
140+
this._renderer.setElementProperty(this._elementRef.nativeElement, 'type', this._type);
141+
}
142+
}
125143

126144
/**
127145
* Emits an event when the placeholder changes so that the `md-input-container` can re-validate.
@@ -130,10 +148,7 @@ export class MdInputDirective implements AfterContentInit {
130148

131149
get empty() { return (this.value == null || this.value === '') && !this._isNeverEmpty(); }
132150

133-
focused = false;
134-
135151
private get _uid() { return this._cachedUid = this._cachedUid || `md-input-${nextUniqueId++}`; }
136-
private _cachedUid: string;
137152

138153
private _neverEmptyInputTypes = [
139154
'date',
@@ -172,12 +187,18 @@ export class MdInputDirective implements AfterContentInit {
172187

173188
/** Make sure the input is a supported type. */
174189
private _validateType() {
175-
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) != -1) {
190+
if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) {
176191
throw new MdInputContainerUnsupportedTypeError(this._type);
177192
}
178193
}
179194

180-
private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) != -1; }
195+
private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) !== -1; }
196+
197+
/** Determines if the component host is a textarea. If not recognizable it returns false. */
198+
private _isTextarea() {
199+
let nativeElement = this._elementRef.nativeElement;
200+
return nativeElement ? nativeElement.nodeName.toLowerCase() === 'textarea' : false;
201+
}
181202
}
182203

183204

0 commit comments

Comments
 (0)