placeholder="Pick a date"
(dateInput)="onDateInput($event)"
(dateChange)="onDateChange($event)">
+
+
"{{resultPickerModel.getError('mdDatepickerParse').text}}" is not a valid date!
@@ -47,13 +54,6 @@
Result
Too late!Date unavailable!
-
-
Last input: {{lastDateInput}}
Last change: {{lastDateChange}}
@@ -83,9 +83,9 @@
Input disabled datepicker
-
-
+
Input disabled, datepicker popup enabled
@@ -95,7 +95,7 @@
Input disabled, datepicker popup enabled
+
-
diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts
index 15f17c50c210..c05b4c6f9bd7 100644
--- a/src/demo-app/demo-material-module.ts
+++ b/src/demo-app/demo-material-module.ts
@@ -11,6 +11,7 @@ import {
MdDatepickerModule,
MdDialogModule,
MdExpansionModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,
@@ -50,6 +51,7 @@ import {CdkTableModule} from '@angular/cdk/table';
MdDatepickerModule,
MdDialogModule,
MdExpansionModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,
diff --git a/src/demo-app/tabs/tabs-demo.scss b/src/demo-app/tabs/tabs-demo.scss
index 767ff004d70e..fd69322feeb4 100644
--- a/src/demo-app/tabs/tabs-demo.scss
+++ b/src/demo-app/tabs/tabs-demo.scss
@@ -43,7 +43,7 @@ tabs-demo .mat-card {
margin-top: 8px;
}
- .mat-input-container {
+ .mat-form-field {
width: 100px;
}
diff --git a/src/e2e-app/e2e-app-module.ts b/src/e2e-app/e2e-app-module.ts
index 606293fb165c..21c5afabb684 100644
--- a/src/e2e-app/e2e-app-module.ts
+++ b/src/e2e-app/e2e-app-module.ts
@@ -20,10 +20,24 @@ import {InputE2E} from './input/input-e2e';
import {SidenavE2E} from './sidenav/sidenav-e2e';
import {BlockScrollStrategyE2E} from './block-scroll-strategy/block-scroll-strategy-e2e';
import {
- OverlayContainer, FullscreenOverlayContainer, MdGridListModule, MdProgressBarModule,
- MdProgressSpinnerModule, MdTabsModule, MdRadioModule, MdSlideToggleModule, MdMenuModule,
- MdListModule, MdInputModule, MdIconModule, MdDialogModule, MdCheckboxModule, MdButtonModule,
- MdSidenavModule, MdNativeDateModule,
+ FullscreenOverlayContainer,
+ MdButtonModule,
+ MdCheckboxModule,
+ MdDialogModule,
+ MdFormFieldModule,
+ MdGridListModule,
+ MdIconModule,
+ MdInputModule,
+ MdListModule,
+ MdMenuModule,
+ MdNativeDateModule,
+ MdProgressBarModule,
+ MdProgressSpinnerModule,
+ MdRadioModule,
+ MdSidenavModule,
+ MdSlideToggleModule,
+ MdTabsModule,
+ OverlayContainer,
} from '@angular/material';
import {ExampleModule} from '@angular/material-examples';
@@ -35,6 +49,7 @@ import {ExampleModule} from '@angular/material-examples';
MdButtonModule,
MdCheckboxModule,
MdDialogModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,
diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts
index ee98bc35fee3..234927443da4 100644
--- a/src/lib/autocomplete/autocomplete-trigger.ts
+++ b/src/lib/autocomplete/autocomplete-trigger.ts
@@ -40,7 +40,7 @@ import {Observable} from 'rxjs/Observable';
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
import {ENTER, UP_ARROW, DOWN_ARROW, ESCAPE} from '../core/keyboard/keycodes';
import {Directionality} from '../core/bidi/index';
-import {MdInputContainer} from '../input/input-container';
+import {MdFormField} from '../form-field/index';
import {Subscription} from 'rxjs/Subscription';
import {merge} from 'rxjs/observable/merge';
import {fromEvent} from 'rxjs/observable/fromEvent';
@@ -153,7 +153,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
private _changeDetectorRef: ChangeDetectorRef,
@Inject(MD_AUTOCOMPLETE_SCROLL_STRATEGY) private _scrollStrategy,
@Optional() private _dir: Directionality,
- @Optional() @Host() private _inputContainer: MdInputContainer,
+ @Optional() @Host() private _formField: MdFormField,
@Optional() @Inject(DOCUMENT) private _document: any) {}
ngOnDestroy() {
@@ -246,8 +246,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
fromEvent(this._document, 'touchend')
)).call(filter, (event: MouseEvent | TouchEvent) => {
const clickTarget = event.target as HTMLElement;
- const inputContainer = this._inputContainer ?
- this._inputContainer._elementRef.nativeElement : null;
+ const inputContainer = this._formField ?
+ this._formField._elementRef.nativeElement : null;
return this._panelOpen &&
clickTarget !== this._element.nativeElement &&
@@ -329,8 +329,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
* This method manually floats the placeholder until the panel can be closed.
*/
private _floatPlaceholder(): void {
- if (this._inputContainer && this._inputContainer.floatPlaceholder === 'auto') {
- this._inputContainer.floatPlaceholder = 'always';
+ if (this._formField && this._formField.floatPlaceholder === 'auto') {
+ this._formField.floatPlaceholder = 'always';
this._manuallyFloatingPlaceholder = true;
}
}
@@ -338,7 +338,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
/** If the placeholder has been manually elevated, return it to its normal state. */
private _resetPlaceholder(): void {
if (this._manuallyFloatingPlaceholder) {
- this._inputContainer.floatPlaceholder = 'auto';
+ this._formField.floatPlaceholder = 'auto';
this._manuallyFloatingPlaceholder = false;
}
}
@@ -408,10 +408,10 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
// The display value can also be the number zero and shouldn't fall back to an empty string.
const inputValue = toDisplay != null ? toDisplay : '';
- // If it's used in a Material container, we should set it through
- // the property so it can go through the change detection.
- if (this._inputContainer) {
- this._inputContainer._mdInputChild.value = inputValue;
+ // If it's used within a `MdFormField`, we should set it through the property so it can go
+ // through change detection.
+ if (this._formField) {
+ this._formField._control.value = inputValue;
} else {
this._element.nativeElement.value = inputValue;
}
@@ -469,7 +469,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
}
private _getConnectedElement(): ElementRef {
- return this._inputContainer ? this._inputContainer._connectionContainerRef : this._element;
+ return this._formField ? this._formField._connectionContainerRef : this._element;
}
/** Returns the width of the input element, so the panel width can match it. */
diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts
index 927cddd98b0a..1599f189717a 100644
--- a/src/lib/autocomplete/autocomplete.spec.ts
+++ b/src/lib/autocomplete/autocomplete.spec.ts
@@ -23,7 +23,7 @@ import {Directionality, Direction} from '../core/bidi/index';
import {Subscription} from 'rxjs/Subscription';
import {ENTER, DOWN_ARROW, SPACE, UP_ARROW, ESCAPE} from '../core/keyboard/keycodes';
import {MdOption} from '../core/option/option';
-import {MdInputContainer} from '../input/input-container';
+import {MdFormField, MdFormFieldModule} from '../form-field/index';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
import {createKeyboardEvent, dispatchFakeEvent, typeInElement} from '@angular/cdk/testing';
@@ -41,6 +41,7 @@ describe('MdAutocomplete', () => {
TestBed.configureTestingModule({
imports: [
MdAutocompleteModule,
+ MdFormFieldModule,
MdInputModule,
FormsModule,
ReactiveFormsModule,
@@ -517,7 +518,7 @@ describe('MdAutocomplete', () => {
it('should disable input in view when disabled programmatically', () => {
const inputUnderline =
- fixture.debugElement.query(By.css('.mat-input-underline')).nativeElement;
+ fixture.debugElement.query(By.css('.mat-form-field-underline')).nativeElement;
expect(input.disabled)
.toBe(false, `Expected input to start out enabled in view.`);
@@ -1319,10 +1320,10 @@ describe('MdAutocomplete', () => {
fixture.detectChanges();
const input = fixture.nativeElement.querySelector('input');
- const placeholder = fixture.nativeElement.querySelector('.mat-input-placeholder');
+ const placeholder = fixture.nativeElement.querySelector('.mat-form-field-placeholder');
expect(input.value).toBe('California');
- expect(placeholder.classList).not.toContain('mat-empty');
+ expect(placeholder.classList).not.toContain('mat-form-field-empty');
}));
});
@@ -1417,7 +1418,7 @@ class SimpleAutocomplete implements OnDestroy {
@ViewChild(MdAutocompleteTrigger) trigger: MdAutocompleteTrigger;
@ViewChild(MdAutocomplete) panel: MdAutocomplete;
- @ViewChild(MdInputContainer) inputContainer: MdInputContainer;
+ @ViewChild(MdFormField) inputContainer: MdFormField;
@ViewChildren(MdOption) options: QueryList;
states = [
diff --git a/src/lib/chips/chip-list.spec.ts b/src/lib/chips/chip-list.spec.ts
index f3b91356d9c9..5d656d96cb6e 100644
--- a/src/lib/chips/chip-list.spec.ts
+++ b/src/lib/chips/chip-list.spec.ts
@@ -9,6 +9,7 @@ import {createKeyboardEvent} from '@angular/cdk/testing';
import {MdInputModule} from '../input/index';
import {LEFT_ARROW, RIGHT_ARROW, BACKSPACE, DELETE, TAB} from '../core/keyboard/keycodes';
import {Directionality} from '../core';
+import {MdFormFieldModule} from '../form-field/index';
describe('MdChipList', () => {
let fixture: ComponentFixture;
@@ -23,7 +24,7 @@ describe('MdChipList', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MdChipsModule, MdInputModule, NoopAnimationsModule],
+ imports: [MdChipsModule, MdFormFieldModule, MdInputModule, NoopAnimationsModule],
declarations: [
StandardChipList, InputContainerChipList
],
diff --git a/src/lib/chips/chips.scss b/src/lib/chips/chips.scss
index 947a69879049..b170a27039e1 100644
--- a/src/lib/chips/chips.scss
+++ b/src/lib/chips/chips.scss
@@ -32,7 +32,7 @@ $mat-chips-chip-margin: 8px;
}
}
- .mat-input-prefix & {
+ .mat-form-field-prefix & {
&:last-child {
margin-right: $mat-chips-chip-margin;
}
@@ -76,7 +76,7 @@ $mat-chips-chip-margin: 8px;
}
}
-.mat-input-prefix .mat-chip-list-wrapper {
+.mat-form-field-prefix .mat-chip-list-wrapper {
margin-bottom: $mat-chips-chip-margin;
}
diff --git a/src/lib/core/compatibility/compatibility.ts b/src/lib/core/compatibility/compatibility.ts
index c6ba4d7f574b..53ed876ff3d4 100644
--- a/src/lib/core/compatibility/compatibility.ts
+++ b/src/lib/core/compatibility/compatibility.ts
@@ -75,6 +75,7 @@ export const MAT_ELEMENTS_SELECTOR = `
mat-hint,
mat-icon,
mat-input-container,
+ mat-form-field,
mat-list,
mat-list-item,
mat-menu,
@@ -151,6 +152,7 @@ export const MD_ELEMENTS_SELECTOR = `
md-hint,
md-icon,
md-input-container,
+ md-form-field,
md-list,
md-list-item,
md-menu,
diff --git a/src/lib/core/theming/_all-theme.scss b/src/lib/core/theming/_all-theme.scss
index a7c7c80161ef..f442ef708e00 100644
--- a/src/lib/core/theming/_all-theme.scss
+++ b/src/lib/core/theming/_all-theme.scss
@@ -27,6 +27,7 @@
@import '../../toolbar/toolbar-theme';
@import '../../tooltip/tooltip-theme';
@import '../../snack-bar/simple-snack-bar-theme';
+@import '../../form-field/form-field-theme';
// Create a theme.
@@ -42,6 +43,7 @@
@include mat-datepicker-theme($theme);
@include mat-dialog-theme($theme);
@include mat-expansion-panel-theme($theme);
+ @include mat-form-field-theme($theme);
@include mat-grid-list-theme($theme);
@include mat-icon-theme($theme);
@include mat-input-theme($theme);
diff --git a/src/lib/core/typography/_all-typography.scss b/src/lib/core/typography/_all-typography.scss
index 4453d6a4ef57..35e77fd257e6 100644
--- a/src/lib/core/typography/_all-typography.scss
+++ b/src/lib/core/typography/_all-typography.scss
@@ -28,6 +28,7 @@
@import '../../snack-bar/simple-snack-bar-theme';
@import '../option/option-theme';
@import '../option/optgroup-theme';
+@import '../../form-field/form-field-theme';
// Includes all of the typographic styles.
@@ -47,6 +48,7 @@
@include mat-datepicker-typography($config);
@include mat-dialog-typography($config);
@include mat-expansion-panel-typography($config);
+ @include mat-form-field-typography($config);
@include mat-grid-list-typography($config);
@include mat-icon-typography($config);
@include mat-input-typography($config);
diff --git a/src/lib/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts
index 156e481f9746..89b326ee348b 100644
--- a/src/lib/datepicker/datepicker-input.ts
+++ b/src/lib/datepicker/datepicker-input.ts
@@ -31,7 +31,7 @@ import {
Validators
} from '@angular/forms';
import {Subscription} from 'rxjs/Subscription';
-import {MdInputContainer} from '../input/input-container';
+import {MdFormField} from '../form-field/index';
import {DOWN_ARROW} from '../core/keyboard/keycodes';
import {DateAdapter} from '../core/datetime/index';
import {createMissingDateImplError} from './datepicker-errors';
@@ -212,7 +212,7 @@ export class MdDatepickerInput implements AfterContentInit, ControlValueAcces
private _renderer: Renderer2,
@Optional() private _dateAdapter: DateAdapter,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats,
- @Optional() private _mdInputContainer: MdInputContainer) {
+ @Optional() private _mdInputContainer: MdFormField) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts
index 8b7309003719..1b2b44678c57 100644
--- a/src/lib/datepicker/datepicker.spec.ts
+++ b/src/lib/datepicker/datepicker.spec.ts
@@ -16,6 +16,7 @@ import {
createKeyboardEvent,
dispatchEvent,
} from '@angular/cdk/testing';
+import {MdFormFieldModule} from '../form-field/index';
describe('MdDatepicker', () => {
afterEach(inject([OverlayContainer], (container: OverlayContainer) => {
@@ -28,6 +29,7 @@ describe('MdDatepicker', () => {
imports: [
FormsModule,
MdDatepickerModule,
+ MdFormFieldModule,
MdInputModule,
MdNativeDateModule,
NoopAnimationsModule,
@@ -596,7 +598,7 @@ describe('MdDatepicker', () => {
it('should attach popup to input-container underline', () => {
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
- expect(attachToRef.nativeElement.classList.contains('mat-input-underline'))
+ expect(attachToRef.nativeElement.classList.contains('mat-form-field-underline'))
.toBe(true, 'popup should be attached to input-container underline');
});
});
@@ -817,6 +819,7 @@ describe('MdDatepicker', () => {
imports: [
FormsModule,
MdDatepickerModule,
+ MdFormFieldModule,
MdInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
@@ -840,7 +843,13 @@ describe('MdDatepicker', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [MdDatepickerModule, MdInputModule, MdNativeDateModule, NoopAnimationsModule],
+ imports: [
+ MdDatepickerModule,
+ MdFormFieldModule,
+ MdInputModule,
+ MdNativeDateModule,
+ NoopAnimationsModule
+ ],
declarations: [StandardDatepicker],
}).compileComponents();
diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss
new file mode 100644
index 000000000000..8f475fd4f96f
--- /dev/null
+++ b/src/lib/form-field/_form-field-theme.scss
@@ -0,0 +1,218 @@
+@import '../core/theming/palette';
+@import '../core/theming/theming';
+@import '../core/style/form-common';
+@import '../core/typography/typography-utils';
+
+
+@mixin mat-form-field-theme($theme) {
+ $primary: map-get($theme, primary);
+ $accent: map-get($theme, accent);
+ $warn: map-get($theme, warn);
+ $background: map-get($theme, background);
+ $foreground: map-get($theme, foreground);
+ $is-dark-theme: map-get($theme, is-dark);
+
+ // Placeholder colors. Required is used for the `*` star shown in the placeholder.
+ $placeholder-color: mat-color($foreground, secondary-text);
+ $floating-placeholder-color: mat-color($primary);
+ $required-placeholder-color: mat-color($accent);
+
+ // Underline colors.
+ $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42));
+ $underline-color-accent: mat-color($accent);
+ $underline-color-warn: mat-color($warn);
+ $underline-focused-color: mat-color($primary);
+
+ .mat-form-field-placeholder {
+ color: $placeholder-color;
+ }
+
+ .mat-hint {
+ color: mat-color($foreground, secondary-text);
+ }
+
+ .mat-focused .mat-form-field-placeholder {
+ color: $floating-placeholder-color;
+
+ &.mat-accent {
+ color: $underline-color-accent;
+ }
+
+ &.mat-warn {
+ color: $underline-color-warn;
+ }
+ }
+
+ .mat-form-field-autofill-float:-webkit-autofill + .mat-form-field-placeholder,
+ .mat-focused .mat-form-field-placeholder.mat-form-field-float {
+ .mat-form-field-required-marker {
+ color: $required-placeholder-color;
+ }
+ }
+
+ .mat-form-field-underline {
+ background-color: $underline-color;
+
+ &.mat-disabled {
+ @include mat-control-disabled-underline($underline-color);
+ }
+ }
+
+ .mat-form-field-ripple {
+ background-color: $underline-focused-color;
+
+ &.mat-accent {
+ background-color: $underline-color-accent;
+ }
+
+ &.mat-warn {
+ background-color: $underline-color-warn;
+ }
+ }
+
+ // Styling for the error state of the form field. Note that while the same can be
+ // achieved with the ng-* classes, we use this approach in order to ensure that the same
+ // logic is used to style the error state and to show the error messages.
+ .mat-form-field-invalid {
+ .mat-form-field-placeholder {
+ color: $underline-color-warn;
+
+ &.mat-accent,
+ &.mat-form-field-float .mat-form-field-required-marker {
+ color: $underline-color-warn;
+ }
+ }
+
+ .mat-form-field-ripple {
+ background-color: $underline-color-warn;
+ }
+ }
+
+ .mat-error {
+ color: $underline-color-warn;
+ }
+}
+
+// Applies a floating placeholder above the form field control itself.
+@mixin _mat-form-field-placeholder-floating($font-scale, $infix-padding, $infix-margin-top) {
+ // We use perspective to fix the text blurriness as described here:
+ // http://www.useragentman.com/blog/2014/05/04/fixing-typography-inside-of-2-d-css-transforms/
+ // This results in a small jitter after the label floats on Firefox, which the
+ // translateZ fixes.
+ transform: translateY(-$infix-margin-top - $infix-padding) scale($font-scale) perspective(100px)
+ translateZ(0.001px);
+ // The tricks above used to smooth out the animation on chrome and firefox actually make things
+ // worse on IE, so we don't include them in the IE version.
+ -ms-transform: translateY(-$infix-margin-top - $infix-padding)
+ scale($font-scale);
+
+ width: 100% / $font-scale;
+}
+
+@mixin mat-form-field-typography($config) {
+ // The unit-less line-height from the font config.
+ $line-height: mat-line-height($config, input);
+
+ // The amount to scale the font for the floating label and subscript.
+ $subscript-font-scale: 0.75;
+ // The amount to scale the font for the prefix and suffix icons.
+ $prefix-suffix-icon-font-scale: 1.5;
+
+ // The amount of space between the top of the line and the top of the actual text
+ // (as a fraction of the font-size).
+ $line-spacing: ($line-height - 1) / 2;
+ // The padding on the infix. Mocks show half of the text size, but seem to measure from the edge
+ // of the text itself, not the edge of the line; therefore we subtract off the line spacing.
+ $infix-padding: 0.5em - $line-spacing;
+ // The margin applied to the form-field-infix to reserve space for the floating label.
+ $infix-margin-top: 1em * $line-height * $subscript-font-scale;
+ // Font size to use for the label and subscript text.
+ $subscript-font-size: $subscript-font-scale * 100%;
+ // Font size to use for the for the prefix and suffix icons.
+ $prefix-suffix-icon-font-size: $prefix-suffix-icon-font-scale * 100%;
+ // The space between the bottom of the .mat-form-field-flex area and the subscript wrapper.
+ // Mocks show half of the text size, but this margin is applied to an element with the subscript
+ // text font size, so we need to divide by the scale factor to make it half of the original text
+ // size. We again need to subtract off the line spacing since the mocks measure to the edge of the
+ // text, not the edge of the line.
+ $subscript-margin-top: 0.5em / $subscript-font-scale - ($line-spacing * 2);
+ // The padding applied to the form-field-wrapper to reserve space for the subscript, since it's
+ // absolutely positioned. This is a combination of the subscript's margin and line-height, but we
+ // need to multiply by the subscript font scale factor since the wrapper has a larger font size.
+ $wrapper-padding-bottom: ($subscript-margin-top + $line-height) * $subscript-font-scale;
+
+ .mat-form-field {
+ font-family: mat-font-family($config);
+ font-size: inherit;
+ font-weight: mat-font-weight($config, input);
+ line-height: mat-line-height($config, input);
+ }
+
+ .mat-form-field-wrapper {
+ padding-bottom: $wrapper-padding-bottom;
+ }
+
+ .mat-form-field-prefix,
+ .mat-form-field-suffix {
+ // Allow icons in a prefix or suffix to adapt to the correct size.
+ .mat-icon {
+ font-size: $prefix-suffix-icon-font-size;
+ line-height: $line-height;
+ }
+
+ // Allow icon buttons in a prefix or suffix to adapt to the correct size.
+ .mat-icon-button {
+ height: $prefix-suffix-icon-font-scale * 1em;
+ width: $prefix-suffix-icon-font-scale * 1em;
+
+ .mat-icon {
+ height: $line-height * 1em;
+ line-height: $line-height;
+ }
+ }
+ }
+
+ .mat-form-field-infix {
+ padding: $infix-padding 0;
+ // Throws off the baseline if we do it as a real margin, so we do it as a border instead.
+ border-top: $infix-margin-top solid transparent;
+ }
+
+ .mat-form-field-autofill-float {
+ &:-webkit-autofill + .mat-form-field-placeholder-wrapper .mat-form-field-float {
+ @include _mat-form-field-placeholder-floating(
+ $subscript-font-scale, $infix-padding, $infix-margin-top);
+ }
+ }
+
+ .mat-form-field-placeholder-wrapper {
+ top: -$infix-margin-top;
+ padding-top: $infix-margin-top;
+ }
+
+ .mat-form-field-placeholder {
+ top: $infix-margin-top + $infix-padding;
+
+ // Show the placeholder above the control when it's not empty, or focused.
+ &.mat-form-field-float:not(.mat-form-field-empty),
+ .mat-focused &.mat-form-field-float {
+ @include _mat-form-field-placeholder-floating($subscript-font-scale,
+ $infix-padding, $infix-margin-top);
+ }
+ }
+
+ .mat-form-field-underline {
+ // We want the underline to start at the end of the content box, not the padding box,
+ // so we move it up by the padding amount.
+ bottom: $wrapper-padding-bottom;
+ }
+
+ .mat-form-field-subscript-wrapper {
+ font-size: $subscript-font-size;
+ margin-top: $subscript-margin-top;
+
+ // We want the subscript to start at the end of the content box, not the padding box,
+ // so we move it up by the padding amount (adjusted for the smaller font size);
+ top: calc(100% - #{$wrapper-padding-bottom / $subscript-font-scale});
+ }
+}
diff --git a/src/lib/form-field/error.ts b/src/lib/form-field/error.ts
new file mode 100644
index 000000000000..40d5bb1762ec
--- /dev/null
+++ b/src/lib/form-field/error.ts
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, Input} from '@angular/core';
+
+
+let nextUniqueId = 0;
+
+
+/** Single error message to be shown underneath the form field. */
+@Directive({
+ selector: 'md-error, mat-error',
+ host: {
+ 'class': 'mat-error',
+ 'role': 'alert',
+ '[attr.id]': 'id',
+ }
+})
+export class MdError {
+ @Input() id: string = `mat-error-${nextUniqueId++}`;
+}
diff --git a/src/lib/form-field/form-field-control.ts b/src/lib/form-field/form-field-control.ts
new file mode 100644
index 000000000000..98a6ea02089b
--- /dev/null
+++ b/src/lib/form-field/form-field-control.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Observable} from 'rxjs/Observable';
+import {NgControl} from '@angular/forms';
+
+
+/** An interface which allows a control to work inside of a `MdFormField`. */
+export abstract class MdFormFieldControl {
+ /** The value of the control. */
+ value: T;
+
+ /**
+ * Stream that emits whenever the state of the control changes such that the parent `MdFormField`
+ * needs to run change detection.
+ */
+ readonly stateChanges: Observable;
+
+ /** The element ID for this control. */
+ readonly id: string;
+
+ /** The placeholder for this control. */
+ readonly placeholder: string;
+
+ /** Gets the NgControl for this control. */
+ readonly ngControl: NgControl | null;
+
+ /** Whether the control is focused. */
+ readonly focused: boolean;
+
+ /** Whether the control is empty. */
+ readonly empty: boolean;
+
+ /** Whether the control is required. */
+ readonly required: boolean;
+
+ /** Whether the control is disabled. */
+ readonly disabled: boolean;
+
+ /** Whether the control is in an error state. */
+ readonly errorState: boolean;
+
+ /** Sets the list of element IDs that currently describe this control. */
+ abstract setDescribedByIds(ids: string[]): void;
+
+ /** Focuses this control. */
+ abstract focus(): void;
+}
diff --git a/src/lib/form-field/form-field-errors.ts b/src/lib/form-field/form-field-errors.ts
new file mode 100644
index 000000000000..008393915e4c
--- /dev/null
+++ b/src/lib/form-field/form-field-errors.ts
@@ -0,0 +1,23 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/** @docs-private */
+export function getMdFormFieldPlaceholderConflictError(): Error {
+ return Error('Placeholder attribute and child element were both specified.');
+}
+
+/** @docs-private */
+export function getMdFormFieldDuplicatedHintError(align: string): Error {
+ return Error(`A hint was already declared for 'align="${align}"'.`);
+}
+
+/** @docs-private */
+export function getMdFormFieldMissingControlError(): Error {
+ return Error('md-form-field must contain a MdFormFieldControl. ' +
+ 'Did you forget to add mdInput to the native input or textarea element?');
+}
diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html
new file mode 100644
index 000000000000..cfb7bfedab83
--- /dev/null
+++ b/src/lib/form-field/form-field.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{hintLabel}}
+
+
+
+
+
+
diff --git a/src/lib/input/input-container.scss b/src/lib/form-field/form-field.scss
similarity index 51%
rename from src/lib/input/input-container.scss
rename to src/lib/form-field/form-field.scss
index 0154daac6635..d4e76aedded5 100644
--- a/src/lib/input/input-container.scss
+++ b/src/lib/form-field/form-field.scss
@@ -3,12 +3,12 @@
// Min amount of space between start and end hint.
-$mat-input-hint-min-space: 1em !default;
+$mat-form-field-hint-min-space: 1em !default;
// The height of the underline.
-$mat-input-underline-height: 1px !default;
+$mat-form-field-underline-height: 1px !default;
-.mat-input-container {
+.mat-form-field {
display: inline-block;
position: relative;
width: 200px;
@@ -23,20 +23,20 @@ $mat-input-underline-height: 1px !default;
// Global wrapper. We need to apply margin to the element for spacing, but
// cannot apply it to the host element directly.
-.mat-input-wrapper {
+.mat-form-field-wrapper {
position: relative;
}
// We use a flex layout to baseline align the prefix and suffix elements.
// The underline is outside of it so it can cover all of the elements under this flex container.
-.mat-input-flex {
+.mat-form-field-flex {
display: inline-flex;
align-items: baseline;
width: 100%;
}
-.mat-input-prefix,
-.mat-input-suffix {
+.mat-form-field-prefix,
+.mat-form-field-suffix {
white-space: nowrap;
flex: none;
@@ -56,75 +56,32 @@ $mat-input-underline-height: 1px !default;
}
}
-.mat-input-infix {
+.mat-form-field-infix {
display: block;
position: relative;
flex: auto;
}
-// The Input element proper.
-.mat-input-element {
- // Font needs to be inherited, because by default has a system font.
- font: inherit;
-
- // The Material input should match whatever background it is above.
- background: transparent;
-
- // If background matches current background then so should the color for proper contrast
- color: currentColor;
-
- // By default, has a padding, border, outline and a default width.
- border: none;
- outline: none;
- padding: 0;
- margin: 0;
- width: 100%;
-
- // Prevent textareas from being resized outside the container.
- max-width: 100%;
- resize: vertical;
-
- // Needed to make last line of the textarea line up with the baseline.
- vertical-align: bottom;
-
- // Undo the red box-shadow glow added by Firefox on invalid inputs.
- // See https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-ui-invalid
- &:-moz-ui-invalid {
- box-shadow: none;
+// Pseudo-class for Chrome and Safari auto-fill to move the placeholder to the floating position.
+// This is necessary because these browsers do not actually fire any events when a form auto-fill is
+// occurring. Once the autofill is committed, a change event happen and the regular md-form-field
+// classes take over to fulfill this behaviour. Assumes the autofill is non-empty.
+.mat-form-field-autofill-float:-webkit-autofill + .mat-form-field-placeholder-wrapper {
+ // The control is still technically empty at this point, so we need to hide non-floating
+ // placeholders to prevent overlapping with the autofilled value.
+ .mat-form-field-placeholder {
+ display: none;
}
- // Pseudo-class for Chrome and Safari auto-fill to move the placeholder to
- // the floating position. This is necessary because these browsers do not actually
- // fire any events when a form auto-fill is occurring.
- // Once the autofill is committed, a change event happen and the regular md-input-container
- // classes take over to fulfill this behaviour.
- // Assumes the autofill is non-empty.
- &:-webkit-autofill + .mat-input-placeholder-wrapper {
- // The input is still technically empty at this point, so we need to hide non-floating
- // placeholders to prevent overlapping with the autofilled value.
- .mat-input-placeholder {
- display: none;
- }
-
- .mat-float {
- display: block;
- transition: none;
- }
- }
-
- // Note that we can't use something like visibility: hidden or
- // display: none, because IE ends up preventing the user from
- // focusing the input altogether.
- @include input-placeholder {
- // Needs to be !important, because the placeholder will end up inheriting the
- // input color in IE, if the consumer overrides it with a higher specificity.
- color: transparent !important;
+ .mat-form-field-float {
+ display: block;
+ transition: none;
}
}
// Used to hide the placeholder overflow on IE, since IE doesn't take transform into account when
// determining overflow.
-.mat-input-placeholder-wrapper {
+.mat-form-field-placeholder-wrapper {
position: absolute;
left: 0;
box-sizing: content-box;
@@ -134,16 +91,11 @@ $mat-input-underline-height: 1px !default;
pointer-events: none; // We shouldn't catch mouse events (let them through).
}
-// Prevents IE from always adding a scrollbar by default.
-textarea.mat-input-element {
- overflow: auto;
-}
-
// The placeholder label. This is invisible unless it is. The logic to show it is
// basically `empty || (float && (!empty || focused))`. Float is dependent on the
-// `floatingPlaceholder` input.
-.mat-input-placeholder {
- // The placeholder is after the , but needs to be aligned top-left of the
+// `floatingPlaceholder` property.
+.mat-form-field-placeholder {
+ // The placeholder is after the form field control, but needs to be aligned top-left of the
// infix
.
position: absolute;
left: 0;
@@ -163,15 +115,15 @@ textarea.mat-input-element {
transform-origin: 0 0;
transition: transform $swift-ease-out-duration $swift-ease-out-timing-function,
- color $swift-ease-out-duration $swift-ease-out-timing-function,
- width $swift-ease-out-duration $swift-ease-out-timing-function;
+ color $swift-ease-out-duration $swift-ease-out-timing-function,
+ width $swift-ease-out-duration $swift-ease-out-timing-function;
- // Hide the placeholder initially, and only show it when it's floating or the input is empty.
+ // Hide the placeholder initially, and only show it when it's floating or the control is empty.
display: none;
- &.mat-empty,
- &.mat-float:not(.mat-empty),
- .mat-focused &.mat-float {
+ &.mat-form-field-empty,
+ &.mat-form-field-float:not(.mat-form-field-empty),
+ .mat-focused &.mat-form-field-float {
display: block;
}
@@ -182,17 +134,17 @@ textarea.mat-input-element {
}
}
-// Disable the placeholder animation when the input is not empty (this prevents placeholder
+// Disable the placeholder animation when the control is not empty (this prevents placeholder
// animating up when the value is set programmatically).
-.mat-input-placeholder:not(.mat-empty) {
+.mat-form-field-placeholder:not(.mat-form-field-empty) {
transition: none;
}
-// The underline is what's shown under the input, its prefix and its suffix.
+// The underline is what's shown under the control, its prefix and its suffix.
// The ripple is the blue animation coming on top of it.
-.mat-input-underline {
+.mat-form-field-underline {
position: absolute;
- height: $mat-input-underline-height;
+ height: $mat-form-field-underline-height;
width: 100%;
&.mat-disabled {
@@ -200,9 +152,9 @@ textarea.mat-input-element {
background-color: transparent;
}
- .mat-input-ripple {
+ .mat-form-field-ripple {
position: absolute;
- height: $mat-input-underline-height;
+ height: $mat-form-field-underline-height;
top: 0;
left: 0;
width: 100%;
@@ -212,11 +164,11 @@ textarea.mat-input-element {
transition: background-color $swift-ease-in-duration $swift-ease-in-timing-function;
.mat-focused & {
- height: $mat-input-underline-height * 2;
+ height: $mat-form-field-underline-height * 2;
}
.mat-focused &,
- .mat-input-invalid & {
+ .mat-form-field-invalid & {
visibility: visible;
transform: scaleX(1);
transition: transform 150ms linear,
@@ -226,15 +178,15 @@ textarea.mat-input-element {
}
// Wrapper for the hints and error messages.
-.mat-input-subscript-wrapper {
+.mat-form-field-subscript-wrapper {
position: absolute;
width: 100%;
- overflow: hidden; // prevents multi-line errors from overlapping the input
+ overflow: hidden; // prevents multi-line errors from overlapping the control
}
// Scale down icons in the placeholder and hint to be the same size as the text.
-.mat-input-subscript-wrapper,
-.mat-input-placeholder-wrapper {
+.mat-form-field-subscript-wrapper,
+.mat-form-field-placeholder-wrapper {
.mat-icon {
width: 1em;
height: 1em;
@@ -244,16 +196,16 @@ textarea.mat-input-element {
}
// Clears the floats on the hints. This is necessary for the hint animation to work.
-.mat-input-hint-wrapper {
+.mat-form-field-hint-wrapper {
display: flex;
}
// Spacer used to make sure start and end hints have enough space between them.
-.mat-input-hint-spacer {
- flex: 1 0 $mat-input-hint-min-space;
+.mat-form-field-hint-spacer {
+ flex: 1 0 $mat-form-field-hint-min-space;
}
-// Single error message displayed beneath the input.
-.mat-input-error {
+// Single error message displayed beneath the form field underline.
+.mat-error {
display: block;
}
diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts
new file mode 100644
index 000000000000..eba04bde9791
--- /dev/null
+++ b/src/lib/form-field/form-field.ts
@@ -0,0 +1,290 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ AfterContentChecked,
+ AfterContentInit,
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChild,
+ ContentChildren,
+ ElementRef,
+ Inject,
+ Input,
+ Optional,
+ QueryList,
+ ViewChild,
+ ViewEncapsulation,
+} from '@angular/core';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {coerceBooleanProperty} from '../core';
+import {
+ getMdFormFieldDuplicatedHintError,
+ getMdFormFieldMissingControlError,
+ getMdFormFieldPlaceholderConflictError,
+} from './form-field-errors';
+import {
+ FloatPlaceholderType,
+ MD_PLACEHOLDER_GLOBAL_OPTIONS,
+ PlaceholderOptions
+} from '../core/placeholder/placeholder-options';
+import {startWith} from '@angular/cdk/rxjs';
+import {MdError} from './error';
+import {MdFormFieldControl} from './form-field-control';
+import {MdHint} from './hint';
+import {MdPlaceholder} from './placeholder';
+import {MdPrefix} from './prefix';
+import {MdSuffix} from './suffix';
+
+
+let nextUniqueId = 0;
+
+
+/** Container for form controls that applies Material Design styling and behavior. */
+@Component({
+ moduleId: module.id,
+ // TODO(mmalerba): the input-container selectors and classes are deprecated and will be removed.
+ selector: 'md-input-container, mat-input-container, md-form-field, mat-form-field',
+ templateUrl: 'form-field.html',
+ // MdInput is a directive and can't have styles, so we need to include its styles here.
+ // The MdInput styles are fairly minimal so it shouldn't be a big deal for people who aren't using
+ // MdInput.
+ styleUrls: ['form-field.css', '../input/input.css'],
+ animations: [
+ // TODO(mmalerba): Use angular animations for placeholder animation as well.
+ trigger('transitionMessages', [
+ state('enter', style({ opacity: 1, transform: 'translateY(0%)' })),
+ transition('void => enter', [
+ style({ opacity: 0, transform: 'translateY(-100%)' }),
+ animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)'),
+ ]),
+ ]),
+ ],
+ host: {
+ 'class': 'mat-input-container mat-form-field',
+ '[class.mat-input-invalid]': '_control.errorState',
+ '[class.mat-form-field-invalid]': '_control.errorState',
+ '[class.mat-focused]': '_control.focused',
+ '[class.ng-untouched]': '_shouldForward("untouched")',
+ '[class.ng-touched]': '_shouldForward("touched")',
+ '[class.ng-pristine]': '_shouldForward("pristine")',
+ '[class.ng-dirty]': '_shouldForward("dirty")',
+ '[class.ng-valid]': '_shouldForward("valid")',
+ '[class.ng-invalid]': '_shouldForward("invalid")',
+ '[class.ng-pending]': '_shouldForward("pending")',
+ '(click)': '_control.focus()',
+ },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+
+export class MdFormField implements AfterViewInit, AfterContentInit, AfterContentChecked {
+ private _placeholderOptions: PlaceholderOptions;
+
+ /** Color of the form field underline, based on the theme. */
+ @Input() color: 'primary' | 'accent' | 'warn' = 'primary';
+
+ /** @deprecated Use `color` instead. */
+ @Input()
+ get dividerColor() { return this.color; }
+ set dividerColor(value) { this.color = value; }
+
+ /** Whether the required marker should be hidden. */
+ @Input()
+ get hideRequiredMarker() { return this._hideRequiredMarker; }
+ set hideRequiredMarker(value: any) {
+ this._hideRequiredMarker = coerceBooleanProperty(value);
+ }
+ private _hideRequiredMarker: boolean;
+
+ /** Whether the floating label should always float or not. */
+ get _shouldAlwaysFloat() { return this._floatPlaceholder === 'always'; }
+
+ /** Whether the placeholder can float or not. */
+ get _canPlaceholderFloat() { return this._floatPlaceholder !== 'never'; }
+
+ /** State of the md-hint and md-error animations. */
+ _subscriptAnimationState: string = '';
+
+ /** Text for the form field hint. */
+ @Input()
+ get hintLabel() { return this._hintLabel; }
+ set hintLabel(value: string) {
+ this._hintLabel = value;
+ this._processHints();
+ }
+ private _hintLabel = '';
+
+ // Unique id for the hint label.
+ _hintLabelId: string = `md-hint-${nextUniqueId++}`;
+
+ /** Whether the placeholder should always float, never float or float as the user types. */
+ @Input()
+ get floatPlaceholder() { return this._floatPlaceholder; }
+ set floatPlaceholder(value: FloatPlaceholderType) {
+ if (value !== this._floatPlaceholder) {
+ this._floatPlaceholder = value || this._placeholderOptions.float || 'auto';
+ this._changeDetectorRef.markForCheck();
+ }
+ }
+ private _floatPlaceholder: FloatPlaceholderType;
+
+ /** Reference to the form field's underline element. */
+ @ViewChild('underline') underlineRef: ElementRef;
+ @ViewChild('connectionContainer') _connectionContainerRef: ElementRef;
+ @ContentChild(MdFormFieldControl) _control: MdFormFieldControl;
+ @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
+ @ContentChildren(MdError) _errorChildren: QueryList;
+ @ContentChildren(MdHint) _hintChildren: QueryList;
+ @ContentChildren(MdPrefix) _prefixChildren: QueryList;
+ @ContentChildren(MdSuffix) _suffixChildren: QueryList;
+
+ constructor(
+ public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef,
+ @Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions) {
+ this._placeholderOptions = placeholderOptions ? placeholderOptions : {};
+ this.floatPlaceholder = this._placeholderOptions.float || 'auto';
+ }
+
+ ngAfterContentInit() {
+ this._validateControlChild();
+
+ // Subscribe to changes in the child control state in order to update the form field UI.
+ startWith.call(this._control.stateChanges, null).subscribe(() => {
+ this._validatePlaceholders();
+ this._syncDescribedByIds();
+ this._changeDetectorRef.markForCheck();
+ });
+
+ let ngControl = this._control.ngControl;
+ if (ngControl && ngControl.valueChanges) {
+ ngControl.valueChanges.subscribe(() => {
+ this._changeDetectorRef.markForCheck();
+ });
+ }
+
+ // Re-validate when the number of hints changes.
+ startWith.call(this._hintChildren.changes, null).subscribe(() => {
+ this._processHints();
+ this._changeDetectorRef.markForCheck();
+ });
+
+ // Update the aria-described by when the number of errors changes.
+ startWith.call(this._errorChildren.changes, null).subscribe(() => {
+ this._syncDescribedByIds();
+ this._changeDetectorRef.markForCheck();
+ });
+ }
+
+ ngAfterContentChecked() {
+ this._validateControlChild();
+ }
+
+ ngAfterViewInit() {
+ // Avoid animations on load.
+ this._subscriptAnimationState = 'enter';
+ this._changeDetectorRef.detectChanges();
+ }
+
+ /** Determines whether a class from the NgControl should be forwarded to the host element. */
+ _shouldForward(prop: string): boolean {
+ let ngControl = this._control ? this._control.ngControl : null;
+ return ngControl && (ngControl as any)[prop];
+ }
+
+ /** Whether the form field has a placeholder. */
+ _hasPlaceholder() {
+ return !!(this._control.placeholder || this._placeholderChild);
+ }
+
+ /** Determines whether to display hints or errors. */
+ _getDisplayedMessages(): 'error' | 'hint' {
+ return (this._errorChildren && this._errorChildren.length > 0 &&
+ this._control.errorState) ? 'error' : 'hint';
+ }
+
+ /**
+ * Ensure that there is only one placeholder (either `placeholder` attribute on the child control
+ * or child element with the `md-placeholder` directive).
+ */
+ private _validatePlaceholders() {
+ if (this._control.placeholder && this._placeholderChild) {
+ throw getMdFormFieldPlaceholderConflictError();
+ }
+ }
+
+ /** Does any extra processing that is required when handling the hints. */
+ private _processHints() {
+ this._validateHints();
+ this._syncDescribedByIds();
+ }
+
+ /**
+ * 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;
+ let endHint: MdHint;
+ this._hintChildren.forEach((hint: MdHint) => {
+ if (hint.align == 'start') {
+ if (startHint || this.hintLabel) {
+ throw getMdFormFieldDuplicatedHintError('start');
+ }
+ startHint = hint;
+ } else if (hint.align == 'end') {
+ if (endHint) {
+ throw getMdFormFieldDuplicatedHintError('end');
+ }
+ endHint = hint;
+ }
+ });
+ }
+ }
+
+ /**
+ * Sets the list of element IDs that describe the child control. This allows the control to update
+ * its `aria-describedby` attribute accordingly.
+ */
+ private _syncDescribedByIds() {
+ if (this._control) {
+ let ids: string[] = [];
+
+ if (this._getDisplayedMessages() === 'hint') {
+ let startHint = this._hintChildren ?
+ this._hintChildren.find(hint => hint.align === 'start') : null;
+ let endHint = this._hintChildren ?
+ this._hintChildren.find(hint => hint.align === 'end') : null;
+
+ if (startHint) {
+ ids.push(startHint.id);
+ } else if (this._hintLabel) {
+ ids.push(this._hintLabelId);
+ }
+
+ if (endHint) {
+ ids.push(endHint.id);
+ }
+ } else if (this._errorChildren) {
+ ids = this._errorChildren.map(mdError => mdError.id);
+ }
+
+ this._control.setDescribedByIds(ids);
+ }
+ }
+
+ /** Throws an error if the form field's control is missing. */
+ protected _validateControlChild() {
+ if (!this._control) {
+ throw getMdFormFieldMissingControlError();
+ }
+ }
+}
diff --git a/src/lib/form-field/hint.ts b/src/lib/form-field/hint.ts
new file mode 100644
index 000000000000..3a5e21857aeb
--- /dev/null
+++ b/src/lib/form-field/hint.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, Input} from '@angular/core';
+
+
+let nextUniqueId = 0;
+
+
+/** Hint text to be shown underneath the form field control. */
+@Directive({
+ selector: 'md-hint, mat-hint',
+ host: {
+ 'class': 'mat-hint',
+ '[class.mat-right]': 'align == "end"',
+ '[attr.id]': 'id',
+ // Remove align attribute to prevent it from interfering with layout.
+ '[attr.align]': 'null',
+ }
+})
+export class MdHint {
+ /** Whether to align the hint label at the start or end of the line. */
+ @Input() align: 'start' | 'end' = 'start';
+
+ /** Unique ID for the hint. Used for the aria-describedby on the form field control. */
+ @Input() id: string = `mat-hint-${nextUniqueId++}`;
+}
diff --git a/src/lib/form-field/index.ts b/src/lib/form-field/index.ts
new file mode 100644
index 000000000000..71d8b6164677
--- /dev/null
+++ b/src/lib/form-field/index.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {NgModule} from '@angular/core';
+import {MdError} from './error';
+import {MdFormField} from './form-field';
+import {MdHint} from './hint';
+import {MdPlaceholder} from './placeholder';
+import {MdPrefix} from './prefix';
+import {MdSuffix} from './suffix';
+import {CommonModule} from '@angular/common';
+import {PlatformModule} from '../core/platform/index';
+
+
+@NgModule({
+ declarations: [
+ MdError,
+ MdHint,
+ MdFormField,
+ MdPlaceholder,
+ MdPrefix,
+ MdSuffix,
+ ],
+ imports: [
+ CommonModule,
+ PlatformModule,
+ ],
+ exports: [
+ MdError,
+ MdHint,
+ MdFormField,
+ MdPlaceholder,
+ MdPrefix,
+ MdSuffix,
+ ],
+})
+export class MdFormFieldModule {}
+
+
+export * from './error';
+export * from './form-field';
+export * from './form-field-control';
+export * from './form-field-errors';
+export * from './hint';
+export * from './placeholder';
+export * from './prefix';
+export * from './suffix';
+
diff --git a/src/lib/form-field/placeholder.ts b/src/lib/form-field/placeholder.ts
new file mode 100644
index 000000000000..260a4b8fef91
--- /dev/null
+++ b/src/lib/form-field/placeholder.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+
+/** The floating placeholder for an `MdFormField`. */
+@Directive({
+ selector: 'md-placeholder, mat-placeholder'
+})
+export class MdPlaceholder {}
diff --git a/src/lib/form-field/prefix.ts b/src/lib/form-field/prefix.ts
new file mode 100644
index 000000000000..3ac184b14331
--- /dev/null
+++ b/src/lib/form-field/prefix.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+
+/** Prefix to be placed the the front of the form field. */
+@Directive({
+ selector: '[mdPrefix], [matPrefix]',
+})
+export class MdPrefix {}
diff --git a/src/lib/form-field/suffix.ts b/src/lib/form-field/suffix.ts
new file mode 100644
index 000000000000..be70eec03959
--- /dev/null
+++ b/src/lib/form-field/suffix.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+
+
+/** Suffix to be placed at the end of the form field. */
+@Directive({
+ selector: '[mdSuffix], [matSuffix]',
+})
+export class MdSuffix {}
diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss
index e4b9ea76a6d8..9cd15260e3c2 100644
--- a/src/lib/input/_input-theme.scss
+++ b/src/lib/input/_input-theme.scss
@@ -5,227 +5,25 @@
@mixin mat-input-theme($theme) {
- $primary: map-get($theme, primary);
- $accent: map-get($theme, accent);
- $warn: map-get($theme, warn);
- $background: map-get($theme, background);
$foreground: map-get($theme, foreground);
$is-dark-theme: map-get($theme, is-dark);
- // Placeholder colors. Required is used for the `*` star shown in the placeholder.
- $input-placeholder-color: mat-color($foreground, secondary-text);
- $input-floating-placeholder-color: mat-color($primary);
- $input-required-placeholder-color: mat-color($accent);
-
- // Underline colors.
- $input-underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42));
- $input-underline-color-accent: mat-color($accent);
- $input-underline-color-warn: mat-color($warn);
- $input-underline-focused-color: mat-color($primary);
-
- .mat-input-placeholder {
- color: $input-placeholder-color;
- }
-
- .mat-hint {
- color: mat-color($foreground, secondary-text);
- }
-
- // :focus is applied to the input, but we apply mat-focused to the other elements
- // that need to listen to it.
- .mat-focused .mat-input-placeholder {
- color: $input-floating-placeholder-color;
-
- &.mat-accent {
- color: $input-underline-color-accent;
- }
-
- &.mat-warn {
- color: $input-underline-color-warn;
- }
- }
-
.mat-input-element:disabled {
color: mat-color($foreground, secondary-text, if($is-dark-theme, 0.7, 0.42));
}
-
- // See _mat-input-placeholder-floating mixin in input-container.scss
- input.mat-input-element:-webkit-autofill + .mat-input-placeholder,
- .mat-focused .mat-input-placeholder.mat-float {
- .mat-placeholder-required {
- color: $input-required-placeholder-color;
- }
- }
-
- .mat-input-underline {
- background-color: $input-underline-color;
-
- &.mat-disabled {
- @include mat-control-disabled-underline($input-underline-color);
- }
- }
-
- .mat-input-ripple {
- background-color: $input-underline-focused-color;
-
- &.mat-accent {
- background-color: $input-underline-color-accent;
- }
-
- &.mat-warn {
- background-color: $input-underline-color-warn;
- }
- }
-
- // Styling for the error state of the input container. Note that while the same can be
- // achieved with the ng-* classes, we use this approach in order to ensure that the same
- // logic is used to style the error state and to show the error messages.
- .mat-input-invalid {
- .mat-input-placeholder {
- color: $input-underline-color-warn;
-
- &.mat-accent,
- &.mat-float .mat-placeholder-required {
- color: $input-underline-color-warn;
- }
- }
-
- .mat-input-ripple {
- background-color: $input-underline-color-warn;
- }
- }
-
- .mat-input-error {
- color: $input-underline-color-warn;
- }
-}
-
-// Applies a floating placeholder above the input itself.
-@mixin _mat-input-placeholder-floating($font-scale, $infix-padding, $infix-margin-top) {
- // We use perspecitve to fix the text blurriness as described here:
- // http://www.useragentman.com/blog/2014/05/04/fixing-typography-inside-of-2-d-css-transforms/
- // This results in a small jitter after the label floats on Firefox, which the
- // translateZ fixes.
- transform: translateY(-$infix-margin-top - $infix-padding)
- scale($font-scale)
- perspective(100px) translateZ(0.001px);
- // The tricks above used to smooth out the animation on chrome and firefox actually make things
- // worse on IE, so we don't include them in the IE version.
- -ms-transform: translateY(-$infix-margin-top - $infix-padding)
- scale($font-scale);
-
- width: 100% / $font-scale;
}
@mixin mat-input-typography($config) {
// The unit-less line-height from the font config.
$line-height: mat-line-height($config, input);
- // The amount to scale the font for the floating label and subscript.
- $subscript-font-scale: 0.75;
- // The amount to scale the font for the prefix and suffix icons.
- $prefix-suffix-icon-font-scale: 1.5;
-
// The amount of space between the top of the line and the top of the actual text
// (as a fraction of the font-size).
$line-spacing: ($line-height - 1) / 2;
- // The padding on the infix. Mocks show half of the text size, but seem to measure from the edge
- // of the text itself, not the edge of the line; therefore we subtract off the line spacing.
- $infix-padding: 0.5em - $line-spacing;
- // The margin applied to the input-infix to reserve space for the floating label.
- $infix-margin-top: 1em * $line-height * $subscript-font-scale;
- // Font size to use for the label and subscript text.
- $subscript-font-size: $subscript-font-scale * 100%;
- // Font size to use for the for the prefix and suffix icons.
- $prefix-suffix-icon-font-size: $prefix-suffix-icon-font-scale * 100%;
- // The space between the bottom of the input table and the subscript container. Mocks show half of
- // the text size, but this margin is applied to an element with the subscript text font size, so
- // we need to divide by the scale factor to make it half of the original text size. We again need
- // to subtract off the line spacing since the mocks measure to the edge of the text, not the edge
- // of the line.
- $subscript-margin-top: 0.5em / $subscript-font-scale - ($line-spacing * 2);
- // The padding applied to the input-wrapper to reserve space for the subscript, since it's
- // absolutely positioned. This is a combination of the subscript's margin and line-height, but we
- // need to multiply by the subscript font scale factor since the wrapper has a larger font size.
- $wrapper-padding-bottom: ($subscript-margin-top + $line-height) * $subscript-font-scale;
-
- .mat-input-container {
- font-family: mat-font-family($config);
- font-size: inherit;
- font-weight: mat-font-weight($config, input);
- line-height: mat-line-height($config, input);
- }
-
- .mat-input-wrapper {
- padding-bottom: $wrapper-padding-bottom;
- }
-
- .mat-input-prefix,
- .mat-input-suffix {
- // Allow icons in a prefix or suffix to adapt to the correct size.
- .mat-icon {
- font-size: $prefix-suffix-icon-font-size;
- line-height: $line-height;
- }
-
- // Allow icon buttons in a prefix or suffix to adapt to the correct size.
- .mat-icon-button {
- height: $prefix-suffix-icon-font-scale * 1em;
- width: $prefix-suffix-icon-font-scale * 1em;
-
- .mat-icon {
- height: $line-height * 1em;
- line-height: $line-height;
- }
- }
- }
-
- .mat-input-infix {
- padding: $infix-padding 0;
- // Throws off the baseline if we do it as a real margin, so we do it as a border instead.
- border-top: $infix-margin-top solid transparent;
- }
-
- .mat-input-element {
- &:-webkit-autofill + .mat-input-placeholder-wrapper .mat-float {
- @include _mat-input-placeholder-floating($subscript-font-scale,
- $infix-padding, $infix-margin-top);
- }
- }
// elements seem to have their height set slightly too large on Safari causing the text to
// be misaligned w.r.t. the placeholder. Adding this margin corrects it.
input.mat-input-element {
margin-top: -$line-spacing * 1em;
}
-
- .mat-input-placeholder-wrapper {
- top: -$infix-margin-top;
- padding-top: $infix-margin-top;
- }
-
- .mat-input-placeholder {
- top: $infix-margin-top + $infix-padding;
-
- // Show the placeholder above the input when it's not empty, or focused.
- &.mat-float:not(.mat-empty), .mat-focused &.mat-float {
- @include _mat-input-placeholder-floating($subscript-font-scale,
- $infix-padding, $infix-margin-top);
- }
- }
-
- .mat-input-underline {
- // We want the underline to start at the end of the content box, not the padding box,
- // so we move it up by the padding amount.
- bottom: $wrapper-padding-bottom;
- }
-
- .mat-input-subscript-wrapper {
- font-size: $subscript-font-size;
- margin-top: $subscript-margin-top;
-
- // We want the subscript to start at the end of the content box, not the padding box,
- // so we move it up by the padding amount (adjusted for the smaller font size);
- top: calc(100% - #{$wrapper-padding-bottom / $subscript-font-scale});
- }
}
diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts
index b616a558408d..495d4e8c37f7 100644
--- a/src/lib/input/index.ts
+++ b/src/lib/input/index.ts
@@ -7,43 +7,28 @@
*/
import {NgModule} from '@angular/core';
-import {
- MdErrorDirective,
- MdHint,
- MdInputContainer,
- MdInputDirective,
- MdPlaceholder,
- MdPrefix,
- MdSuffix
-} from './input-container';
+import {MdInput} from './input';
import {MdTextareaAutosize} from './autosize';
import {CommonModule} from '@angular/common';
import {PlatformModule} from '../core/platform/index';
+import {MdFormFieldModule} from '../form-field/index';
@NgModule({
declarations: [
- MdErrorDirective,
- MdHint,
- MdInputContainer,
- MdInputDirective,
- MdPlaceholder,
- MdPrefix,
- MdSuffix,
+ MdInput,
MdTextareaAutosize,
],
imports: [
CommonModule,
+ MdFormFieldModule,
PlatformModule,
],
exports: [
- MdErrorDirective,
- MdHint,
- MdInputContainer,
- MdInputDirective,
- MdPlaceholder,
- MdPrefix,
- MdSuffix,
+ // We re-export the `MdFormFieldModule` since `MdInput` will almost always be used together with
+ // `MdFormField`.
+ MdFormFieldModule,
+ MdInput,
MdTextareaAutosize,
],
})
@@ -51,6 +36,6 @@ export class MdInputModule {}
export * from './autosize';
-export * from './input-container';
-export * from './input-container-errors';
+export * from './input';
+export * from './input-errors';
diff --git a/src/lib/input/input-container-errors.ts b/src/lib/input/input-container-errors.ts
deleted file mode 100644
index a5de9f863a37..000000000000
--- a/src/lib/input/input-container-errors.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * @license
- * Copyright Google Inc. All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-/** @docs-private */
-export function getMdInputContainerPlaceholderConflictError(): Error {
- return Error('Placeholder attribute and child element were both specified.');
-}
-
-/** @docs-private */
-export function getMdInputContainerUnsupportedTypeError(type: string): Error {
- return Error(`Input type "${type}" isn't supported by md-input-container.`);
-}
-
-/** @docs-private */
-export function getMdInputContainerDuplicatedHintError(align: string): Error {
- return Error(`A hint was already declared for 'align="${align}"'.`);
-}
-
-/** @docs-private */
-export function getMdInputContainerMissingMdInputError(): Error {
- return Error('md-input-container must contain an mdInput directive. ' +
- 'Did you forget to add mdInput to the native input or textarea element?');
-}
diff --git a/src/lib/input/input-container.html b/src/lib/input/input-container.html
deleted file mode 100644
index 0714e4019a79..000000000000
--- a/src/lib/input/input-container.html
+++ /dev/null
@@ -1,53 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{hintLabel}}
-
-
-
-
-
-
diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts
deleted file mode 100644
index d6c27ceecf74..000000000000
--- a/src/lib/input/input-container.ts
+++ /dev/null
@@ -1,624 +0,0 @@
-/**
- * @license
- * Copyright Google Inc. All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import {
- AfterContentChecked,
- AfterContentInit,
- AfterViewInit,
- ChangeDetectionStrategy,
- ChangeDetectorRef,
- Component,
- ContentChild,
- ContentChildren,
- Directive,
- DoCheck,
- ElementRef,
- Inject,
- Input,
- OnChanges,
- OnDestroy,
- Optional,
- QueryList,
- Renderer2,
- Self,
- ViewChild,
- ViewEncapsulation,
-} from '@angular/core';
-import {animate, state, style, transition, trigger} from '@angular/animations';
-import {coerceBooleanProperty, Platform} from '../core';
-import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
-import {getSupportedInputTypes} from '../core/platform/features';
-import {
- getMdInputContainerDuplicatedHintError,
- getMdInputContainerMissingMdInputError,
- getMdInputContainerPlaceholderConflictError,
- getMdInputContainerUnsupportedTypeError
-} from './input-container-errors';
-import {
- FloatPlaceholderType,
- MD_PLACEHOLDER_GLOBAL_OPTIONS,
- PlaceholderOptions
-} from '../core/placeholder/placeholder-options';
-import {
- defaultErrorStateMatcher,
- ErrorOptions,
- ErrorStateMatcher,
- MD_ERROR_GLOBAL_OPTIONS
-} from '../core/error/error-options';
-import {Subject} from 'rxjs/Subject';
-import {startWith} from '@angular/cdk/rxjs';
-
-// 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 {}
-
-
-/** Hint text to be shown underneath the input. */
-@Directive({
- selector: 'md-hint, mat-hint',
- host: {
- 'class': 'mat-hint',
- '[class.mat-right]': 'align == "end"',
- '[attr.id]': 'id',
- }
-})
-export class MdHint {
- /** Whether to align the hint label at the start or end of the line. */
- @Input() align: 'start' | 'end' = 'start';
-
- /** Unique ID for the hint. Used for the aria-describedby on the input. */
- @Input() id: string = `md-input-hint-${nextUniqueId++}`;
-}
-
-/** Single error message to be shown underneath the input. */
-@Directive({
- selector: 'md-error, mat-error',
- host: {
- 'class': 'mat-input-error',
- 'role': 'alert',
- '[attr.id]': 'id',
- }
-})
-export class MdErrorDirective {
- @Input() id: string = `md-input-error-${nextUniqueId++}`;
-}
-
-/** Prefix to be placed the the front of the input. */
-@Directive({
- selector: '[mdPrefix], [matPrefix]'
-})
-export class MdPrefix {}
-
-
-/** Suffix to be placed at the end of the input. */
-@Directive({
- selector: '[mdSuffix], [matSuffix]'
-})
-export class MdSuffix {}
-
-
-/** Marker for the input element that `MdInputContainer` is wrapping. */
-@Directive({
- selector: `input[mdInput], textarea[mdInput], input[matInput], textarea[matInput]`,
- host: {
- 'class': 'mat-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',
- '[attr.aria-describedby]': 'ariaDescribedby || null',
- '[attr.aria-invalid]': '_isErrorState',
- '(blur)': '_focusChanged(false)',
- '(focus)': '_focusChanged(true)',
- '(input)': '_onInput()',
- }
-})
-export class MdInputDirective implements OnChanges, OnDestroy, DoCheck {
- /** Variables used as cache for getters and setters. */
- private _type = 'text';
- private _placeholder: string = '';
- private _disabled = false;
- private _required = false;
- private _readonly = false;
- private _id: string;
- private _uid = `md-input-${nextUniqueId++}`;
- private _errorOptions: ErrorOptions;
- private _previousNativeValue = this.value;
-
- /** Whether the input is in an error state. */
- _isErrorState = false;
-
- /** Whether the element is focused or not. */
- focused = false;
-
- /** Sets the aria-describedby attribute on the input for improved a11y. */
- ariaDescribedby: string;
-
- /**
- * Stream that emits whenever the state of the input changes. This allows for other components
- * (mostly `md-input-container`) that depend on the properties of `mdInput` to update their view.
- */
- _stateChanges = new Subject();
-
- /** Whether the element is disabled. */
- @Input()
- get disabled() { return this._ngControl ? this._ngControl.disabled : this._disabled; }
- set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
-
- /** Unique id of the element. */
- @Input()
- get id() { return this._id; }
- set id(value: string) { this._id = value || this._uid; }
-
- /** Placeholder attribute of the element. */
- @Input() placeholder: string = '';
-
- /** Whether the element is required. */
- @Input()
- get required() { return this._required; }
- set required(value: any) { this._required = coerceBooleanProperty(value); }
-
- /** Input type of the element. */
- @Input()
- get type() { return this._type; }
- set type(value: string) {
- this._type = value || 'text';
- this._validateType();
-
- // 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() && getSupportedInputTypes().has(this._type)) {
- this._renderer.setProperty(this._elementRef.nativeElement, 'type', this._type);
- }
- }
-
- /** Whether the element is readonly. */
- @Input()
- get readonly() { return this._readonly; }
- set readonly(value: any) { this._readonly = coerceBooleanProperty(value); }
-
- /** A function used to control when error messages are shown. */
- @Input() errorStateMatcher: ErrorStateMatcher;
-
- /** The input element's value. */
- get value() { return this._elementRef.nativeElement.value; }
- set value(value: string) {
- if (value !== this.value) {
- this._elementRef.nativeElement.value = value;
- this._stateChanges.next();
- }
- }
-
- /** Whether the input is empty. */
- get empty() {
- return !this._isNeverEmpty() &&
- (this.value == null || this.value === '') &&
- // Check if the input contains bad input. If so, we know that it only appears empty because
- // the value failed to parse. From the user's perspective it is not empty.
- // TODO(mmalerba): Add e2e test for bad input case.
- !this._isBadInput();
- }
-
- private _neverEmptyInputTypes = [
- 'date',
- 'datetime',
- 'datetime-local',
- 'month',
- 'time',
- 'week'
- ].filter(t => getSupportedInputTypes().has(t));
-
- constructor(private _elementRef: ElementRef,
- private _renderer: Renderer2,
- private _platform: Platform,
- @Optional() @Self() public _ngControl: NgControl,
- @Optional() private _parentForm: NgForm,
- @Optional() private _parentFormGroup: FormGroupDirective,
- @Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
-
- // Force setter to be called in case id was not specified.
- this.id = this.id;
- this._errorOptions = errorOptions ? errorOptions : {};
- this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
-
- // On some versions of iOS the caret gets stuck in the wrong place when holding down the delete
- // key. In order to get around this we need to "jiggle" the caret loose. Since this bug only
- // exists on iOS, we only bother to install the listener on iOS.
- if (_platform.IOS) {
- _renderer.listen(_elementRef.nativeElement, 'keyup', (event: Event) => {
- let el = event.target as HTMLInputElement;
- if (!el.value && !el.selectionStart && !el.selectionEnd) {
- // Note: Just setting `0, 0` doesn't fix the issue. Setting `1, 1` fixes it for the first
- // time that you type text and then hold delete. Toggling to `1, 1` and then back to
- // `0, 0` seems to completely fix it.
- el.setSelectionRange(1, 1);
- el.setSelectionRange(0, 0);
- }
- });
- }
- }
-
- ngOnChanges() {
- this._stateChanges.next();
- }
-
- ngOnDestroy() {
- this._stateChanges.complete();
- }
-
- ngDoCheck() {
- if (this._ngControl) {
- // We need to re-evaluate this on every change detection cycle, because there are some
- // error triggers that we can't subscribe to (e.g. parent form submissions). This means
- // that whatever logic is in here has to be super lean or we risk destroying the performance.
- this._updateErrorState();
- } else {
- // When the input isn't used together with `@angular/forms`, we need to check manually for
- // changes to the native `value` property in order to update the floating label.
- this._dirtyCheckNativeValue();
- }
- }
-
- _onFocus() {
- if (!this._readonly) {
- this.focused = true;
- }
- }
-
- /** Focuses the input element. */
- focus() {
- this._elementRef.nativeElement.focus();
- }
-
- /** Callback for the cases where the focused state of the input changes. */
- _focusChanged(isFocused: boolean) {
- if (isFocused !== this.focused) {
- this.focused = isFocused;
- this._stateChanges.next();
- }
- }
-
- _onInput() {
- // This is a noop function and is used to let Angular know whenever the value changes.
- // Angular will run a new change detection each time the `input` event has been dispatched.
- // It's necessary that Angular recognizes the value change, because when floatingLabel
- // is set to false and Angular forms aren't used, the placeholder won't recognize the
- // value changes and will not disappear.
- // Listening to the input event wouldn't be necessary when the input is using the
- // FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
- }
-
- /** Re-evaluates the error state. This is only relevant with @angular/forms. */
- private _updateErrorState() {
- const oldState = this._isErrorState;
- const control = this._ngControl;
- const parent = this._parentFormGroup || this._parentForm;
- const newState = control && this.errorStateMatcher(control.control as FormControl, parent);
-
- if (newState !== oldState) {
- this._isErrorState = newState;
- this._stateChanges.next();
- }
- }
-
- /** Does some manual dirty checking on the native input `value` property. */
- private _dirtyCheckNativeValue() {
- const newValue = this.value;
-
- if (this._previousNativeValue !== newValue) {
- this._previousNativeValue = newValue;
- this._stateChanges.next();
- }
- }
-
- /** Make sure the input is a supported type. */
- private _validateType() {
- if (MD_INPUT_INVALID_TYPES.indexOf(this._type) > -1) {
- throw getMdInputContainerUnsupportedTypeError(this._type);
- }
- }
-
- /** Checks whether the input type isn't one of the types that are never empty. */
- private _isNeverEmpty() {
- return this._neverEmptyInputTypes.indexOf(this._type) > -1;
- }
-
- /** Checks whether the input is invalid based on the native validation. */
- private _isBadInput() {
- // The `validity` property won't be present on platform-server.
- let validity = (this._elementRef.nativeElement as HTMLInputElement).validity;
- return validity && validity.badInput;
- }
-
- /** Determines if the component host is a textarea. If not recognizable it returns false. */
- private _isTextarea() {
- let nativeElement = this._elementRef.nativeElement;
-
- // In Universal, we don't have access to `nodeName`, but the same can be achieved with `name`.
- // Note that this shouldn't be necessary once Angular switches to an API that resembles the
- // DOM closer.
- let nodeName = this._platform.isBrowser ? nativeElement.nodeName : nativeElement.name;
- return nodeName ? nodeName.toLowerCase() === 'textarea' : false;
- }
-}
-
-
-/**
- * Container for text inputs that applies Material Design styling and behavior.
- */
-@Component({
- moduleId: module.id,
- selector: 'md-input-container, mat-input-container',
- templateUrl: 'input-container.html',
- styleUrls: ['input-container.css'],
- animations: [
- trigger('transitionMessages', [
- state('enter', style({ opacity: 1, transform: 'translateY(0%)' })),
- transition('void => enter', [
- style({ opacity: 0, transform: 'translateY(-100%)' }),
- animate('300ms cubic-bezier(0.55, 0, 0.55, 0.2)')
- ])
- ])
- ],
- host: {
- // Remove align attribute to prevent it from interfering with layout.
- '[attr.align]': 'null',
- 'class': 'mat-input-container',
- '[class.mat-input-invalid]': '_mdInputChild._isErrorState',
- '[class.mat-focused]': '_mdInputChild.focused',
- '[class.ng-untouched]': '_shouldForward("untouched")',
- '[class.ng-touched]': '_shouldForward("touched")',
- '[class.ng-pristine]': '_shouldForward("pristine")',
- '[class.ng-dirty]': '_shouldForward("dirty")',
- '[class.ng-valid]': '_shouldForward("valid")',
- '[class.ng-invalid]': '_shouldForward("invalid")',
- '[class.ng-pending]': '_shouldForward("pending")',
- '(click)': '_focusInput()',
- },
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-
-export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterContentChecked {
- private _placeholderOptions: PlaceholderOptions;
-
- /** Color of the input divider, based on the theme. */
- @Input() color: 'primary' | 'accent' | 'warn' = 'primary';
-
- /** @deprecated Use `color` instead. */
- @Input()
- get dividerColor() { return this.color; }
- set dividerColor(value) { this.color = value; }
-
- /** Whether the required marker should be hidden. */
- @Input()
- get hideRequiredMarker() { return this._hideRequiredMarker; }
- set hideRequiredMarker(value: any) {
- this._hideRequiredMarker = coerceBooleanProperty(value);
- }
- private _hideRequiredMarker: boolean;
-
- /** Whether the floating label should always float or not. */
- get _shouldAlwaysFloat() { return this._floatPlaceholder === 'always'; }
-
- /** Whether the placeholder can float or not. */
- get _canPlaceholderFloat() { return this._floatPlaceholder !== 'never'; }
-
- /** State of the md-hint and md-error animations. */
- _subscriptAnimationState: string = '';
-
- /** Text for the input hint. */
- @Input()
- get hintLabel() { return this._hintLabel; }
- set hintLabel(value: string) {
- this._hintLabel = value;
- this._processHints();
- }
- private _hintLabel = '';
-
- // Unique id for the hint label.
- _hintLabelId: string = `md-input-hint-${nextUniqueId++}`;
-
- /** Whether the placeholder should always float, never float or float as the user types. */
- @Input()
- get floatPlaceholder() { return this._floatPlaceholder; }
- set floatPlaceholder(value: FloatPlaceholderType) {
- if (value !== this._floatPlaceholder) {
- this._floatPlaceholder = value || this._placeholderOptions.float || 'auto';
- this._changeDetectorRef.markForCheck();
- }
- }
- private _floatPlaceholder: FloatPlaceholderType;
-
- /** Reference to the input's underline element. */
- @ViewChild('underline') underlineRef: ElementRef;
- @ViewChild('connectionContainer') _connectionContainerRef: ElementRef;
- @ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
- @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
- @ContentChildren(MdErrorDirective) _errorChildren: QueryList;
- @ContentChildren(MdHint) _hintChildren: QueryList;
- @ContentChildren(MdPrefix) _prefixChildren: QueryList;
- @ContentChildren(MdSuffix) _suffixChildren: QueryList;
-
- constructor(
- public _elementRef: ElementRef,
- private _changeDetectorRef: ChangeDetectorRef,
- @Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions) {
- this._placeholderOptions = placeholderOptions ? placeholderOptions : {};
- this.floatPlaceholder = this._placeholderOptions.float || 'auto';
- }
-
- ngAfterContentInit() {
- this._validateInputChild();
-
- // Subscribe to changes in the child input state in order to update the container UI.
- startWith.call(this._mdInputChild._stateChanges, null).subscribe(() => {
- this._validatePlaceholders();
- this._syncAriaDescribedby();
- this._changeDetectorRef.markForCheck();
- });
-
- if (this._mdInputChild._ngControl && this._mdInputChild._ngControl.valueChanges) {
- this._mdInputChild._ngControl.valueChanges.subscribe(() => {
- this._changeDetectorRef.markForCheck();
- });
- }
-
- // Re-validate when the number of hints changes.
- startWith.call(this._hintChildren.changes, null).subscribe(() => {
- this._processHints();
- this._changeDetectorRef.markForCheck();
- });
-
- // Update the aria-described by when the number of errors changes.
- startWith.call(this._errorChildren.changes, null).subscribe(() => {
- this._syncAriaDescribedby();
- this._changeDetectorRef.markForCheck();
- });
- }
-
- ngAfterContentChecked() {
- this._validateInputChild();
- }
-
- ngAfterViewInit() {
- // Avoid animations on load.
- this._subscriptAnimationState = 'enter';
- this._changeDetectorRef.detectChanges();
- }
-
- /** Determines whether a class from the NgControl should be forwarded to the host element. */
- _shouldForward(prop: string): boolean {
- let control = this._mdInputChild ? this._mdInputChild._ngControl : null;
- return control && (control as any)[prop];
- }
-
- /** Whether the input has a placeholder. */
- _hasPlaceholder() {
- return !!(this._mdInputChild.placeholder || this._placeholderChild);
- }
-
- /** Focuses the underlying input. */
- _focusInput() {
- this._mdInputChild.focus();
- }
-
- /** Determines whether to display hints or errors. */
- _getDisplayedMessages(): 'error' | 'hint' {
- let input = this._mdInputChild;
- return (this._errorChildren && this._errorChildren.length > 0 && input._isErrorState) ?
- 'error' : 'hint';
- }
-
- /**
- * 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 getMdInputContainerPlaceholderConflictError();
- }
- }
-
- /**
- * Does any extra processing that is required when handling the hints.
- */
- private _processHints() {
- this._validateHints();
- this._syncAriaDescribedby();
- }
-
- /**
- * 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;
- let endHint: MdHint;
- this._hintChildren.forEach((hint: MdHint) => {
- if (hint.align == 'start') {
- if (startHint || this.hintLabel) {
- throw getMdInputContainerDuplicatedHintError('start');
- }
- startHint = hint;
- } else if (hint.align == 'end') {
- if (endHint) {
- throw getMdInputContainerDuplicatedHintError('end');
- }
- endHint = hint;
- }
- });
- }
- }
-
- /**
- * Sets the child input's `aria-describedby` to a space-separated list of the ids
- * of the currently-specified hints, as well as a generated id for the hint label.
- */
- private _syncAriaDescribedby() {
- if (this._mdInputChild) {
- let ids: string[] = [];
-
- if (this._getDisplayedMessages() === 'hint') {
- let startHint = this._hintChildren ?
- this._hintChildren.find(hint => hint.align === 'start') : null;
- let endHint = this._hintChildren ?
- this._hintChildren.find(hint => hint.align === 'end') : null;
-
- if (startHint) {
- ids.push(startHint.id);
- } else if (this._hintLabel) {
- ids.push(this._hintLabelId);
- }
-
- if (endHint) {
- ids.push(endHint.id);
- }
- } else if (this._errorChildren) {
- ids = this._errorChildren.map(mdError => mdError.id);
- }
-
- this._mdInputChild.ariaDescribedby = ids.join(' ');
- }
- }
-
- /**
- * Throws an error if the container's input child was removed.
- */
- protected _validateInputChild() {
- if (!this._mdInputChild) {
- throw getMdInputContainerMissingMdInputError();
- }
- }
-}
diff --git a/src/lib/input/input-errors.ts b/src/lib/input/input-errors.ts
new file mode 100644
index 000000000000..0ed0090f4e74
--- /dev/null
+++ b/src/lib/input/input-errors.ts
@@ -0,0 +1,12 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/** @docs-private */
+export function getMdInputUnsupportedTypeError(type: string): Error {
+ return Error(`Input type "${type}" isn't supported by mdInput.`);
+}
diff --git a/src/lib/input/input.scss b/src/lib/input/input.scss
new file mode 100644
index 000000000000..b960fa972d02
--- /dev/null
+++ b/src/lib/input/input.scss
@@ -0,0 +1,49 @@
+@import '../core/style/variables';
+@import '../core/style/vendor-prefixes';
+
+
+// The Input element proper.
+.mat-input-element {
+ // Font needs to be inherited, because by default has a system font.
+ font: inherit;
+
+ // The Material input should match whatever background it is above.
+ background: transparent;
+
+ // If background matches current background then so should the color for proper contrast
+ color: currentColor;
+
+ // By default, has a padding, border, outline and a default width.
+ border: none;
+ outline: none;
+ padding: 0;
+ margin: 0;
+ width: 100%;
+
+ // Prevent textareas from being resized outside the form field.
+ max-width: 100%;
+ resize: vertical;
+
+ // Needed to make last line of the textarea line up with the baseline.
+ vertical-align: bottom;
+
+ // Undo the red box-shadow glow added by Firefox on invalid inputs.
+ // See https://developer.mozilla.org/en-US/docs/Web/CSS/:-moz-ui-invalid
+ &:-moz-ui-invalid {
+ box-shadow: none;
+ }
+
+ // Note that we can't use something like visibility: hidden or
+ // display: none, because IE ends up preventing the user from
+ // focusing the input altogether.
+ @include input-placeholder {
+ // Needs to be !important, because the placeholder will end up inheriting the
+ // input color in IE, if the consumer overrides it with a higher specificity.
+ color: transparent !important;
+ }
+}
+
+// Prevents IE from always adding a scrollbar by default.
+textarea.mat-input-element {
+ overflow: auto;
+}
diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input.spec.ts
similarity index 88%
rename from src/lib/input/input-container.spec.ts
rename to src/lib/input/input.spec.ts
index 4c1b92ed567c..83ba4e6b7666 100644
--- a/src/lib/input/input-container.spec.ts
+++ b/src/lib/input/input.spec.ts
@@ -12,15 +12,17 @@ import {
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MdInputModule} from './index';
-import {MdInputContainer, MdInputDirective} from './input-container';
+import {MdInput} from './input';
import {Platform} from '../core/platform/platform';
import {PlatformModule} from '../core/platform/index';
import {wrappedErrorMessage, dispatchFakeEvent} from '@angular/cdk/testing';
import {
- getMdInputContainerDuplicatedHintError,
- getMdInputContainerMissingMdInputError,
- getMdInputContainerPlaceholderConflictError
-} from './input-container-errors';
+ MdFormField,
+ MdFormFieldModule,
+ getMdFormFieldDuplicatedHintError,
+ getMdFormFieldMissingControlError,
+ getMdFormFieldPlaceholderConflictError,
+} from '../form-field/index';
import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options';
import {MD_ERROR_GLOBAL_OPTIONS, showOnDirtyErrorStateMatcher} from '../core/error/error-options';
@@ -29,6 +31,7 @@ describe('MdInputContainer without forms', function () {
TestBed.configureTestingModule({
imports: [
FormsModule,
+ MdFormFieldModule,
MdInputModule,
NoopAnimationsModule,
PlatformModule,
@@ -73,8 +76,8 @@ describe('MdInputContainer without forms', function () {
let fixture = TestBed.createComponent(MdInputContainerWithId);
fixture.detectChanges();
- let inputContainer = fixture.debugElement.query(By.directive(MdInputContainer))
- .componentInstance as MdInputContainer;
+ let inputContainer = fixture.debugElement.query(By.directive(MdFormField))
+ .componentInstance as MdFormField;
expect(inputContainer.floatPlaceholder).toBe('auto',
'Expected MdInputContainer to set floatingLabel to auto by default.');
});
@@ -84,6 +87,7 @@ describe('MdInputContainer without forms', function () {
TestBed.configureTestingModule({
imports: [
FormsModule,
+ MdFormFieldModule,
MdInputModule,
NoopAnimationsModule
],
@@ -96,8 +100,8 @@ describe('MdInputContainer without forms', function () {
let fixture = TestBed.createComponent(MdInputContainerWithId);
fixture.detectChanges();
- let inputContainer = fixture.debugElement.query(By.directive(MdInputContainer))
- .componentInstance as MdInputContainer;
+ let inputContainer = fixture.debugElement.query(By.directive(MdFormField))
+ .componentInstance as MdFormField;
expect(inputContainer.floatPlaceholder).toBe('always',
'Expected MdInputContainer to set floatingLabel to always from global option.');
});
@@ -110,7 +114,7 @@ describe('MdInputContainer without forms', function () {
let el = fixture.debugElement.query(By.css('label')).nativeElement;
expect(el).not.toBeNull();
- expect(el.classList.contains('mat-empty')).toBe(false);
+ expect(el.classList.contains('mat-form-field-empty')).toBe(false);
}
}));
@@ -123,7 +127,7 @@ describe('MdInputContainer without forms', function () {
let el = fixture.debugElement.query(By.css('label')).nativeElement;
expect(el).not.toBeNull();
- expect(el.classList.contains('mat-empty')).toBe(true);
+ expect(el.classList.contains('mat-form-field-empty')).toBe(true);
}
}));
@@ -133,7 +137,7 @@ describe('MdInputContainer without forms', function () {
let el = fixture.debugElement.query(By.css('label')).nativeElement;
expect(el).not.toBeNull();
- expect(el.classList.contains('mat-empty')).toBe(true);
+ expect(el.classList.contains('mat-form-field-empty')).toBe(true);
});
it('should treat password input type as empty at init', () => {
@@ -142,7 +146,7 @@ describe('MdInputContainer without forms', function () {
let el = fixture.debugElement.query(By.css('label')).nativeElement;
expect(el).not.toBeNull();
- expect(el.classList.contains('mat-empty')).toBe(true);
+ expect(el.classList.contains('mat-form-field-empty')).toBe(true);
});
it('should treat number input type as empty at init', () => {
@@ -151,7 +155,7 @@ describe('MdInputContainer without forms', function () {
let el = fixture.debugElement.query(By.css('label')).nativeElement;
expect(el).not.toBeNull();
- expect(el.classList.contains('mat-empty')).toBe(true);
+ expect(el.classList.contains('mat-form-field-empty')).toBe(true);
});
it('should not be empty after input entered', async(() => {
@@ -161,7 +165,7 @@ describe('MdInputContainer without forms', function () {
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('mat-empty')).toBe(true, 'should be empty');
+ expect(el.classList.contains('mat-form-field-empty')).toBe(true, 'should be empty');
inputEl.nativeElement.value = 'hello';
// Simulate input event.
@@ -169,7 +173,7 @@ describe('MdInputContainer without forms', function () {
fixture.detectChanges();
el = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(el.classList.contains('mat-empty')).toBe(false, 'should not be empty');
+ expect(el.classList.contains('mat-form-field-empty')).toBe(false, 'should not be empty');
}));
it('should update the placeholder when input entered', async(() => {
@@ -179,8 +183,8 @@ describe('MdInputContainer without forms', function () {
let inputEl = fixture.debugElement.query(By.css('input'));
let labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(labelEl.classList).toContain('mat-empty');
- expect(labelEl.classList).not.toContain('mat-float');
+ expect(labelEl.classList).toContain('mat-form-field-empty');
+ expect(labelEl.classList).not.toContain('mat-form-field-float');
// Update the value of the input.
inputEl.nativeElement.value = 'Text';
@@ -188,22 +192,23 @@ describe('MdInputContainer without forms', function () {
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();
- expect(labelEl.classList).not.toContain('mat-empty');
- expect(labelEl.classList).not.toContain('mat-float');
+ expect(labelEl.classList).not.toContain('mat-form-field-empty');
+ expect(labelEl.classList).not.toContain('mat-form-field-float');
}));
it('should not be empty when the value set before view init', async(() => {
let fixture = TestBed.createComponent(MdInputContainerWithValueBinding);
fixture.detectChanges();
- let placeholderEl = fixture.debugElement.query(By.css('.mat-input-placeholder')).nativeElement;
+ let placeholderEl =
+ fixture.debugElement.query(By.css('.mat-form-field-placeholder')).nativeElement;
- expect(placeholderEl.classList).not.toContain('mat-empty');
+ expect(placeholderEl.classList).not.toContain('mat-form-field-empty');
fixture.componentInstance.value = '';
fixture.detectChanges();
- expect(placeholderEl.classList).toContain('mat-empty');
+ expect(placeholderEl.classList).toContain('mat-form-field-empty');
}));
it('should add id', () => {
@@ -236,40 +241,40 @@ describe('MdInputContainer without forms', function () {
let fixture = TestBed.createComponent(MdInputContainerInvalidHintTestController);
expect(() => fixture.detectChanges()).toThrowError(
- wrappedErrorMessage(getMdInputContainerDuplicatedHintError('start')));
+ wrappedErrorMessage(getMdFormFieldDuplicatedHintError('start')));
});
it('validates there\'s only one hint label per side (attribute)', () => {
let fixture = TestBed.createComponent(MdInputContainerInvalidHint2TestController);
expect(() => fixture.detectChanges()).toThrowError(
- wrappedErrorMessage(getMdInputContainerDuplicatedHintError('start')));
+ wrappedErrorMessage(getMdFormFieldDuplicatedHintError('start')));
});
it('validates there\'s only one placeholder', () => {
let fixture = TestBed.createComponent(MdInputContainerInvalidPlaceholderTestController);
expect(() => fixture.detectChanges()).toThrowError(
- wrappedErrorMessage(getMdInputContainerPlaceholderConflictError()));
+ wrappedErrorMessage(getMdFormFieldPlaceholderConflictError()));
});
it('validates that mdInput child is present', () => {
let fixture = TestBed.createComponent(MdInputContainerMissingMdInputTestController);
expect(() => fixture.detectChanges()).toThrowError(
- wrappedErrorMessage(getMdInputContainerMissingMdInputError()));
+ wrappedErrorMessage(getMdFormFieldMissingControlError()));
});
it('validates that mdInput child is present after initialization', async(() => {
let fixture = TestBed.createComponent(MdInputContainerWithNgIf);
expect(() => fixture.detectChanges()).not.toThrowError(
- wrappedErrorMessage(getMdInputContainerMissingMdInputError()));
+ wrappedErrorMessage(getMdFormFieldMissingControlError()));
fixture.componentInstance.renderInput = false;
expect(() => fixture.detectChanges()).toThrowError(
- wrappedErrorMessage(getMdInputContainerMissingMdInputError()));
+ wrappedErrorMessage(getMdFormFieldMissingControlError()));
}));
it('validates the type', () => {
@@ -381,7 +386,7 @@ describe('MdInputContainer without forms', function () {
let fixture = TestBed.createComponent(MdInputContainerPlaceholderRequiredTestComponent);
fixture.detectChanges();
- let el = fixture.debugElement.query(By.css('.mat-placeholder-required')).nativeElement;
+ let el = fixture.debugElement.query(By.css('.mat-form-field-required-marker')).nativeElement;
expect(el.getAttribute('aria-hidden')).toBe('true');
});
@@ -404,7 +409,8 @@ describe('MdInputContainer without forms', function () {
const fixture = TestBed.createComponent(MdInputContainerWithDisabled);
fixture.detectChanges();
- const underlineEl = fixture.debugElement.query(By.css('.mat-input-underline')).nativeElement;
+ const underlineEl =
+ fixture.debugElement.query(By.css('.mat-form-field-underline')).nativeElement;
const inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(underlineEl.classList.contains('mat-disabled'))
@@ -496,8 +502,8 @@ describe('MdInputContainer without forms', function () {
fixture.detectChanges();
- let hintLabel = fixture.debugElement.query(By.css('.mat-hint')).nativeElement;
- let endLabel = fixture.debugElement.query(By.css('.mat-hint[align="end"]')).nativeElement;
+ let hintLabel = fixture.debugElement.query(By.css('.mat-hint:not(.mat-right)')).nativeElement;
+ let endLabel = fixture.debugElement.query(By.css('.mat-hint.mat-right')).nativeElement;
let input = fixture.debugElement.query(By.css('input')).nativeElement;
let ariaValue = input.getAttribute('aria-describedby');
@@ -511,14 +517,14 @@ describe('MdInputContainer without forms', function () {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
let labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(labelEl.classList).not.toContain('mat-empty');
- expect(labelEl.classList).toContain('mat-float');
+ expect(labelEl.classList).not.toContain('mat-form-field-empty');
+ expect(labelEl.classList).toContain('mat-form-field-float');
fixture.componentInstance.shouldFloat = 'auto';
fixture.detectChanges();
- expect(labelEl.classList).toContain('mat-empty');
- expect(labelEl.classList).toContain('mat-float');
+ expect(labelEl.classList).toContain('mat-form-field-empty');
+ expect(labelEl.classList).toContain('mat-form-field-float');
// Update the value of the input.
inputEl.value = 'Text';
@@ -526,8 +532,8 @@ describe('MdInputContainer without forms', function () {
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();
- expect(labelEl.classList).not.toContain('mat-empty');
- expect(labelEl.classList).toContain('mat-float');
+ expect(labelEl.classList).not.toContain('mat-form-field-empty');
+ expect(labelEl.classList).toContain('mat-form-field-float');
});
it('should always float the placeholder when floatPlaceholder is set to true', () => {
@@ -537,8 +543,8 @@ describe('MdInputContainer without forms', function () {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
let labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(labelEl.classList).not.toContain('mat-empty');
- expect(labelEl.classList).toContain('mat-float');
+ expect(labelEl.classList).not.toContain('mat-form-field-empty');
+ expect(labelEl.classList).toContain('mat-form-field-float');
fixture.detectChanges();
@@ -548,8 +554,8 @@ describe('MdInputContainer without forms', function () {
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();
- expect(labelEl.classList).not.toContain('mat-empty');
- expect(labelEl.classList).toContain('mat-float');
+ expect(labelEl.classList).not.toContain('mat-form-field-empty');
+ expect(labelEl.classList).toContain('mat-form-field-float');
});
@@ -562,8 +568,8 @@ describe('MdInputContainer without forms', function () {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
let labelEl = fixture.debugElement.query(By.css('label')).nativeElement;
- expect(labelEl.classList).toContain('mat-empty');
- expect(labelEl.classList).not.toContain('mat-float');
+ expect(labelEl.classList).toContain('mat-form-field-empty');
+ expect(labelEl.classList).not.toContain('mat-form-field-float');
// Update the value of the input.
inputEl.value = 'Text';
@@ -571,8 +577,8 @@ describe('MdInputContainer without forms', function () {
// Fake behavior of the `(input)` event which should trigger a change detection.
fixture.detectChanges();
- expect(labelEl.classList).not.toContain('mat-empty');
- expect(labelEl.classList).not.toContain('mat-float');
+ expect(labelEl.classList).not.toContain('mat-form-field-empty');
+ expect(labelEl.classList).not.toContain('mat-form-field-float');
});
it('should be able to toggle the floating placeholder programmatically', () => {
@@ -580,26 +586,27 @@ describe('MdInputContainer without forms', function () {
fixture.detectChanges();
- const inputContainer = fixture.debugElement.query(By.directive(MdInputContainer));
- const containerInstance = inputContainer.componentInstance as MdInputContainer;
- const placeholder = inputContainer.nativeElement.querySelector('.mat-input-placeholder');
+ const inputContainer = fixture.debugElement.query(By.directive(MdFormField));
+ const containerInstance = inputContainer.componentInstance as MdFormField;
+ const placeholder = inputContainer.nativeElement.querySelector('.mat-form-field-placeholder');
expect(containerInstance.floatPlaceholder).toBe('auto');
- expect(placeholder.classList).toContain('mat-empty', 'Expected input to be considered empty.');
+ expect(placeholder.classList)
+ .toContain('mat-form-field-empty', 'Expected input to be considered empty.');
containerInstance.floatPlaceholder = 'always';
fixture.detectChanges();
expect(placeholder.classList)
- .not.toContain('mat-empty', 'Expected input to be considered not empty.');
+ .not.toContain('mat-form-field-empty', 'Expected input to be considered not empty.');
});
it('should not have prefix and suffix elements when none are specified', () => {
let fixture = TestBed.createComponent(MdInputContainerWithId);
fixture.detectChanges();
- let prefixEl = fixture.debugElement.query(By.css('.mat-input-prefix'));
- let suffixEl = fixture.debugElement.query(By.css('.mat-input-suffix'));
+ let prefixEl = fixture.debugElement.query(By.css('.mat-form-field-prefix'));
+ let suffixEl = fixture.debugElement.query(By.css('.mat-form-field-suffix'));
expect(prefixEl).toBeNull();
expect(suffixEl).toBeNull();
@@ -609,8 +616,8 @@ describe('MdInputContainer without forms', function () {
let fixture = TestBed.createComponent(MdInputContainerWithPrefixAndSuffix);
fixture.detectChanges();
- let prefixEl = fixture.debugElement.query(By.css('.mat-input-prefix'));
- let suffixEl = fixture.debugElement.query(By.css('.mat-input-suffix'));
+ let prefixEl = fixture.debugElement.query(By.css('.mat-form-field-prefix'));
+ let suffixEl = fixture.debugElement.query(By.css('.mat-form-field-suffix'));
expect(prefixEl).not.toBeNull();
expect(suffixEl).not.toBeNull();
@@ -623,49 +630,32 @@ describe('MdInputContainer without forms', function () {
fixture.detectChanges();
let component = fixture.componentInstance;
- let placeholder = fixture.debugElement
- .query(By.css('.mat-input-placeholder')).nativeElement;
+ let placeholder =
+ fixture.debugElement.query(By.css('.mat-form-field-placeholder')).nativeElement;
- expect(placeholder.classList).toContain('mat-empty', 'Input initially empty');
+ expect(placeholder.classList).toContain('mat-form-field-empty', 'Input initially empty');
component.formControl.setValue('something');
fixture.detectChanges();
- expect(placeholder.classList).not.toContain('mat-empty', 'Input no longer empty');
+ expect(placeholder.classList).not.toContain('mat-form-field-empty', 'Input no longer empty');
});
it('should set the focused class when the input is focused', () => {
let fixture = TestBed.createComponent(MdInputContainerTextTestController);
fixture.detectChanges();
- let input = fixture.debugElement.query(By.directive(MdInputDirective))
- .injector.get(MdInputDirective);
+ let input = fixture.debugElement.query(By.directive(MdInput))
+ .injector.get(MdInput);
let container = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
// Call the focus handler directly to avoid flakyness where
// browsers don't focus elements if the window is minimized.
- input._onFocus();
+ input._focusChanged(true);
fixture.detectChanges();
expect(container.classList).toContain('mat-focused');
});
-
- it('should not highlight when focusing a readonly input', () => {
- let fixture = TestBed.createComponent(MdInputContainerWithReadonlyInput);
- fixture.detectChanges();
-
- let input = fixture.debugElement.query(By.directive(MdInputDirective))
- .injector.get(MdInputDirective);
- let container = fixture.debugElement.query(By.css('md-input-container')).nativeElement;
-
- // Call the focus handler directly to avoid flakyness where
- // browsers don't focus elements if the window is minimized.
- input._onFocus();
- fixture.detectChanges();
-
- expect(input.focused).toBe(false);
- expect(container.classList).not.toContain('mat-focused');
- });
});
describe('MdInputContainer with forms', () => {
@@ -674,6 +664,7 @@ describe('MdInputContainer with forms', () => {
TestBed.configureTestingModule({
imports: [
FormsModule,
+ MdFormFieldModule,
MdInputModule,
NoopAnimationsModule,
PlatformModule,
@@ -721,7 +712,7 @@ describe('MdInputContainer with forms', () => {
fixture.whenStable().then(() => {
expect(containerEl.classList)
- .toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(inputEl.getAttribute('aria-invalid'))
@@ -740,7 +731,7 @@ describe('MdInputContainer with forms', () => {
fixture.whenStable().then(() => {
expect(testComponent.form.submitted).toBe(true, 'Expected form to have been submitted');
expect(containerEl.classList)
- .toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(inputEl.getAttribute('aria-invalid'))
@@ -773,7 +764,7 @@ describe('MdInputContainer with forms', () => {
expect(component.formGroupDirective.submitted)
.toBe(true, 'Expected form to have been submitted');
expect(containerEl.classList)
- .toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(inputEl.getAttribute('aria-invalid'))
@@ -787,7 +778,7 @@ describe('MdInputContainer with forms', () => {
fixture.whenStable().then(() => {
expect(containerEl.classList)
- .toContain('mat-input-invalid', 'Expected container to have the invalid CSS class.');
+ .toContain('mat-form-field-invalid', 'Expected container to have the invalid CSS class.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(1, 'Expected one error message to have been rendered.');
expect(containerEl.querySelectorAll('md-hint').length)
@@ -797,7 +788,7 @@ describe('MdInputContainer with forms', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
- expect(containerEl.classList).not.toContain('mat-input-invalid',
+ expect(containerEl.classList).not.toContain('mat-form-field-invalid',
'Expected container not to have the invalid class when valid.');
expect(containerEl.querySelectorAll('md-error').length)
.toBe(0, 'Expected no error messages when the input is valid.');
@@ -840,7 +831,7 @@ describe('MdInputContainer with forms', () => {
fixture.componentInstance.formControl.markAsTouched();
fixture.detectChanges();
- let errorIds = fixture.debugElement.queryAll(By.css('.mat-input-error'))
+ let errorIds = fixture.debugElement.queryAll(By.css('.mat-error'))
.map(el => el.nativeElement.getAttribute('id')).join(' ');
describedBy = inputEl.getAttribute('aria-describedby');
@@ -888,6 +879,7 @@ describe('MdInputContainer with forms', () => {
TestBed.configureTestingModule({
imports: [
FormsModule,
+ MdFormFieldModule,
MdInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
@@ -919,6 +911,7 @@ describe('MdInputContainer with forms', () => {
TestBed.configureTestingModule({
imports: [
FormsModule,
+ MdFormFieldModule,
MdInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
@@ -961,8 +954,8 @@ describe('MdInputContainer with forms', () => {
let fixture = TestBed.createComponent(MdInputContainerWithFormControl);
fixture.detectChanges();
- let input = fixture.debugElement.query(By.directive(MdInputDirective))
- .injector.get(MdInputDirective);
+ let input = fixture.debugElement.query(By.directive(MdInput))
+ .injector.get(MdInput);
expect(input.value).toBeFalsy();
@@ -975,7 +968,8 @@ describe('MdInputContainer with forms', () => {
const fixture = TestBed.createComponent(MdInputContainerWithFormControl);
fixture.detectChanges();
- const underlineEl = fixture.debugElement.query(By.css('.mat-input-underline')).nativeElement;
+ const underlineEl =
+ fixture.debugElement.query(By.css('.mat-form-field-underline')).nativeElement;
const inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(underlineEl.classList)
@@ -999,7 +993,7 @@ describe('MdInputContainer with forms', () => {
let el = fixture.debugElement.query(By.css('label')).nativeElement;
expect(el).not.toBeNull();
- expect(el.classList.contains('mat-empty')).toBe(false);
+ expect(el.classList.contains('mat-form-field-empty')).toBe(false);
});
}));
});
diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts
new file mode 100644
index 000000000000..f0449a9bc994
--- /dev/null
+++ b/src/lib/input/input.ts
@@ -0,0 +1,286 @@
+/**
+ * @license
+ * Copyright Google Inc. All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ Directive,
+ DoCheck,
+ ElementRef,
+ Inject,
+ Input,
+ OnChanges,
+ OnDestroy,
+ Optional,
+ Renderer2,
+ Self,
+} from '@angular/core';
+import {coerceBooleanProperty, Platform} from '../core';
+import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms';
+import {getSupportedInputTypes} from '../core/platform/features';
+import {getMdInputUnsupportedTypeError} from './input-errors';
+import {
+ defaultErrorStateMatcher,
+ ErrorOptions,
+ ErrorStateMatcher,
+ MD_ERROR_GLOBAL_OPTIONS
+} from '../core/error/error-options';
+import {Subject} from 'rxjs/Subject';
+import {MdFormFieldControl} from '../form-field/index';
+
+// Invalid input type. Using one of these will throw an MdInputUnsupportedTypeError.
+const MD_INPUT_INVALID_TYPES = [
+ 'button',
+ 'checkbox',
+ 'color',
+ 'file',
+ 'hidden',
+ 'image',
+ 'radio',
+ 'range',
+ 'reset',
+ 'submit'
+];
+
+let nextUniqueId = 0;
+
+
+/** Directive that allows a native input to work inside a `MdFormField`. */
+@Directive({
+ selector: `input[mdInput], textarea[mdInput], input[matInput], textarea[matInput]`,
+ host: {
+ 'class': 'mat-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',
+ '[attr.aria-describedby]': '_ariaDescribedby || null',
+ '[attr.aria-invalid]': 'errorState',
+ '(blur)': '_focusChanged(false)',
+ '(focus)': '_focusChanged(true)',
+ '(input)': '_onInput()',
+ },
+ providers: [{provide: MdFormFieldControl, useExisting: MdInput}],
+})
+export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoCheck {
+ /** Variables used as cache for getters and setters. */
+ private _type = 'text';
+ private _disabled = false;
+ private _required = false;
+ private _id: string;
+ private _uid = `md-input-${nextUniqueId++}`;
+ private _errorOptions: ErrorOptions;
+ private _previousNativeValue = this.value;
+
+ /** Whether the input is focused. */
+ focused = false;
+
+ /** Whether the input is in an error state. */
+ errorState = false;
+
+ /** The aria-describedby attribute on the input for improved a11y. */
+ _ariaDescribedby: string;
+
+ /**
+ * Stream that emits whenever the state of the input changes such that the wrapping `MdFormField`
+ * needs to run change detection.
+ */
+ stateChanges = new Subject();
+
+ /** Whether the element is disabled. */
+ @Input()
+ get disabled() { return this.ngControl ? this.ngControl.disabled : this._disabled; }
+ set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
+
+ /** Unique id of the element. */
+ @Input()
+ get id() { return this._id; }
+ set id(value: string) { this._id = value || this._uid; }
+
+ /** Placeholder attribute of the element. */
+ @Input() placeholder: string = '';
+
+ /** Whether the element is required. */
+ @Input()
+ get required() { return this._required; }
+ set required(value: any) { this._required = coerceBooleanProperty(value); }
+
+ /** Input type of the element. */
+ @Input()
+ get type() { return this._type; }
+ set type(value: string) {
+ this._type = value || 'text';
+ this._validateType();
+
+ // 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() && getSupportedInputTypes().has(this._type)) {
+ this._renderer.setProperty(this._elementRef.nativeElement, 'type', this._type);
+ }
+ }
+
+ /** A function used to control when error messages are shown. */
+ @Input() errorStateMatcher: ErrorStateMatcher;
+
+ /** The input element's value. */
+ get value() { return this._elementRef.nativeElement.value; }
+ set value(value: string) {
+ if (value !== this.value) {
+ this._elementRef.nativeElement.value = value;
+ this.stateChanges.next();
+ }
+ }
+
+ private _neverEmptyInputTypes = [
+ 'date',
+ 'datetime',
+ 'datetime-local',
+ 'month',
+ 'time',
+ 'week'
+ ].filter(t => getSupportedInputTypes().has(t));
+
+ constructor(private _elementRef: ElementRef,
+ private _renderer: Renderer2,
+ private _platform: Platform,
+ @Optional() @Self() public ngControl: NgControl,
+ @Optional() private _parentForm: NgForm,
+ @Optional() private _parentFormGroup: FormGroupDirective,
+ @Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) {
+
+ // Force setter to be called in case id was not specified.
+ this.id = this.id;
+ this._errorOptions = errorOptions ? errorOptions : {};
+ this.errorStateMatcher = this._errorOptions.errorStateMatcher || defaultErrorStateMatcher;
+
+ // On some versions of iOS the caret gets stuck in the wrong place when holding down the delete
+ // key. In order to get around this we need to "jiggle" the caret loose. Since this bug only
+ // exists on iOS, we only bother to install the listener on iOS.
+ if (_platform.IOS) {
+ _renderer.listen(_elementRef.nativeElement, 'keyup', (event: Event) => {
+ let el = event.target as HTMLInputElement;
+ if (!el.value && !el.selectionStart && !el.selectionEnd) {
+ // Note: Just setting `0, 0` doesn't fix the issue. Setting `1, 1` fixes it for the first
+ // time that you type text and then hold delete. Toggling to `1, 1` and then back to
+ // `0, 0` seems to completely fix it.
+ el.setSelectionRange(1, 1);
+ el.setSelectionRange(0, 0);
+ }
+ });
+ }
+ }
+
+ ngOnChanges() {
+ this.stateChanges.next();
+ }
+
+ ngOnDestroy() {
+ this.stateChanges.complete();
+ }
+
+ ngDoCheck() {
+ if (this.ngControl) {
+ // We need to re-evaluate this on every change detection cycle, because there are some
+ // error triggers that we can't subscribe to (e.g. parent form submissions). This means
+ // that whatever logic is in here has to be super lean or we risk destroying the performance.
+ this._updateErrorState();
+ } else {
+ // When the input isn't used together with `@angular/forms`, we need to check manually for
+ // changes to the native `value` property in order to update the floating label.
+ this._dirtyCheckNativeValue();
+ }
+ }
+
+ /** Callback for the cases where the focused state of the input changes. */
+ _focusChanged(isFocused: boolean) {
+ if (isFocused !== this.focused) {
+ this.focused = isFocused;
+ this.stateChanges.next();
+ }
+ }
+
+ _onInput() {
+ // This is a noop function and is used to let Angular know whenever the value changes.
+ // Angular will run a new change detection each time the `input` event has been dispatched.
+ // It's necessary that Angular recognizes the value change, because when floatingLabel
+ // is set to false and Angular forms aren't used, the placeholder won't recognize the
+ // value changes and will not disappear.
+ // Listening to the input event wouldn't be necessary when the input is using the
+ // FormsModule or ReactiveFormsModule, because Angular forms also listens to input events.
+ }
+
+ /** Re-evaluates the error state. This is only relevant with @angular/forms. */
+ private _updateErrorState() {
+ const oldState = this.errorState;
+ const ngControl = this.ngControl;
+ const parent = this._parentFormGroup || this._parentForm;
+ const newState = ngControl && this.errorStateMatcher(ngControl.control as FormControl, parent);
+
+ if (newState !== oldState) {
+ this.errorState = newState;
+ this.stateChanges.next();
+ }
+ }
+
+ /** Does some manual dirty checking on the native input `value` property. */
+ private _dirtyCheckNativeValue() {
+ const newValue = this.value;
+
+ if (this._previousNativeValue !== newValue) {
+ this._previousNativeValue = newValue;
+ this.stateChanges.next();
+ }
+ }
+
+ /** Make sure the input is a supported type. */
+ private _validateType() {
+ if (MD_INPUT_INVALID_TYPES.indexOf(this._type) > -1) {
+ throw getMdInputUnsupportedTypeError(this._type);
+ }
+ }
+
+ /** Checks whether the input type is one of the types that are never empty. */
+ private _isNeverEmpty() {
+ return this._neverEmptyInputTypes.indexOf(this._type) > -1;
+ }
+
+ /** Checks whether the input is invalid based on the native validation. */
+ private _isBadInput() {
+ // The `validity` property won't be present on platform-server.
+ let validity = (this._elementRef.nativeElement as HTMLInputElement).validity;
+ return validity && validity.badInput;
+ }
+
+ /** Determines if the component host is a textarea. If not recognizable it returns false. */
+ private _isTextarea() {
+ let nativeElement = this._elementRef.nativeElement;
+
+ // In Universal, we don't have access to `nodeName`, but the same can be achieved with `name`.
+ // Note that this shouldn't be necessary once Angular switches to an API that resembles the
+ // DOM closer.
+ let nodeName = this._platform.isBrowser ? nativeElement.nodeName : nativeElement.name;
+ return nodeName ? nodeName.toLowerCase() === 'textarea' : false;
+ }
+
+ // Implemented as part of MdFormFieldControl.
+ get empty(): boolean {
+ return !this._isNeverEmpty() &&
+ (this.value == null || this.value === '') &&
+ // Check if the input contains bad input. If so, we know that it only appears empty because
+ // the value failed to parse. From the user's perspective it is not empty.
+ // TODO(mmalerba): Add e2e test for bad input case.
+ !this._isBadInput();
+ }
+
+ // Implemented as part of MdFormFieldControl.
+ setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); }
+
+ // Implemented as part of MdFormFieldControl.
+ focus() { this._elementRef.nativeElement.focus(); }
+}
diff --git a/src/lib/module.ts b/src/lib/module.ts
index e5fa0671e79b..6d867b1d777a 100644
--- a/src/lib/module.ts
+++ b/src/lib/module.ts
@@ -48,6 +48,7 @@ import {MdExpansionModule} from './expansion/index';
import {MdTableModule} from './table/index';
import {MdSortModule} from './sort/index';
import {MdPaginatorModule} from './paginator/index';
+import {MdFormFieldModule} from './form-field/index';
const MATERIAL_MODULES = [
MdAutocompleteModule,
@@ -60,6 +61,7 @@ const MATERIAL_MODULES = [
MdTableModule,
MdDialogModule,
MdExpansionModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,
diff --git a/src/lib/public_api.ts b/src/lib/public_api.ts
index a42ab73d968b..7c4bdffb425a 100644
--- a/src/lib/public_api.ts
+++ b/src/lib/public_api.ts
@@ -25,6 +25,7 @@ export * from './checkbox/index';
export * from './datepicker/index';
export * from './dialog/index';
export * from './expansion/index';
+export * from './form-field/index';
export * from './grid-list/index';
export * from './icon/index';
export * from './input/index';
diff --git a/src/material-examples/material-module.ts b/src/material-examples/material-module.ts
index eb050ff20f7b..c4f234d336cb 100644
--- a/src/material-examples/material-module.ts
+++ b/src/material-examples/material-module.ts
@@ -8,7 +8,7 @@ import {
MdListModule, MdMenuModule, MdProgressBarModule, MdProgressSpinnerModule,
MdRadioModule, MdSelectModule, MdSidenavModule, MdSliderModule, MdSortModule,
MdSlideToggleModule, MdSnackBarModule, MdTableModule, MdTabsModule, MdToolbarModule,
- MdTooltipModule
+ MdTooltipModule, MdFormFieldModule
} from '@angular/material';
@NgModule({
@@ -22,6 +22,7 @@ import {
MdChipsModule,
MdDatepickerModule,
MdDialogModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,
diff --git a/src/material-examples/table-filtering/table-filtering-example.css b/src/material-examples/table-filtering/table-filtering-example.css
index cd8b70d5b9fa..5d643e27561d 100644
--- a/src/material-examples/table-filtering/table-filtering-example.css
+++ b/src/material-examples/table-filtering/table-filtering-example.css
@@ -22,7 +22,7 @@
justify-content: space-between;
}
-.mat-input-container {
+.mat-form-field {
font-size: 14px;
flex-grow: 1;
margin-left: 32px;
diff --git a/src/material-examples/table-overview/table-overview-example.css b/src/material-examples/table-overview/table-overview-example.css
index 50e84d1bbecd..c5c27f6fa424 100644
--- a/src/material-examples/table-overview/table-overview-example.css
+++ b/src/material-examples/table-overview/table-overview-example.css
@@ -16,7 +16,7 @@
border-bottom: 1px solid transparent;
}
-.mat-input-container {
+.mat-form-field {
font-size: 14px;
flex-grow: 1;
margin-top: 8px;
diff --git a/src/universal-app/kitchen-sink/kitchen-sink.ts b/src/universal-app/kitchen-sink/kitchen-sink.ts
index cf5b756795c1..73c641203a1b 100644
--- a/src/universal-app/kitchen-sink/kitchen-sink.ts
+++ b/src/universal-app/kitchen-sink/kitchen-sink.ts
@@ -10,6 +10,7 @@ import {
MdDatepickerModule,
MdDialogModule,
MdExpansionModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,
@@ -72,6 +73,7 @@ export class KitchenSink {
MdChipsModule,
MdDatepickerModule,
MdDialogModule,
+ MdFormFieldModule,
MdGridListModule,
MdIconModule,
MdInputModule,