.
position: absolute;
@@ -133,7 +133,7 @@ md-input, md-textarea {
// Show the placeholder above the input when it's not empty, or focused.
&.md-float:not(.md-empty), &.md-float.md-focused {
- @include md-input-placeholder-floating;
+ @include md-placeholder-floating;
}
[dir='rtl'] & {
diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts
index d1591c615210..9b295b4a795f 100644
--- a/src/lib/input/input.spec.ts
+++ b/src/lib/input/input.spec.ts
@@ -5,58 +5,18 @@ import {
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
-import {MdInput, MdInputModule} from './input';
-
-function isInternetExplorer11() {
- return 'ActiveXObject' in window;
-}
+import {MdInput, MdInputModule} from './index';
+import {ProjectionModule} from '../core/projection/projection';
describe('MdInput', function () {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MdInputModule.forRoot(), FormsModule],
+ imports: [MdInputModule.forRoot(), FormsModule, ProjectionModule.forRoot()],
declarations: [
+ MdInputStyleClassTransferedTestComponent,
MdInputNumberTypeConservedTestComponent,
- MdInputPlaceholderRequiredTestComponent,
- MdInputPlaceholderElementTestComponent,
- MdInputPlaceholderAttrTestComponent,
- MdInputHintLabel2TestController,
- MdInputHintLabelTestController,
MdInputInvalidTypeTestController,
- MdInputInvalidPlaceholderTestController,
- MdInputInvalidHint2TestController,
- MdInputInvalidHintTestController,
MdInputBaseTestController,
- MdInputAriaTestController,
- MdInputWithBlurAndFocusEvents,
- MdInputWithNameTestController,
- MdInputWithId,
- MdInputWithAutocomplete,
- MdInputWithUnboundAutocomplete,
- MdInputWithUnboundAutocompleteWithValue,
- MdInputWithAutocorrect,
- MdInputWithUnboundAutocorrect,
- MdInputWithAutocapitalize,
- MdInputWithUnboundAutocapitalize,
- MdInputWithAutofocus,
- MdInputWithUnboundAutofocus,
- MdInputWithReadonly,
- MdInputWithUnboundReadonly,
- MdInputWithSpellcheck,
- MdInputWithUnboundSpellcheck,
- MdInputWithDisabled,
- MdInputWithUnboundDisabled,
- MdInputWithRequired,
- MdInputWithUnboundRequired,
- MdInputWithList,
- MdInputWithMax,
- MdInputWithMin,
- MdInputWithStep,
- MdInputWithTabindex,
- MdInputDateTestController,
- MdInputTextTestController,
- MdInputPasswordTestController,
- MdInputNumberTestController,
MdTextareaWithBindings,
],
});
@@ -71,72 +31,11 @@ describe('MdInput', function () {
expect(fixture.debugElement.query(By.css('input'))).toBeTruthy();
});
- it('should default to flating placeholders', () => {
- let fixture = TestBed.createComponent(MdInputBaseTestController);
- fixture.detectChanges();
-
- let mdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance as MdInput;
- expect(mdInput.floatingPlaceholder)
- .toBe(true, 'Expected MdInput to default to having floating placeholders turned on');
- });
-
- it('should not be treated as empty if type is date', () => {
- if (isInternetExplorer11()) {
- return;
- }
- let fixture = TestBed.createComponent(MdInputDateTestController);
- fixture.componentInstance.placeholder = 'Placeholder';
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(el).not.toBeNull();
- expect(el.className.includes('md-empty')).toBe(false);
- });
-
- it('should treat text input type as empty at init', () => {
- if (isInternetExplorer11()) {
- return;
- }
- let fixture = TestBed.createComponent(MdInputTextTestController);
- fixture.componentInstance.placeholder = 'Placeholder';
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(el).not.toBeNull();
- expect(el.className.includes('md-empty')).toBe(true);
- });
-
- it('should treat password input type as empty at init', () => {
- if (isInternetExplorer11()) {
- return;
- }
- let fixture = TestBed.createComponent(MdInputPasswordTestController);
- fixture.componentInstance.placeholder = 'Placeholder';
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(el).not.toBeNull();
- expect(el.className.includes('md-empty')).toBe(true);
- });
-
- it('should treat number input type as empty at init', () => {
- if (isInternetExplorer11()) {
- return;
- }
- let fixture = TestBed.createComponent(MdInputNumberTestController);
- fixture.componentInstance.placeholder = 'Placeholder';
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(el).not.toBeNull();
- expect(el.className.includes('md-empty')).toBe(true);
- });
-
// TODO(kara): update when core/testing adds fix
it('support ngModel', async(() => {
let fixture = TestBed.createComponent(MdInputBaseTestController);
-
fixture.detectChanges();
+
let instance = fixture.componentInstance;
let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
@@ -151,24 +50,21 @@ describe('MdInput', function () {
});
}));
- it('should have a different ID for outer element and internal input', () => {
- let fixture = TestBed.createComponent(MdInputWithId);
+ it('moves the class and style to the outer container', async(() => {
+ let fixture = TestBed.createComponent(MdInputStyleClassTransferedTestComponent);
fixture.detectChanges();
- const componentElement: HTMLElement =
- fixture.debugElement.query(By.directive(MdInput)).nativeElement;
- const inputElement: HTMLInputElement =
- fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(componentElement.id).toBe('test-id');
- expect(inputElement.id).toBeTruthy();
- expect(inputElement.id).not.toBe(componentElement.id);
- });
+ let el = fixture.debugElement.query(By.directive(MdInput)).nativeElement;
+ // We set the input element's class.
+ expect(el.getAttribute('class')).toBe('md-input-element');
+ expect(el.getAttribute('style')).toBeNull();
+ }));
it('counts characters', async(() => {
let fixture = TestBed.createComponent(MdInputBaseTestController);
- let instance = fixture.componentInstance;
fixture.detectChanges();
+
+ let instance = fixture.componentInstance;
let inputInstance = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
expect(inputInstance.characterCount).toEqual(0);
@@ -179,44 +75,6 @@ describe('MdInput', function () {
});
}));
- it('copies aria attributes to the inner input', () => {
- let fixture = TestBed.createComponent(MdInputAriaTestController);
- let instance = fixture.componentInstance;
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
- expect(el.getAttribute('aria-label')).toEqual('label');
- instance.ariaLabel = 'label 2';
- fixture.detectChanges();
- expect(el.getAttribute('aria-label')).toEqual('label 2');
-
- expect(el.getAttribute('aria-disabled')).toBeTruthy();
- });
-
- it(`validates there's only one hint label per side`, () => {
- let fixture = TestBed.createComponent(MdInputInvalidHintTestController);
-
- expect(() => fixture.detectChanges()).toThrow();
- // TODO(jelbourn): .toThrow(new MdInputDuplicatedHintError('start'));
- // See https://github.com/angular/angular/issues/8348
- });
-
- it(`validates there's only one hint label per side (attribute)`, () => {
- let fixture = TestBed.createComponent(MdInputInvalidHint2TestController);
-
- expect(() => fixture.detectChanges()).toThrow();
- // TODO(jelbourn): .toThrow(new MdInputDuplicatedHintError('start'));
- // See https://github.com/angular/angular/issues/8348
- });
-
- it('validates there\'s only one placeholder', () => {
- let fixture = TestBed.createComponent(MdInputInvalidPlaceholderTestController);
-
- expect(() => fixture.detectChanges()).toThrow();
- // TODO(jelbourn): .toThrow(new MdInputPlaceholderConflictError());
- // See https://github.com/angular/angular/issues/8348
- });
-
it('validates the type', () => {
let fixture = TestBed.createComponent(MdInputInvalidTypeTestController);
@@ -227,400 +85,6 @@ describe('MdInput', function () {
expect(() => fixture.detectChanges()).toThrow(/* new MdInputUnsupportedTypeError('file') */);
});
- it('supports hint labels attribute', () => {
- let fixture = TestBed.createComponent(MdInputHintLabelTestController);
- fixture.detectChanges();
-
- // If the hint label is empty, expect no label.
- expect(fixture.debugElement.query(By.css('.md-hint'))).toBeNull();
-
- fixture.componentInstance.label = 'label';
- fixture.detectChanges();
- expect(fixture.debugElement.query(By.css('.md-hint'))).not.toBeNull();
- });
-
- it('supports hint labels elements', () => {
- let fixture = TestBed.createComponent(MdInputHintLabel2TestController);
- fixture.detectChanges();
-
- // In this case, we should have an empty
.
- let el = fixture.debugElement.query(By.css('md-hint')).nativeElement;
- expect(el.textContent).toBeFalsy();
-
- fixture.componentInstance.label = 'label';
- fixture.detectChanges();
- el = fixture.debugElement.query(By.css('md-hint')).nativeElement;
- expect(el.textContent).toBe('label');
- });
-
- it('supports placeholder attribute', () => {
- let fixture = TestBed.createComponent(MdInputPlaceholderAttrTestComponent);
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label'));
- expect(el).toBeNull();
-
- 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);
- });
-
- it('supports placeholder element', () => {
- let fixture = TestBed.createComponent(MdInputPlaceholderElementTestComponent);
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label'));
- expect(el).not.toBeNull();
- expect(el.nativeElement.textContent).toMatch('Default Placeholder');
-
- 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);
- });
-
- it('supports placeholder required star', () => {
- let fixture = TestBed.createComponent(MdInputPlaceholderRequiredTestComponent);
- fixture.detectChanges();
-
- let el = fixture.debugElement.query(By.css('label'));
- expect(el).not.toBeNull();
- expect(el.nativeElement.textContent).toMatch(/hello\s+\*/g);
- });
-
- it('supports number types and conserved its value type from Angular', () => {
- let fixture = TestBed.createComponent(MdInputNumberTypeConservedTestComponent);
- fixture.detectChanges();
-
- const input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- const inputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- // Fake a `change` event being triggered.
- inputElement.value = '3';
- input._handleChange( {target: inputElement});
-
- fixture.detectChanges();
- expect(fixture.componentInstance.value).toBe(3);
- expect(typeof fixture.componentInstance.value).toBe('number');
- });
-
- it('supports blur and focus events', () => {
- let fixture = TestBed.createComponent(MdInputWithBlurAndFocusEvents);
- const testComponent = fixture.componentInstance;
- const inputComponent = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- const fakeEvent = {};
-
- spyOn(testComponent, 'onFocus');
- spyOn(testComponent, 'onBlur');
-
- expect(testComponent.onFocus).not.toHaveBeenCalled();
- expect(testComponent.onBlur).not.toHaveBeenCalled();
-
- inputComponent._handleFocus(fakeEvent);
- expect(testComponent.onFocus).toHaveBeenCalledWith(fakeEvent);
-
- inputComponent._handleBlur(fakeEvent);
- expect(testComponent.onBlur).toHaveBeenCalledWith(fakeEvent);
- });
-
- it('supports the autoComplete attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithAutocomplete);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autocomplete')).toBeNull();
-
- input.autocomplete = 'on';
- fixture.detectChanges();
- expect(el.getAttribute('autocomplete')).toEqual('on');
- });
-
- it('supports the autoCorrect attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithAutocorrect);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autocorrect')).toBeNull();
-
- input.autocorrect = 'on';
- fixture.detectChanges();
- expect(el.getAttribute('autocorrect')).toEqual('on');
- });
-
- it('supports the autoCapitalize attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithAutocapitalize);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autocapitalize')).toBeNull();
-
- input.autocapitalize = 'on';
- fixture.detectChanges();
- expect(el.getAttribute('autocapitalize')).toEqual('on');
- });
-
- it('supports the autoComplete attribute as an unbound attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundAutocomplete);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autocomplete')).toEqual('');
- });
-
- it('supports the autoComplete attribute as an unbound value attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundAutocompleteWithValue);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autocomplete')).toEqual('name');
- });
-
- it('supports the autoFocus attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithAutofocus);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autofocus')).toBeNull();
-
- input.autofocus = true;
- fixture.detectChanges();
- expect(el.getAttribute('autofocus')).toEqual('');
- });
-
- it('supports the autoFocus attribute as an unbound attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundAutofocus);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('autofocus')).toEqual('');
- });
-
- it('supports the disabled attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithDisabled);
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- input.disabled = false;
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
- expect(el).not.toBeNull();
-
- fixture.detectChanges();
- expect(el.getAttribute('disabled')).toEqual(null);
-
- fixture.componentInstance.disabled = true;
- fixture.detectChanges();
- fixture.whenStable().then(() => {
- expect(el.getAttribute('disabled')).toEqual('');
- });
- });
-
- it('supports the disabled attribute as an unbound attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundDisabled);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- fixture.whenStable().then(() => {
- expect(el.getAttribute('disabled')).toEqual('');
- });
- });
-
- it('supports the list attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithList);
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- input.disabled = false;
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
- fixture.detectChanges();
- expect(el.getAttribute('list')).toEqual(null);
-
- input.list = 'datalist-id';
- fixture.detectChanges();
- expect(el.getAttribute('list')).toEqual('datalist-id');
- });
-
- it('supports the max attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithMax);
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- input.disabled = false;
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
- expect(el).not.toBeNull();
-
- fixture.detectChanges();
- expect(el.getAttribute('max')).toEqual(null);
-
- input.max = 10;
- fixture.detectChanges();
- expect(el.getAttribute('max')).toEqual('10');
-
- input.max = '2000-01-02';
- fixture.detectChanges();
- expect(el.getAttribute('max')).toEqual('2000-01-02');
- });
-
- it('supports the min attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithMin);
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- input.disabled = false;
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
- expect(el).not.toBeNull();
- fixture.detectChanges();
- expect(el.getAttribute('min')).toEqual(null);
-
- input.min = 10;
- fixture.detectChanges();
- expect(el.getAttribute('min')).toEqual('10');
-
- input.min = '2000-01-02';
- fixture.detectChanges();
- expect(el.getAttribute('min')).toEqual('2000-01-02');
- });
-
- it('supports the readOnly attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithReadonly);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('readonly')).toBeNull();
-
- input.readonly = true;
- fixture.detectChanges();
- expect(el.getAttribute('readonly')).toEqual('');
- });
-
- it('supports the readOnly attribute as an unbound attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundReadonly);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('readonly')).toEqual('');
- });
-
- it('supports the required attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithRequired);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('required')).toBeNull();
-
- input.required = true;
- fixture.detectChanges();
- expect(el.getAttribute('required')).toEqual('');
- });
-
- it('supports the required attribute as an unbound attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundRequired);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('required')).toEqual('');
- });
-
- it('supports the spellCheck attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithSpellcheck);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('spellcheck')).toEqual('false');
-
- input.spellcheck = true;
- fixture.detectChanges();
- expect(el.getAttribute('spellcheck')).toEqual('true');
- });
-
- it('supports the spellCheck attribute as an unbound attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithUnboundSpellcheck);
- fixture.detectChanges();
-
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('spellcheck')).toEqual('true');
- });
-
- it('supports the step attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithStep);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('step')).toEqual(null);
-
- input.step = 0.5;
- fixture.detectChanges();
- expect(el.getAttribute('step')).toEqual('0.5');
- });
-
- it('supports the tabIndex attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithTabindex);
- fixture.detectChanges();
-
- let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance;
- let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement;
-
- expect(el).not.toBeNull();
- expect(el.getAttribute('tabindex')).toEqual(null);
-
- input.tabindex = 1;
- fixture.detectChanges();
- expect(el.getAttribute('tabindex')).toEqual('1');
- });
-
- it('supports a name attribute', () => {
- let fixture = TestBed.createComponent(MdInputWithNameTestController);
-
- fixture.detectChanges();
-
- const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input'))
- .nativeElement;
-
- expect(inputElement.name).toBe('some-name');
- });
-
describe('md-textarea', () => {
it('supports the rows, cols, and wrap attributes', () => {
let fixture = TestBed.createComponent(MdTextareaWithBindings);
@@ -633,182 +97,39 @@ describe('MdInput', function () {
expect(textarea.wrap).toBe('hard');
});
});
-
});
-@Component({template: ``})
-class MdInputWithId {
- value: number = 0;
+@Component({template: ``})
+class MdInputStyleClassTransferedTestComponent {
}
-@Component({template: ``})
+@Component({template: ``})
class MdInputNumberTypeConservedTestComponent {
value: number = 0;
}
-@Component({template: ``})
-class MdInputPlaceholderRequiredTestComponent {
-}
-
-@Component({template: ` {{placeholder}} `})
-class MdInputPlaceholderElementTestComponent {
- placeholder: string = 'Default Placeholder';
-}
-
-@Component({template: ``})
-class MdInputPlaceholderAttrTestComponent {
- placeholder: string = '';
-}
+@Component({template: ``})
+class MdInputPlaceholderStringTestController { }
-@Component({template: ` {{label}} `})
-class MdInputHintLabel2TestController {
- label: string = '';
-}
-
-@Component({template: ``})
-class MdInputHintLabelTestController {
- label: string = '';
-}
+@Component({template: `
+
+ template placeholder
+`})
+class MdInputPlaceholderTemplateTestController { }
-@Component({template: ``})
+@Component({template: ``})
class MdInputInvalidTypeTestController { }
-@Component({
- template: `
-
- World
- `
-})
-class MdInputInvalidPlaceholderTestController { }
-
-@Component({
- template: `
-
- World
- `
-})
-class MdInputInvalidHint2TestController { }
-
-@Component({
- template: `
-
- Hello
- World
- `
-})
-class MdInputInvalidHintTestController { }
-
-@Component({template: ``})
+@Component({template: ``})
class MdInputBaseTestController {
model: any = '';
}
-@Component({template:
- ``})
-class MdInputAriaTestController {
- ariaLabel: string = 'label';
- ariaDisabled: boolean = true;
-}
-
-@Component({template: ``})
-class MdInputWithBlurAndFocusEvents {
- onBlur(event: FocusEvent) {}
- onFocus(event: FocusEvent) {}
-}
-
-@Component({template: ``})
-class MdInputWithNameTestController { }
-
-@Component({template: ``})
-class MdInputWithAutocomplete { }
-
-@Component({template: ``})
-class MdInputWithUnboundAutocomplete { }
-
-@Component({template: ``})
-class MdInputWithUnboundAutocompleteWithValue { }
-
-@Component({template: ``})
-class MdInputWithAutocorrect { }
-
-@Component({template: ``})
-class MdInputWithUnboundAutocorrect { }
-
-@Component({template: ``})
-class MdInputWithAutocapitalize { }
-
-@Component({template: ``})
-class MdInputWithUnboundAutocapitalize { }
-
-@Component({template: ``})
-class MdInputWithAutofocus { }
-
-@Component({template: ``})
-class MdInputWithUnboundAutofocus { }
-
-@Component({template: ``})
-class MdInputWithReadonly { }
-
-@Component({template: ``})
-class MdInputWithUnboundReadonly { }
-
-@Component({template: ``})
-class MdInputWithSpellcheck { }
-
-@Component({template: ``})
-class MdInputWithUnboundSpellcheck { }
-
-@Component({template: ``})
-class MdInputWithDisabled {
- disabled: boolean;
-}
-
-@Component({template: ``})
-class MdInputWithUnboundDisabled { }
-
-@Component({template: ``})
-class MdInputWithRequired { }
-
-@Component({template: ``})
-class MdInputWithUnboundRequired { }
-
-@Component({template: ``})
-class MdInputWithList { }
-
-@Component({template: ``})
-class MdInputWithMax { }
-
-@Component({template: ``})
-class MdInputWithMin { }
-
-@Component({template: ``})
-class MdInputWithStep { }
-
-@Component({template: ``})
-class MdInputWithTabindex { }
-
-@Component({template: ``})
-class MdInputDateTestController {
- placeholder: string = '';
-}
-
-@Component({template: ``})
-class MdInputTextTestController {
- placeholder: string = '';
-}
-
-@Component({template: ``})
-class MdInputPasswordTestController {
- placeholder: string = '';
-}
-
-@Component({template: ``})
-class MdInputNumberTestController {
- placeholder: string = '';
-}
-
-@Component({template:
- ``})
+@Component({
+ template: `
+
+ `})
class MdTextareaWithBindings {
rows: number = 4;
cols: number = 8;
diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts
index 3003b3e3e71b..6f931dfdfdff 100644
--- a/src/lib/input/input.ts
+++ b/src/lib/input/input.ts
@@ -1,38 +1,33 @@
import {
- forwardRef,
Component,
+ ElementRef,
HostBinding,
+ HostListener,
Input,
- Directive,
- AfterContentInit,
- ContentChild,
- SimpleChange,
- ContentChildren,
+ OnInit,
+ TemplateRef,
ViewChild,
- ElementRef,
- QueryList,
- OnChanges,
- EventEmitter,
- Output,
- NgModule,
- ModuleWithProviders,
ViewEncapsulation,
+ forwardRef,
} from '@angular/core';
-import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms';
-import {CommonModule} from '@angular/common';
-import {MdError, coerceBooleanProperty} from '../core';
-import {Observable} from 'rxjs/Observable';
-import {MdTextareaAutosize} from './autosize';
-
+import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
+import {
+ DomProjection,
+ DomProjectionHost,
+ MdError,
+ coerceBooleanProperty
+} from '../core';
+import {MD_PLACEHOLDER_HOST, MdPlaceholderHost} from './placeholder';
+import {PortalHost} from '../core/portal/portal';
+import {PortalHostDirective} from '../core/portal/portal-directives';
-const noop = () => {};
+export class MdInputUnsupportedTypeError extends MdError {
+ constructor(type: string) {
+ super(`Input type "${type}" isn't supported by md-input.`);
+ }
+}
-export const MD_INPUT_CONTROL_VALUE_ACCESSOR: any = {
- provide: 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 = [
@@ -42,334 +37,104 @@ const MD_INPUT_INVALID_INPUT_TYPE = [
];
-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 HTMLElement and
- * improve on its behaviour, along with styling it according to the Material Design.
+ * A component that can be attached to either an input or a textarea. This is the master
*/
@Component({
moduleId: module.id,
- selector: 'md-input, md-textarea',
+ selector: 'input[md-input], textarea[md-textarea]',
templateUrl: 'input.html',
styleUrls: ['input.css'],
- providers: [MD_INPUT_CONTROL_VALUE_ACCESSOR],
- host: {'(click)' : 'focus()'},
encapsulation: ViewEncapsulation.None,
+ providers: [
+ { provide: MD_PLACEHOLDER_HOST, useExisting: forwardRef(() => MdInput) }
+ ],
+ host: {
+ // This is to remove the properties of the `input md-input` itself. We still want to use them
+ // as an @Input though, so we use HostBinding.
+ 'class': '',
+ 'style': '',
+ 'attr.align': ''
+ }
})
-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;
+export class MdInput implements OnInit, MdPlaceholderHost {
+ @ViewChild(DomProjectionHost) _host: DomProjectionHost;
+ @ViewChild('suffix') _suffix: TemplateRef;
+ @ViewChild('prefix') _prefix: TemplateRef;
- private _ariaDisabled: boolean;
- private _ariaRequired: boolean;
- private _ariaInvalid: boolean;
-
- @Input('aria-disabled')
- get ariaDisabled(): boolean { return this._ariaDisabled; }
- set ariaDisabled(value) { this._ariaDisabled = coerceBooleanProperty(value); }
-
- @Input('aria-required')
- get ariaRequired(): boolean { return this._ariaRequired; }
- set ariaRequired(value) { this._ariaRequired = coerceBooleanProperty(value); }
-
- @Input('aria-invalid')
- get ariaInvalid(): boolean { return this._ariaInvalid; }
- set ariaInvalid(value) { this._ariaInvalid = coerceBooleanProperty(value); }
-
- /**
- * Content directives.
- */
- @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
- @ContentChildren(MdHint) _hintChildren: QueryList;
+ @ViewChild('suffixWrapper', { read: PortalHostDirective }) placeholderPortalHost: PortalHost;
+ private _focused: boolean = false;
- /** Readonly properties. */
- get focused() { return this._focused; }
- get empty() { return (this._value == null || this._value === '') && this.type !== 'date'; }
- get characterCount(): number {
- return this.empty ? 0 : ('' + this._value).length;
+ @Input('class') _cssClass: string = '';
+ @Input('style') _cssStyle: string = '';
+ get _safeCssStyle(): SafeStyle {
+ return this._dom.bypassSecurityTrustStyle(this._cssStyle || '');
}
- get inputId(): string { return `${this.id}-input`; }
-
- /**
- * Bindings.
- */
- @Input() align: 'start' | 'end' = 'start';
- @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary';
- @Input() hintLabel: string = '';
+ @HostBinding('attr.class') get _attrClass(): any { return 'md-input-element'; }
+ @HostBinding('attr.style') get _attrStyle(): any { return null; }
- @Input() autocomplete: string;
- @Input() autocorrect: string;
- @Input() autocapitalize: string;
- @Input() id: string = `md-input-${nextUniqueId++}`;
- @Input() list: string = null;
- @Input() max: string | number = null;
- @Input() maxlength: number = null;
- @Input() min: string | number = null;
- @Input() minlength: number = null;
- @Input() placeholder: string = null;
- @Input() step: number = null;
- @Input() tabindex: number = null;
- @Input() type: string = 'text';
- @Input() name: string = null;
+ @Input('type') _type: string;
- // textarea-specific
- @Input() rows: number = null;
- @Input() cols: number = null;
- @Input() wrap: 'soft' | 'hard' = null;
+ constructor(private _dom: DomSanitizer, private _projection: DomProjection,
+ private _ref: ElementRef) {}
- private _floatingPlaceholder: boolean = true;
- private _autofocus: boolean = false;
- private _disabled: boolean = false;
- private _readonly: boolean = false;
- private _required: boolean = false;
- private _spellcheck: boolean = false;
-
- @Input()
- get floatingPlaceholder(): boolean { return this._floatingPlaceholder; }
- set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); }
+ _suffixTemplate(): TemplateRef {
+ if (typeof this.mdSuffix == 'string') {
+ return this._suffix;
+ } else if (this.mdSuffix instanceof TemplateRef) {
+ return this.mdSuffix;
+ } else {
+ return null;
+ }
+ }
- @Input()
- get autofocus(): boolean { return this._autofocus; }
- set autofocus(value) { this._autofocus = coerceBooleanProperty(value); }
+ _prefixTemplate(): TemplateRef {
+ if (typeof this.mdPrefix == 'string') {
+ return this._prefix;
+ } else if (this.mdPrefix instanceof TemplateRef) {
+ return this.mdPrefix;
+ } else {
+ return null;
+ }
+ }
@Input()
get disabled(): boolean { return this._disabled; }
set disabled(value) { this._disabled = coerceBooleanProperty(value); }
+ private _disabled: boolean = false;
- @Input()
- get readonly(): boolean { return this._readonly; }
- set readonly(value) { this._readonly = coerceBooleanProperty(value); }
-
- @Input()
- get required(): boolean { return this._required; }
- set required(value) { this._required = coerceBooleanProperty(value); }
-
- @Input()
- get spellcheck(): boolean { return this._spellcheck; }
- set spellcheck(value) { this._spellcheck = coerceBooleanProperty(value); }
-
-
- private _blurEmitter: EventEmitter = new EventEmitter();
- private _focusEmitter: EventEmitter = new EventEmitter();
-
- @Output('blur')
- get onBlur(): Observable {
- return this._blurEmitter.asObservable();
+ get _value() { return this._ref.nativeElement.value; }
+ get empty() {
+ return (this._value == null || this._value === '')
+ && this._ref.nativeElement.type !== 'date';
}
-
- @Output('focus')
- get onFocus(): Observable {
- return this._focusEmitter.asObservable();
+ get characterCount(): number {
+ return this.empty ? 0 : ('' + this._value).length;
}
- 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);
+ ngOnInit() {
+ this._projection.project(this._ref, this._host);
+
+ if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this._type) != -1) {
+ throw new MdInputUnsupportedTypeError(this._type);
}
}
- // 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') get _align(): any { return null; }
-
-
- @ViewChild('input') _inputElement: ElementRef;
-
- _elementType: 'input' | 'textarea';
-
- constructor(elementRef: ElementRef) {
- // Set the element type depending on normalized selector used(md-input / md-textarea)
- this._elementType = elementRef.nativeElement.nodeName.toLowerCase() === 'md-input' ?
- 'input' :
- 'textarea';
- }
+ get focused() { return this._focused; }
- /** Set focus on input */
- focus() {
- this._inputElement.nativeElement.focus();
- }
+ @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary';
+ @Input() align: 'start' | 'end' = 'start';
+ @Input() mdPrefix: string | TemplateRef;
+ @Input() mdSuffix: string | TemplateRef;
- _handleFocus(event: FocusEvent) {
+ @HostListener('focus') _onFocus() {
this._focused = true;
- this._focusEmitter.emit(event);
}
-
- _handleBlur(event: FocusEvent) {
+ @HostListener('blur') _onBlur() {
this._focused = false;
- this._onTouchedCallback();
- this._blurEmitter.emit(event);
}
- _handleChange(event: Event) {
- this.value = (event.target).value;
- this._onTouchedCallback();
- }
-
- _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 are mutually exclusive.
- * - type attribute is not one of the forbidden types (see constant at the top).
- * - Maximum one of each `` 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;
- }
- });
- }
- }
-}
-
-
-@NgModule({
- declarations: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize],
- imports: [CommonModule, FormsModule],
- exports: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize],
-})
-export class MdInputModule {
- static forRoot(): ModuleWithProviders {
- return {
- ngModule: MdInputModule,
- providers: []
- };
+ focus() {
+ this._ref.nativeElement.focus();
}
}
diff --git a/src/lib/input/placeholder.html b/src/lib/input/placeholder.html
new file mode 100644
index 000000000000..38b6c22380c0
--- /dev/null
+++ b/src/lib/input/placeholder.html
@@ -0,0 +1,12 @@
+
+
+{{content}}
diff --git a/src/lib/input/placeholder.scss b/src/lib/input/placeholder.scss
new file mode 100644
index 000000000000..139597f9cb07
--- /dev/null
+++ b/src/lib/input/placeholder.scss
@@ -0,0 +1,2 @@
+
+
diff --git a/src/lib/input/placeholder.spec.ts b/src/lib/input/placeholder.spec.ts
new file mode 100644
index 000000000000..5c373e717788
--- /dev/null
+++ b/src/lib/input/placeholder.spec.ts
@@ -0,0 +1,67 @@
+import {
+ async,
+ TestBed,
+} from '@angular/core/testing';
+import {Component} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {MdInputModule} from './index';
+import {ProjectionModule} from '../core/projection/projection';
+
+describe('MdPlaceholder', function () {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [MdInputModule.forRoot(), FormsModule, ProjectionModule.forRoot()],
+ declarations: [
+ MdPlaceholderStringTestController,
+ MdPlaceholderTemplateTestController,
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ it('works with string placeholder', () => {
+ let fixture = TestBed.createComponent(MdPlaceholderStringTestController);
+ fixture.detectChanges();
+
+ let instance: MdPlaceholderStringTestController = fixture.componentInstance;
+ let el: HTMLElement = fixture.nativeElement;
+ let labelEl: HTMLLabelElement = el.querySelector('label');
+ expect(labelEl).not.toBeNull();
+ expect(labelEl.textContent).toEqual('string placeholder');
+
+ instance.required = true;
+ fixture.detectChanges();
+ expect(labelEl.textContent).toEqual('string placeholder *');
+ });
+
+ it('works with template placeholder', () => {
+ let fixture = TestBed.createComponent(MdPlaceholderTemplateTestController);
+ fixture.detectChanges();
+
+ let instance: MdPlaceholderTemplateTestController = fixture.componentInstance;
+ let el: HTMLElement = fixture.nativeElement;
+ let labelEl: HTMLLabelElement = el.querySelector('label');
+ expect(labelEl).not.toBeNull();
+ expect(labelEl.textContent).toEqual('template placeholder');
+ expect(labelEl.querySelector('b')).not.toBeNull();
+ expect(labelEl.querySelector('b').textContent).toEqual('placeholder');
+
+ instance.required = true;
+ fixture.detectChanges();
+ expect(labelEl.textContent).toEqual('template placeholder *');
+ });
+});
+
+@Component({template: ``})
+class MdPlaceholderStringTestController {
+ required: boolean = false;
+}
+
+@Component({template: `
+
+ template placeholder
+`})
+class MdPlaceholderTemplateTestController {
+ required: boolean = false;
+}
diff --git a/src/lib/input/placeholder.ts b/src/lib/input/placeholder.ts
new file mode 100644
index 000000000000..10e2b03e8605
--- /dev/null
+++ b/src/lib/input/placeholder.ts
@@ -0,0 +1,94 @@
+import {
+ Component,
+ ComponentRef,
+ Directive,
+ HostBinding,
+ HostListener,
+ Inject,
+ Input,
+ OpaqueToken,
+ TemplateRef,
+ ViewChild,
+ ViewContainerRef,
+} from '@angular/core';
+import {PortalHost} from '../core';
+import {ComponentPortal} from '../core/portal/portal';
+import {coerceBooleanProperty} from '../core/coersion/boolean-property';
+
+
+/**
+ * A token to provide the host interface that MdPlaceholder will use to inject itself in
+ * the View tree. If this token isn't on the host of the directive, an error will be thrown.
+ * @type {OpaqueToken}
+ */
+export const MD_PLACEHOLDER_HOST = new OpaqueToken('mdPlaceholderHost');
+
+/**
+ * Interface for components that want to host an MdPlaceholder directive.
+ */
+export interface MdPlaceholderHost {
+ placeholderPortalHost: PortalHost;
+ readonly dividerColor: string;
+ readonly empty: boolean;
+}
+
+
+@Component({
+ moduleId: module.id,
+ selector: '',
+ templateUrl: 'placeholder.html',
+ styleUrls: ['placeholder.css'],
+})
+export class MdPlaceholderContent {
+ @ViewChild('stringTemplate') public _stringTemplate: TemplateRef;
+ public content: string | TemplateRef = '';
+ public placeholder: MdPlaceholder = null;
+
+ _template(): TemplateRef {
+ if (typeof this.content == 'string') {
+ return this._stringTemplate;
+ } else if (this.content instanceof TemplateRef) {
+ return this.content;
+ } else {
+ return null;
+ }
+ }
+}
+
+
+@Directive({
+ selector: '[md-input][placeholder], [md-textarea][placeholder], [md-placeholder]:not(template)',
+})
+export class MdPlaceholder {
+ @Input('required') set required(v: boolean | null) {
+ this._required = coerceBooleanProperty(v);
+ }
+ @Input('placeholder') placeholder: string | TemplateRef;
+ @Input('floatingPlaceholder') set floatingPlaceholder(v: boolean) {
+ this._floatingPlaceholder = coerceBooleanProperty(v);
+ }
+ get floatingPlaceholder(): boolean { return this._floatingPlaceholder; }
+
+ get dividerColor() { return this._host.dividerColor; }
+ get empty() { return this._host.empty; }
+
+ @HostBinding('attr.placeholder') _attrPlaceholder: any = null;
+
+ @HostListener('focus') _onFocus() { this._focused = true; }
+ @HostListener('blur') _onBlur() { this._focused = false; }
+
+ _focused: boolean = false;
+ _required: boolean = false;
+ _floatingPlaceholder: boolean = true;
+
+ constructor(@Inject(MD_PLACEHOLDER_HOST) public _host: MdPlaceholderHost,
+ private _vcr: ViewContainerRef) {}
+
+ ngOnInit() {
+ const portal = new ComponentPortal(MdPlaceholderContent, this._vcr);
+ const componentRef: ComponentRef =
+ this._host.placeholderPortalHost.attach(portal);
+ componentRef.instance.content = this.placeholder;
+ componentRef.instance.placeholder = this;
+ }
+}