-
\ No newline at end of file
+
diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html
index 6950c40864d0..fb9826927a7f 100644
--- a/src/demo-app/tabs/tabs-demo.html
+++ b/src/demo-app/tabs/tabs-demo.html
@@ -127,7 +127,9 @@
Tab Group Demo - Dynamic Height
-
+
+
+
@@ -169,7 +171,9 @@
Tab Group Demo - Fixed Height
-
+
+
+
@@ -192,7 +196,9 @@
Async Tabs
-
+
+
+
@@ -205,4 +211,4 @@
Tabs with simplified api
This tab is about combustion!
-
\ No newline at end of file
+
diff --git a/src/demo-app/tooltip/tooltip-demo.html b/src/demo-app/tooltip/tooltip-demo.html
index 6da3a8b8efc6..bba9c3fa3f8b 100644
--- a/src/demo-app/tooltip/tooltip-demo.html
+++ b/src/demo-app/tooltip/tooltip-demo.html
@@ -24,7 +24,7 @@
Tooltip Demo
Message:
-
+
Mouse over to
diff --git a/src/lib/core/a11y/index.ts b/src/lib/core/a11y/index.ts
index 0be8e05abeb6..4c1955615510 100644
--- a/src/lib/core/a11y/index.ts
+++ b/src/lib/core/a11y/index.ts
@@ -3,7 +3,7 @@ import {FocusTrap} from './focus-trap';
import {MdLiveAnnouncer} from './live-announcer';
import {InteractivityChecker} from './interactivity-checker';
import {CommonModule} from '@angular/common';
-import {PlatformModule} from '../platform/platform';
+import {PlatformModule} from '../platform/index';
export const A11Y_PROVIDERS = [MdLiveAnnouncer, InteractivityChecker];
diff --git a/src/lib/core/core.ts b/src/lib/core/core.ts
index e5127292671d..0791f8842195 100644
--- a/src/lib/core/core.ts
+++ b/src/lib/core/core.ts
@@ -31,6 +31,7 @@ export * from './projection/projection';
// Platform
export * from './platform/platform';
+export * from './platform/features';
// Overlay
export {Overlay, OVERLAY_PROVIDERS} from './overlay/overlay';
diff --git a/src/lib/core/platform/features.ts b/src/lib/core/platform/features.ts
new file mode 100644
index 000000000000..1f6c62ab0c3f
--- /dev/null
+++ b/src/lib/core/platform/features.ts
@@ -0,0 +1,36 @@
+let supportedInputTypes: Set;
+
+/** @returns {Set} the input types supported by this browser. */
+export function getSupportedInputTypes(): Set {
+ if (!supportedInputTypes) {
+ let featureTestInput = document.createElement('input');
+ supportedInputTypes = new Set([
+ 'button',
+ 'checkbox',
+ 'color',
+ 'date',
+ 'datetime-local',
+ 'email',
+ 'file',
+ 'hidden',
+ 'image',
+ 'month',
+ 'number',
+ 'password',
+ 'radio',
+ 'range',
+ 'reset',
+ 'search',
+ 'submit',
+ 'tel',
+ 'text',
+ 'time',
+ 'url',
+ 'week',
+ ].filter(value => {
+ featureTestInput.setAttribute('type', value);
+ return featureTestInput.type === value;
+ }));
+ }
+ return supportedInputTypes;
+}
diff --git a/src/lib/core/platform/index.ts b/src/lib/core/platform/index.ts
new file mode 100644
index 000000000000..d7e93942da24
--- /dev/null
+++ b/src/lib/core/platform/index.ts
@@ -0,0 +1,13 @@
+import {NgModule, ModuleWithProviders} from '@angular/core';
+import {MdPlatform} from './platform';
+
+
+@NgModule({})
+export class PlatformModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: PlatformModule,
+ providers: [MdPlatform],
+ };
+ }
+}
diff --git a/src/lib/core/platform/platform.ts b/src/lib/core/platform/platform.ts
index 44477046e4ef..3b2e655c7d13 100644
--- a/src/lib/core/platform/platform.ts
+++ b/src/lib/core/platform/platform.ts
@@ -1,4 +1,4 @@
-import {Injectable, NgModule, ModuleWithProviders} from '@angular/core';
+import {Injectable} from '@angular/core';
declare const window: any;
@@ -12,7 +12,6 @@ const hasV8BreakIterator = (window.Intl && (window.Intl as any).v8BreakIterator)
*/
@Injectable()
export class MdPlatform {
-
/** Layout Engines */
EDGE = /(edge)/i.test(navigator.userAgent);
TRIDENT = /(msie|trident)/i.test(navigator.userAgent);
@@ -35,15 +34,4 @@ export class MdPlatform {
// Trident on mobile adds the android platform to the userAgent to trick detections.
ANDROID = /android/i.test(navigator.userAgent) && !this.TRIDENT;
-
-}
-
-@NgModule({})
-export class PlatformModule {
- static forRoot(): ModuleWithProviders {
- return {
- ngModule: PlatformModule,
- providers: [MdPlatform],
- };
- }
}
diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss
index 0c424275b7ee..e4a52a093f54 100644
--- a/src/lib/input/_input-theme.scss
+++ b/src/lib/input/_input-theme.scss
@@ -39,8 +39,8 @@
}
// See md-input-placeholder-floating mixin in input.scss
- md-input input:-webkit-autofill + .md-input-placeholder, .md-input-placeholder.md-float.md-focused {
-
+ input.md-input-element:-webkit-autofill + .md-input-placeholder,
+ .md-input-placeholder.md-float.md-focused {
.md-placeholder-required {
color: $input-required-placeholder-color;
}
diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts
index c630aefeaf7f..ab131b1f1f40 100644
--- a/src/lib/input/index.ts
+++ b/src/lib/input/index.ts
@@ -1,2 +1,4 @@
+export * from './autosize'
export * from './input';
-export {MdTextareaAutosize} from './autosize';
+export * from './input-container';
+export * from './input-container-errors';
diff --git a/src/lib/input/input-container-errors.ts b/src/lib/input/input-container-errors.ts
new file mode 100644
index 000000000000..214997f60f64
--- /dev/null
+++ b/src/lib/input/input-container-errors.ts
@@ -0,0 +1,22 @@
+import {MdError} from '../core/errors/error';
+
+
+export class MdInputContainerPlaceholderConflictError extends MdError {
+ constructor() {
+ super('Placeholder attribute and child element were both specified.');
+ }
+}
+
+
+export class MdInputContainerUnsupportedTypeError extends MdError {
+ constructor(type: string) {
+ super(`Input type "${type}" isn't supported by md-input-container.`);
+ }
+}
+
+
+export class MdInputContainerDuplicatedHintError extends MdError {
+ constructor(align: string) {
+ super(`A hint was already declared for 'align="${align}"'.`);
+ }
+}
diff --git a/src/lib/input/input-container.html b/src/lib/input/input-container.html
new file mode 100644
index 000000000000..04f0f8ee1d16
--- /dev/null
+++ b/src/lib/input/input-container.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{hintLabel}}
+
+
diff --git a/src/lib/input/input-container.scss b/src/lib/input/input-container.scss
new file mode 100644
index 000000000000..28c6086e5945
--- /dev/null
+++ b/src/lib/input/input-container.scss
@@ -0,0 +1,29 @@
+@import '../core/style/variables';
+
+md-input-container {
+ display: inline-block;
+ position: relative;
+ font-family: $md-font-family;
+ line-height: normal;
+
+ // To avoid problems with text-align.
+ text-align: left;
+
+ [dir='rtl'] & {
+ text-align: right;
+ }
+}
+
+.md-input-element {
+ &::placeholder {
+ visibility: hidden;
+ }
+
+ .md-end & {
+ text-align: right;
+
+ [dir='rtl'] & {
+ text-align: left;
+ }
+ }
+}
diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts
new file mode 100644
index 000000000000..c0c2bd1985c6
--- /dev/null
+++ b/src/lib/input/input-container.spec.ts
@@ -0,0 +1,408 @@
+import {async, TestBed, inject} from '@angular/core/testing';
+import {Component} from '@angular/core';
+import {FormsModule, ReactiveFormsModule} from '@angular/forms';
+import {By} from '@angular/platform-browser';
+import {MdInputModule} from './input';
+import {MdInputContainer} from './input-container';
+import {MdPlatform} from '../core/platform/platform';
+import {PlatformModule} from '../core/platform/index';
+
+
+describe('MdInputContainer', function () {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ MdInputModule.forRoot(),
+ PlatformModule.forRoot(),
+ FormsModule,
+ ReactiveFormsModule
+ ],
+ declarations: [
+ MdInputContainerPlaceholderRequiredTestComponent,
+ MdInputContainerPlaceholderElementTestComponent,
+ MdInputContainerPlaceholderAttrTestComponent,
+ MdInputContainerHintLabel2TestController,
+ MdInputContainerHintLabelTestController,
+ MdInputContainerInvalidTypeTestController,
+ MdInputContainerInvalidPlaceholderTestController,
+ MdInputContainerInvalidHint2TestController,
+ MdInputContainerInvalidHintTestController,
+ MdInputContainerBaseTestController,
+ MdInputContainerWithId,
+ MdInputContainerDateTestController,
+ MdInputContainerTextTestController,
+ MdInputContainerPasswordTestController,
+ MdInputContainerNumberTestController,
+ MdTextareaWithBindings,
+ MdInputContainerWithDisabled,
+ ],
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ it('should default to floating placeholders', () => {
+ let fixture = TestBed.createComponent(MdInputContainerBaseTestController);
+ fixture.detectChanges();
+
+ let inputContainer = fixture.debugElement.query(By.directive(MdInputContainer))
+ .componentInstance as MdInputContainer;
+ expect(inputContainer.floatingPlaceholder).toBe(true,
+ 'Expected MdInputContainer to default to having floating placeholders turned on');
+ });
+
+ it('should not be treated as empty if type is date',
+ inject([MdPlatform], (platform: MdPlatform) => {
+ if (!(platform.TRIDENT || platform.FIREFOX)) {
+ let fixture = TestBed.createComponent(MdInputContainerDateTestController);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el).not.toBeNull();
+ expect(el.classList.contains('md-empty')).toBe(false);
+ }
+ }));
+
+ // Firefox and IE don't support type="date" and fallback to type="text".
+ it('should be treated as empty if type is date on Firefox and IE',
+ inject([MdPlatform], (platform: MdPlatform) => {
+ if (platform.TRIDENT || platform.FIREFOX) {
+ let fixture = TestBed.createComponent(MdInputContainerDateTestController);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el).not.toBeNull();
+ expect(el.classList.contains('md-empty')).toBe(true);
+ }
+ }));
+
+ it('should treat text input type as empty at init', () => {
+ let fixture = TestBed.createComponent(MdInputContainerTextTestController);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el).not.toBeNull();
+ expect(el.classList.contains('md-empty')).toBe(true);
+ });
+
+ it('should treat password input type as empty at init', () => {
+ let fixture = TestBed.createComponent(MdInputContainerPasswordTestController);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el).not.toBeNull();
+ expect(el.classList.contains('md-empty')).toBe(true);
+ });
+
+ it('should treat number input type as empty at init', () => {
+ let fixture = TestBed.createComponent(MdInputContainerNumberTestController);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el).not.toBeNull();
+ expect(el.classList.contains('md-empty')).toBe(true);
+ });
+
+ it('should not be empty after input entered', async(() => {
+ let fixture = TestBed.createComponent(MdInputContainerTextTestController);
+ fixture.detectChanges();
+
+ let inputEl = fixture.debugElement.query(By.css('input'));
+ let el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el).not.toBeNull();
+ expect(el.classList.contains('md-empty')).toBe(true, 'should be empty');
+
+ inputEl.nativeElement.value = 'hello';
+ // Simulate input event.
+ inputEl.triggerEventHandler('input', {target: inputEl.nativeElement});
+ fixture.detectChanges();
+
+ el = fixture.debugElement.query(By.css('label')).nativeElement;
+ expect(el.classList.contains('md-empty')).toBe(false, 'should not be empty');
+ }));
+
+ it('should add id', () => {
+ let fixture = TestBed.createComponent(MdInputContainerTextTestController);
+ fixture.detectChanges();
+
+ const inputElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('input')).nativeElement;
+ const labelElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('label')).nativeElement;
+
+ expect(inputElement.id).toBeTruthy();
+ expect(inputElement.id).toEqual(labelElement.getAttribute('for'));
+ });
+
+ it('should not overwrite existing id', () => {
+ let fixture = TestBed.createComponent(MdInputContainerWithId);
+ fixture.detectChanges();
+
+ const inputElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('input')).nativeElement;
+ const labelElement: HTMLInputElement =
+ fixture.debugElement.query(By.css('label')).nativeElement;
+
+ expect(inputElement.id).toBe('test-id');
+ expect(labelElement.getAttribute('for')).toBe('test-id');
+ });
+
+ it('validates there\'s only one hint label per side', () => {
+ let fixture = TestBed.createComponent(MdInputContainerInvalidHintTestController);
+
+ expect(() => fixture.detectChanges()).toThrow();
+ // TODO(jelbourn): .toThrow(new MdInputContainerDuplicatedHintError('start'));
+ // See https://github.com/angular/angular/issues/8348
+ });
+
+ it('validates there\'s only one hint label per side (attribute)', () => {
+ let fixture = TestBed.createComponent(MdInputContainerInvalidHint2TestController);
+
+ expect(() => fixture.detectChanges()).toThrow();
+ // TODO(jelbourn): .toThrow(new MdInputContainerDuplicatedHintError('start'));
+ // See https://github.com/angular/angular/issues/8348
+ });
+
+ it('validates there\'s only one placeholder', () => {
+ let fixture = TestBed.createComponent(MdInputContainerInvalidPlaceholderTestController);
+
+ expect(() => fixture.detectChanges()).toThrow();
+ // TODO(jelbourn): .toThrow(new MdInputContainerPlaceholderConflictError());
+ // See https://github.com/angular/angular/issues/8348
+ });
+
+ it('validates the type', () => {
+ let fixture = TestBed.createComponent(MdInputContainerInvalidTypeTestController);
+
+ // Technically this throws during the OnChanges detection phase,
+ // so the error is really a ChangeDetectionError and it becomes
+ // hard to build a full exception to compare with.
+ // We just check for any exception in this case.
+ expect(() => fixture.detectChanges()).toThrow(
+ /* new MdInputContainerUnsupportedTypeError('file') */);
+ });
+
+ it('supports hint labels attribute', () => {
+ let fixture = TestBed.createComponent(MdInputContainerHintLabelTestController);
+ 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(MdInputContainerHintLabel2TestController);
+ 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', async(() => {
+ let fixture = TestBed.createComponent(MdInputContainerPlaceholderAttrTestComponent);
+ 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', async(() => {
+ let fixture = TestBed.createComponent(MdInputContainerPlaceholderElementTestComponent);
+ 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(MdInputContainerPlaceholderRequiredTestComponent);
+ fixture.detectChanges();
+
+ let el = fixture.debugElement.query(By.css('label'));
+ expect(el).not.toBeNull();
+ expect(el.nativeElement.textContent).toMatch(/hello\s+\*/g);
+ });
+
+ it('supports the disabled attribute', async(() => {
+ let fixture = TestBed.createComponent(MdInputContainerWithDisabled);
+ fixture.detectChanges();
+
+ let underlineEl = fixture.debugElement.query(By.css('.md-input-underline')).nativeElement;
+ expect(underlineEl.classList.contains('md-disabled')).toBe(false, 'should not be disabled');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+ expect(underlineEl.classList.contains('md-disabled')).toBe(true, 'should be disabled');
+ }));
+
+ it('supports textarea', () => {
+ let fixture = TestBed.createComponent(MdTextareaWithBindings);
+ fixture.detectChanges();
+
+ const textarea: HTMLTextAreaElement = fixture.nativeElement.querySelector('textarea');
+ expect(textarea).not.toBeNull();
+ });
+});
+
+@Component({
+ template: `
+
+
+ `
+})
+class MdInputContainerWithId {}
+
+@Component({
+ template: ``
+})
+class MdInputContainerWithDisabled {
+ disabled: boolean;
+}
+
+@Component({
+ template: ``
+})
+class MdInputContainerPlaceholderRequiredTestComponent {}
+
+@Component({
+ template: `
+
+
+ {{placeholder}}
+ `
+})
+class MdInputContainerPlaceholderElementTestComponent {
+ placeholder: string = 'Default Placeholder';
+}
+
+@Component({
+ template: ``
+})
+class MdInputContainerPlaceholderAttrTestComponent {
+ placeholder: string = '';
+}
+
+@Component({
+ template: `{{label}}`
+})
+class MdInputContainerHintLabel2TestController {
+ label: string = '';
+}
+
+@Component({
+ template: ``
+})
+class MdInputContainerHintLabelTestController {
+ label: string = '';
+}
+
+@Component({
+ template: ``
+})
+class MdInputContainerInvalidTypeTestController {}
+
+@Component({
+ template: `
+
+
+ World
+ `
+})
+class MdInputContainerInvalidPlaceholderTestController {}
+
+@Component({
+ template: `
+
+
+ World
+ `
+})
+class MdInputContainerInvalidHint2TestController {}
+
+@Component({
+ template: `
+
+
+ Hello
+ World
+ `
+})
+class MdInputContainerInvalidHintTestController {}
+
+@Component({
+ template: ``
+})
+class MdInputContainerBaseTestController {
+ model: any = '';
+}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MdInputContainerDateTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MdInputContainerTextTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MdInputContainerPasswordTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MdInputContainerNumberTestController {}
+
+@Component({
+ template: `
+
+
+ `
+})
+class MdTextareaWithBindings {
+ rows: number = 4;
+ cols: number = 8;
+ wrap: string = 'hard';
+}
diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts
new file mode 100644
index 000000000000..e50645f90f91
--- /dev/null
+++ b/src/lib/input/input-container.ts
@@ -0,0 +1,266 @@
+import {
+ Component,
+ Input,
+ Directive,
+ AfterContentInit,
+ ContentChild,
+ ContentChildren,
+ ElementRef,
+ QueryList,
+ ViewEncapsulation,
+ Optional,
+ Output,
+ EventEmitter,
+ Renderer
+} from '@angular/core';
+import {coerceBooleanProperty} from '../core';
+import {NgControl} from '@angular/forms';
+import {getSupportedInputTypes} from '../core/platform/features';
+import {
+ MdInputContainerUnsupportedTypeError,
+ MdInputContainerPlaceholderConflictError,
+ MdInputContainerDuplicatedHintError
+} from './input-container-errors';
+
+
+// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError.
+const MD_INPUT_INVALID_TYPES = [
+ 'button',
+ 'checkbox',
+ 'color',
+ 'file',
+ 'hidden',
+ 'image',
+ 'radio',
+ 'range',
+ 'reset',
+ 'submit'
+];
+
+
+let nextUniqueId = 0;
+
+
+/**
+ * The placeholder directive. The content can declare this to implement more
+ * complex placeholders.
+ */
+@Directive({
+ selector: 'md-placeholder, mat-placeholder'
+})
+export class MdPlaceholder {}
+
+
+/** The hint directive, used to tag content as hint labels (going under the input). */
+@Directive({
+ selector: 'md-hint, mat-hint',
+ host: {
+ 'class': 'md-hint',
+ '[class.md-right]': 'align == "end"',
+ }
+})
+export class MdHint {
+ // Whether to align the hint label at the start or end of the line.
+ @Input() align: 'start' | 'end' = 'start';
+}
+
+
+/** The input directive, used to mark the input that `MdInputContainer` is wrapping. */
+@Directive({
+ selector: 'input[md-input], textarea[md-input], input[mat-input], textarea[mat-input]',
+ host: {
+ 'class': 'md-input-element',
+ '[id]': 'id',
+ '(blur)': '_onBlur()',
+ '(focus)': '_onFocus()',
+ '(input)': '_onInput()',
+ }
+})
+export class MdInputDirective implements AfterContentInit {
+ @Input()
+ get disabled() { return this._disabled; }
+ set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
+ private _disabled = false;
+
+ @Input()
+ get id() { return this._id; };
+ set id(value: string) { this._id = value || this._uid; }
+ private _id: string;
+
+ @Input()
+ get placeholder() { return this._placeholder; }
+ set placeholder(value: string) {
+ if (this._placeholder != value) {
+ this._placeholder = value;
+ this._placeholderChange.emit(this._placeholder);
+ }
+ }
+ private _placeholder = '';
+
+ @Input()
+ get required() { return this._required; }
+ set required(value: any) { this._required = coerceBooleanProperty(value); }
+ private _required = false;
+
+ @Input()
+ get type() { return this._type; }
+ set type(value: string) {
+ this._type = value || 'text';
+ this._validateType();
+ }
+ private _type = 'text';
+
+ value: any;
+
+ /**
+ * Emits an event when the placeholder changes so that the `md-input-container` can re-validate.
+ */
+ @Output() _placeholderChange = new EventEmitter();
+
+ 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',
+ 'datetime',
+ 'datetime-local',
+ 'month',
+ 'time',
+ 'week'
+ ].filter(t => getSupportedInputTypes().has(t));
+
+ constructor(private _elementRef: ElementRef,
+ private _renderer: Renderer,
+ @Optional() private _ngControl: NgControl) {
+ // Force setter to be called in case id was not specified.
+ this.id = this.id;
+
+ if (this._ngControl) {
+ this._ngControl.valueChanges.subscribe((value) => {
+ this.value = value;
+ });
+ }
+ }
+
+ ngAfterContentInit() {
+ this.value = this._elementRef.nativeElement.value;
+ }
+
+ /** Focus the input element. */
+ focus() { this._renderer.invokeElementMethod(this._elementRef.nativeElement, 'focus'); }
+
+ _onFocus() { this.focused = true; }
+
+ _onBlur() { this.focused = false; }
+
+ _onInput() { this.value = this._elementRef.nativeElement.value; }
+
+ /** Make sure the input is a supported type. */
+ private _validateType() {
+ if (MD_INPUT_INVALID_TYPES.indexOf(this._type) != -1) {
+ throw new MdInputContainerUnsupportedTypeError(this._type);
+ }
+ }
+
+ private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) != -1; }
+}
+
+
+/**
+ * Component that represents a text input. It encapsulates the HTMLElement and
+ * improve on its behaviour, along with styling it according to the Material Design.
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'md-input-container, mat-input-container',
+ templateUrl: 'input-container.html',
+ styleUrls: ['input.css', 'input-container.css'],
+ host: {
+ // Remove align attribute to prevent it from interfering with layout.
+ '[attr.align]': 'null',
+ '(click)': '_focusInput()',
+ },
+ encapsulation: ViewEncapsulation.None,
+})
+export class MdInputContainer implements AfterContentInit {
+ @Input() align: 'start' | 'end' = 'start';
+
+ @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary';
+
+ @Input()
+ get hintLabel() { return this._hintLabel; }
+ set hintLabel(value: string) {
+ this._hintLabel = value;
+ this._validateHints();
+ }
+ private _hintLabel = '';
+
+ @Input()
+ get floatingPlaceholder(): boolean { return this._floatingPlaceholder; }
+ set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); }
+ private _floatingPlaceholder: boolean = true;
+
+ @ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
+
+ @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
+
+ @ContentChildren(MdHint) _hintChildren: QueryList;
+
+ ngAfterContentInit() {
+ this._validateHints();
+ this._validatePlaceholders();
+
+ // Re-validate when things change.
+ this._hintChildren.changes.subscribe(() => {
+ this._validateHints();
+ });
+ this._mdInputChild._placeholderChange.subscribe(() => {
+ this._validatePlaceholders();
+ });
+ }
+
+ /** Whether the input has a placeholder. */
+ _hasPlaceholder(): boolean {
+ return !!this._mdInputChild.placeholder || !!this._placeholderChild;
+ }
+
+ _focusInput() { this._mdInputChild.focus(); }
+
+ /**
+ * Ensure that there is only one placeholder (either `input` attribute or child element with the
+ * `md-placeholder` attribute.
+ */
+ private _validatePlaceholders() {
+ if (this._mdInputChild.placeholder && this._placeholderChild) {
+ throw new MdInputContainerPlaceholderConflictError();
+ }
+ }
+
+ /**
+ * Ensure that there is a maximum of one of each `` alignment specified, with the
+ * attribute being considered as `align="start"`.
+ */
+ private _validateHints() {
+ if (this._hintChildren) {
+ let startHint: MdHint = null;
+ let endHint: MdHint = null;
+ this._hintChildren.forEach((hint: MdHint) => {
+ if (hint.align == 'start') {
+ if (startHint || this.hintLabel) {
+ throw new MdInputContainerDuplicatedHintError('start');
+ }
+ startHint = hint;
+ } else if (hint.align == 'end') {
+ if (endHint) {
+ throw new MdInputContainerDuplicatedHintError('end');
+ }
+ endHint = hint;
+ }
+ });
+ }
+ }
+}
diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts
index 22023da9caa7..c6c5735d499a 100644
--- a/src/lib/input/input.ts
+++ b/src/lib/input/input.ts
@@ -3,7 +3,6 @@ import {
Component,
HostBinding,
Input,
- Directive,
AfterContentInit,
ContentChild,
SimpleChange,
@@ -17,13 +16,15 @@ import {
Output,
NgModule,
ModuleWithProviders,
- ViewEncapsulation,
+ ViewEncapsulation
} 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 {MdPlaceholder, MdInputContainer, MdHint, MdInputDirective} from './input-container';
import {MdTextareaAutosize} from './autosize';
+import {PlatformModule} from '../core/platform/index';
const noop = () => {};
@@ -65,30 +66,6 @@ export class MdInputDuplicatedHintError extends MdError {
}
-
-/**
- * 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.
@@ -369,15 +346,33 @@ export class MdInput implements ControlValueAccessor, AfterContentInit, OnChange
@NgModule({
- declarations: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize],
- imports: [CommonModule, FormsModule],
- exports: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize],
+ declarations: [
+ MdInput,
+ MdPlaceholder,
+ MdInputContainer,
+ MdHint,
+ MdTextareaAutosize,
+ MdInputDirective
+ ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ PlatformModule,
+ ],
+ exports: [
+ MdInput,
+ MdPlaceholder,
+ MdInputContainer,
+ MdHint,
+ MdTextareaAutosize,
+ MdInputDirective
+ ],
})
export class MdInputModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: MdInputModule,
- providers: []
+ providers: PlatformModule.forRoot().providers,
};
}
}
diff --git a/src/lib/module.ts b/src/lib/module.ts
index 8819356f0aa3..c74a5bcf61a5 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -32,7 +32,7 @@ import {MdToolbarModule} from './toolbar/index';
import {MdTooltipModule} from './tooltip/index';
import {MdMenuModule} from './menu/index';
import {MdDialogModule} from './dialog/index';
-import {PlatformModule} from './core/platform/platform';
+import {PlatformModule} from './core/platform/index';
import {MdAutocompleteModule} from './autocomplete/index';
const MATERIAL_MODULES = [
diff --git a/src/lib/sidenav/sidenav.spec.ts b/src/lib/sidenav/sidenav.spec.ts
index 5678797639b1..34062c9e9d39 100644
--- a/src/lib/sidenav/sidenav.spec.ts
+++ b/src/lib/sidenav/sidenav.spec.ts
@@ -3,7 +3,7 @@ import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdSidenav, MdSidenavModule, MdSidenavToggleResult} from './sidenav';
import {A11yModule} from '../core/a11y/index';
-import {PlatformModule} from '../core/platform/platform';
+import {PlatformModule} from '../core/platform/index';
import {ESCAPE} from '../core/keyboard/keycodes';