Skip to content

Commit

Permalink
fix(input): ensure that property bindings work
Browse files Browse the repository at this point in the history
* Developers are currently not able to use property bindings for `required`, `disabled`, `type` and `placeholder`.
* This commit ensures that the Angular input setters are in sync with the native input element.
* Adds test to ensure that the property bindings work for the native input element (and not for the component instance as before)

Fixes angular#2428
  • Loading branch information
devversion committed Dec 27, 2016
1 parent 026c70a commit fdd44f3
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 23 deletions.
67 changes: 60 additions & 7 deletions src/lib/input/input-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ describe('MdInputContainer', function () {
MdInputContainerZeroTestController,
MdTextareaWithBindings,
MdInputContainerWithDisabled,
MdInputContainerWithRequired,
MdInputContainerWithType,
MdInputContainerMissingMdInputTestController
],
});
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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();
Expand All @@ -310,6 +349,20 @@ class MdInputContainerWithDisabled {
disabled: boolean;
}

@Component({
template: `<md-input-container><input md-input [required]="required"></md-input-container>`
})
class MdInputContainerWithRequired {
required: boolean;
}

@Component({
template: `<md-input-container><input md-input [type]="type"></md-input-container>`
})
class MdInputContainerWithType {
type: string;
}

@Component({
template: `<md-input-container><input md-input required placeholder="hello"></md-input-container>`
})
Expand Down
58 changes: 42 additions & 16 deletions src/lib/input/input-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,53 +71,71 @@ 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()
get type() { return this._type; }
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.
Expand All @@ -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',
Expand Down Expand Up @@ -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);
}
}


Expand Down

0 comments on commit fdd44f3

Please sign in to comment.