diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 335acfaf3c40..6dba14d56f57 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -41,6 +41,8 @@ describe('MdInputContainer', function () { MdInputContainerZeroTestController, MdTextareaWithBindings, MdInputContainerWithDisabled, + MdInputContainerWithRequired, + MdInputContainerWithType, MdInputContainerMissingMdInputTestController ], }); @@ -236,16 +238,20 @@ describe('MdInputContainer', function () { let fixture = TestBed.createComponent(MdInputContainerPlaceholderAttrTestComponent); fixture.detectChanges(); - let el = fixture.debugElement.query(By.css('label')); - expect(el).toBeNull(); + let inputEl = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(fixture.debugElement.query(By.css('label'))).toBeNull(); + expect(inputEl.placeholder).toBe(''); fixture.componentInstance.placeholder = 'Other placeholder'; fixture.detectChanges(); - el = fixture.debugElement.query(By.css('label')); - expect(el).not.toBeNull(); - expect(el.nativeElement.textContent).toMatch('Other placeholder'); - expect(el.nativeElement.textContent).not.toMatch(/\*/g); + let labelEl = fixture.debugElement.query(By.css('label')); + + expect(inputEl.placeholder).toBe('Other placeholder'); + expect(labelEl).not.toBeNull(); + expect(labelEl.nativeElement.textContent).toMatch('Other placeholder'); + expect(labelEl.nativeElement.textContent).not.toMatch(/\*/g); })); it('supports placeholder element', async(() => { @@ -274,18 +280,51 @@ describe('MdInputContainer', function () { expect(el.nativeElement.textContent).toMatch(/hello\s+\*/g); }); - it('supports the disabled attribute', async(() => { + it('supports the disabled attribute as binding', async(() => { let fixture = TestBed.createComponent(MdInputContainerWithDisabled); fixture.detectChanges(); let underlineEl = fixture.debugElement.query(By.css('.md-input-underline')).nativeElement; + let inputEl = fixture.debugElement.query(By.css('input')).nativeElement; + expect(underlineEl.classList.contains('md-disabled')).toBe(false, 'should not be disabled'); + expect(inputEl.disabled).toBe(false); fixture.componentInstance.disabled = true; fixture.detectChanges(); + + expect(inputEl.disabled).toBe(true); expect(underlineEl.classList.contains('md-disabled')).toBe(true, 'should be disabled'); })); + it('supports the required attribute as binding', async(() => { + let fixture = TestBed.createComponent(MdInputContainerWithRequired); + fixture.detectChanges(); + + let inputEl = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(inputEl.required).toBe(false); + + fixture.componentInstance.required = true; + fixture.detectChanges(); + + expect(inputEl.required).toBe(true); + })); + + it('supports the type attribute as binding', async(() => { + let fixture = TestBed.createComponent(MdInputContainerWithType); + fixture.detectChanges(); + + let inputEl = fixture.debugElement.query(By.css('input')).nativeElement; + + expect(inputEl.type).toBe('text'); + + fixture.componentInstance.type = 'password'; + fixture.detectChanges(); + + expect(inputEl.type).toBe('password'); + })); + it('supports textarea', () => { let fixture = TestBed.createComponent(MdTextareaWithBindings); fixture.detectChanges(); @@ -310,6 +349,20 @@ class MdInputContainerWithDisabled { disabled: boolean; } +@Component({ + template: `` +}) +class MdInputContainerWithRequired { + required: boolean; +} + +@Component({ + template: `` +}) +class MdInputContainerWithType { + type: string; +} + @Component({ template: `` }) diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 37d18f2ff0db..8f4bd965f1ab 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -71,41 +71,56 @@ export class MdHint { selector: 'input[md-input], textarea[md-input], input[mat-input], textarea[mat-input]', host: { 'class': 'md-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', '(blur)': '_onBlur()', '(focus)': '_onFocus()', '(input)': '_onInput()', } }) export class MdInputDirective implements AfterContentInit { + + /** 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; + + /** The element's value. */ + value: any; + + /** Whether the element is focused or not. */ + focused = false; + /** Whether the element is disabled. */ @Input() get disabled() { return this._disabled; } set disabled(value: any) { this._disabled = coerceBooleanProperty(value); } - private _disabled = false; /** Unique id of the element. */ @Input() get id() { return this._id; }; - set id(value: string) { this._id = value || this._uid; } - private _id: string; + 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) { + if (this._placeholder !== value) { this._placeholder = value; this._placeholderChange.emit(this._placeholder); } } - private _placeholder = ''; - /** Whether the element is required. */ @Input() get required() { return this._required; } set required(value: any) { this._required = coerceBooleanProperty(value); } - private _required = false; /** Input type of the element. */ @Input() @@ -113,11 +128,14 @@ export class MdInputDirective implements AfterContentInit { set type(value: string) { this._type = value || 'text'; this._validateType(); - } - private _type = 'text'; - /** The element's value. */ - value: any; + // 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()) { + this._renderer.setElementProperty(this._elementRef.nativeElement, 'type', this._type); + } + } /** * Emits an event when the placeholder changes so that the `md-input-container` can re-validate. @@ -126,10 +144,7 @@ export class MdInputDirective implements AfterContentInit { get empty() { return (this.value == null || this.value === '') && !this._isNeverEmpty(); } - focused = false; - private get _uid() { return this._cachedUid = this._cachedUid || `md-input-${nextUniqueId++}`; } - private _cachedUid: string; private _neverEmptyInputTypes = [ 'date', @@ -168,12 +183,23 @@ export class MdInputDirective implements AfterContentInit { /** Make sure the input is a supported type. */ private _validateType() { - if (MD_INPUT_INVALID_TYPES.indexOf(this._type) != -1) { + if (MD_INPUT_INVALID_TYPES.indexOf(this._type) !== -1) { throw new MdInputContainerUnsupportedTypeError(this._type); } } - private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) != -1; } + private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) !== -1; } + + /** Determines if the component host is a textarea. If not recognizable it returns false. */ + private _isTextarea() { + let nativeElement = this._elementRef.nativeElement; + return nativeElement ? nativeElement.nodeName === 'textarea' : 'input'; + } + + /** Sets a property on the native input element. */ + private _setElementProperty(property: string, value: any) { + this._renderer.setElementProperty(this._elementRef.nativeElement, property, value); + } }