From 863bd28a592f80b7ac3f371f00b845ecf3c7d897 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Fri, 4 Aug 2017 15:10:08 -0700 Subject: [PATCH 1/8] refactor all communication between input-container and input into a separate interface --- src/lib/autocomplete/autocomplete-trigger.ts | 7 +- src/lib/input/input-container.html | 10 +- src/lib/input/input-container.spec.ts | 19 +-- src/lib/input/input-container.ts | 160 ++++++++++++------- 4 files changed, 116 insertions(+), 80 deletions(-) diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index ee98bc35fee3..b32ed97aed48 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 {MdInputContainer, MdInputDirective} from '../input/input-container'; import {Subscription} from 'rxjs/Subscription'; import {merge} from 'rxjs/observable/merge'; import {fromEvent} from 'rxjs/observable/fromEvent'; @@ -410,8 +410,9 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { // 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 (this._inputContainer && + this._inputContainer._textFieldControl instanceof MdInputDirective) { + this._inputContainer._textFieldControl.value = inputValue; } else { this._element.nativeElement.value = inputValue; } diff --git a/src/lib/input/input-container.html b/src/lib/input/input-container.html index 0714e4019a79..6fd6b64c4f98 100644 --- a/src/lib/input/input-container.html +++ b/src/lib/input/input-container.html @@ -9,18 +9,18 @@ @@ -31,7 +31,7 @@
+ [class.mat-disabled]="_textFieldControl.isDisabled()"> diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 4c1b92ed567c..ec3e69467f9f 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -644,28 +644,11 @@ describe('MdInputContainer without forms', function () { // 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', () => { diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index d6c27ceecf74..99028b35889b 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -52,6 +52,7 @@ import { } from '../core/error/error-options'; import {Subject} from 'rxjs/Subject'; import {startWith} from '@angular/cdk/rxjs'; +import {Observable} from 'rxjs/Observable'; // Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError. const MD_INPUT_INVALID_TYPES = [ @@ -97,6 +98,7 @@ export class MdHint { @Input() id: string = `md-input-hint-${nextUniqueId++}`; } + /** Single error message to be shown underneath the input. */ @Directive({ selector: 'md-error, mat-error', @@ -110,6 +112,7 @@ export class MdErrorDirective { @Input() id: string = `md-input-error-${nextUniqueId++}`; } + /** Prefix to be placed the the front of the input. */ @Directive({ selector: '[mdPrefix], [matPrefix]' @@ -124,6 +127,43 @@ export class MdPrefix {} export class MdSuffix {} +/** An interface which allows a control to work inside of a md-text-field. */ +export abstract class MdTextFieldControl { + /** Stream that emits whenever the state of the control changes. */ + stateChanges: Observable; + + /** Gets the element ID for this control. */ + abstract getId(): string; + + /** Fets the placeholder for this contorl. */ + abstract getPlaceholder(): string; + + /** Gets the NgControl for this control. */ + abstract getNgControl(): NgControl | null; + + /** Whether the control is focused. */ + abstract isFocused(): boolean; + + /** Whether the control is empty. */ + abstract isEmpty(): boolean; + + /** Whether the control is required. */ + abstract isRequired(): boolean; + + /** Whether the control is disabled. */ + abstract isDisabled(): boolean; + + /** Whether the control is in an error state. */ + abstract isErrorState(): boolean; + + /** Sets the list of element IDs that currently describe this control. */ + abstract setDescribedByIds(ids: string[]): void; + + /** Focuses this control. */ + abstract focus(): void; +} + + /** Marker for the input element that `MdInputContainer` is wrapping. */ @Directive({ selector: `input[mdInput], textarea[mdInput], input[matInput], textarea[matInput]`, @@ -135,39 +175,38 @@ export class MdSuffix {} '[placeholder]': 'placeholder', '[disabled]': 'disabled', '[required]': 'required', - '[attr.aria-describedby]': 'ariaDescribedby || null', + '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-invalid]': '_isErrorState', '(blur)': '_focusChanged(false)', '(focus)': '_focusChanged(true)', '(input)': '_onInput()', - } + }, + providers: [{provide: MdTextFieldControl, useExisting: MdInputDirective}], }) -export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { +export class MdInputDirective implements MdTextFieldControl, 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; + private _focused = false; /** 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; + _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(); + stateChanges = new Subject(); /** Whether the element is disabled. */ @Input() @@ -202,11 +241,6 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { } } - /** 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; @@ -215,20 +249,10 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { set value(value: string) { if (value !== this.value) { this._elementRef.nativeElement.value = value; - this._stateChanges.next(); + 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', @@ -269,11 +293,11 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { } ngOnChanges() { - this._stateChanges.next(); + this.stateChanges.next(); } ngOnDestroy() { - this._stateChanges.complete(); + this.stateChanges.complete(); } ngDoCheck() { @@ -289,12 +313,6 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { } } - _onFocus() { - if (!this._readonly) { - this.focused = true; - } - } - /** Focuses the input element. */ focus() { this._elementRef.nativeElement.focus(); @@ -302,9 +320,9 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { /** 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(); + if (isFocused !== this._focused) { + this._focused = isFocused; + this.stateChanges.next(); } } @@ -327,7 +345,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { if (newState !== oldState) { this._isErrorState = newState; - this._stateChanges.next(); + this.stateChanges.next(); } } @@ -337,7 +355,7 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { if (this._previousNativeValue !== newValue) { this._previousNativeValue = newValue; - this._stateChanges.next(); + this.stateChanges.next(); } } @@ -370,6 +388,40 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { let nodeName = this._platform.isBrowser ? nativeElement.nodeName : nativeElement.name; return nodeName ? nodeName.toLowerCase() === 'textarea' : false; } + + // Implemented as part of MdTextFieldControl. + getId(): string { return this.id; } + + // Implemented as part of MdTextFieldControl. + getPlaceholder(): string { return this.placeholder; } + + // Implemented as part of MdTextFieldControl. + getNgControl(): NgControl | null { return this._ngControl; } + + // Implemented as part of MdTextFieldControl. + isFocused(): boolean { return this._focused; } + + // Implemented as part of MdTextFieldControl. + isEmpty(): 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 MdTextFieldControl. + isRequired(): boolean { return this.required; } + + // Implemented as part of MdTextFieldControl. + isDisabled(): boolean { return this.disabled; } + + // Implemented as part of MdTextFieldControl. + isErrorState(): boolean { return this._isErrorState; } + + // Implemented as part of MdTextFieldControl. + setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); } } @@ -394,8 +446,8 @@ export class MdInputDirective implements OnChanges, OnDestroy, DoCheck { // 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.mat-input-invalid]': '_textFieldControl.isErrorState()', + '[class.mat-focused]': '_textFieldControl.isFocused()', '[class.ng-untouched]': '_shouldForward("untouched")', '[class.ng-touched]': '_shouldForward("touched")', '[class.ng-pristine]': '_shouldForward("pristine")', @@ -463,7 +515,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC /** Reference to the input's underline element. */ @ViewChild('underline') underlineRef: ElementRef; @ViewChild('connectionContainer') _connectionContainerRef: ElementRef; - @ContentChild(MdInputDirective) _mdInputChild: MdInputDirective; + @ContentChild(MdTextFieldControl) _textFieldControl: MdTextFieldControl; @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; @ContentChildren(MdErrorDirective) _errorChildren: QueryList; @ContentChildren(MdHint) _hintChildren: QueryList; @@ -482,14 +534,15 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC this._validateInputChild(); // Subscribe to changes in the child input state in order to update the container UI. - startWith.call(this._mdInputChild._stateChanges, null).subscribe(() => { + startWith.call(this._textFieldControl.stateChanges, null).subscribe(() => { this._validatePlaceholders(); this._syncAriaDescribedby(); this._changeDetectorRef.markForCheck(); }); - if (this._mdInputChild._ngControl && this._mdInputChild._ngControl.valueChanges) { - this._mdInputChild._ngControl.valueChanges.subscribe(() => { + let ngControl = this._textFieldControl.getNgControl(); + if (ngControl && ngControl.valueChanges) { + ngControl.valueChanges.subscribe(() => { this._changeDetectorRef.markForCheck(); }); } @@ -519,25 +572,24 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC /** 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]; + let ngControl = this._textFieldControl ? this._textFieldControl.getNgControl : null; + return ngControl && (ngControl as any)[prop]; } /** Whether the input has a placeholder. */ _hasPlaceholder() { - return !!(this._mdInputChild.placeholder || this._placeholderChild); + return !!(this._textFieldControl.getPlaceholder() || this._placeholderChild); } /** Focuses the underlying input. */ _focusInput() { - this._mdInputChild.focus(); + this._textFieldControl.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'; + return (this._errorChildren && this._errorChildren.length > 0 && + this._textFieldControl.isErrorState()) ? 'error' : 'hint'; } /** @@ -545,7 +597,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC * `md-placeholder` attribute. */ private _validatePlaceholders() { - if (this._mdInputChild.placeholder && this._placeholderChild) { + if (this._textFieldControl.getPlaceholder() && this._placeholderChild) { throw getMdInputContainerPlaceholderConflictError(); } } @@ -587,7 +639,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC * of the currently-specified hints, as well as a generated id for the hint label. */ private _syncAriaDescribedby() { - if (this._mdInputChild) { + if (this._textFieldControl) { let ids: string[] = []; if (this._getDisplayedMessages() === 'hint') { @@ -609,7 +661,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC ids = this._errorChildren.map(mdError => mdError.id); } - this._mdInputChild.ariaDescribedby = ids.join(' '); + this._textFieldControl.setDescribedByIds(ids); } } @@ -617,7 +669,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC * Throws an error if the container's input child was removed. */ protected _validateInputChild() { - if (!this._mdInputChild) { + if (!this._textFieldControl) { throw getMdInputContainerMissingMdInputError(); } } From b607297cdc54fe2db0a5fa73b01902fd1273d40b Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sat, 5 Aug 2017 17:44:09 -0700 Subject: [PATCH 2/8] rename input-container to form-field --- src/demo-app/datepicker/datepicker-demo.html | 28 ++-- src/lib/autocomplete/autocomplete-trigger.ts | 8 +- src/lib/autocomplete/autocomplete.spec.ts | 4 +- src/lib/datepicker/datepicker-input.ts | 4 +- src/lib/input/index.ts | 18 +- src/lib/input/input-container-errors.ts | 14 +- src/lib/input/input-container.html | 12 +- src/lib/input/input-container.spec.ts | 40 ++--- src/lib/input/input-container.ts | 163 +++++++++---------- 9 files changed, 143 insertions(+), 148 deletions(-) diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html index aa63ee703de1..c2ff2cd01a6b 100644 --- a/src/demo-app/datepicker/datepicker-demo.html +++ b/src/demo-app/datepicker/datepicker-demo.html @@ -9,20 +9,20 @@

Options

+ - + -

+ -

Result

@@ -40,6 +40,13 @@

Result

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/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index b32ed97aed48..5d94b20df688 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, MdInputDirective} from '../input/input-container'; +import {MdFormField, MdInput} from '../input/input-container'; 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 _inputContainer: MdFormField, @Optional() @Inject(DOCUMENT) private _document: any) {} ngOnDestroy() { @@ -411,8 +411,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy { // 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._textFieldControl instanceof MdInputDirective) { - this._inputContainer._textFieldControl.value = inputValue; + this._inputContainer._control instanceof MdInput) { + this._inputContainer._control.value = inputValue; } else { this._element.nativeElement.value = inputValue; } diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 927cddd98b0a..d582cc519a03 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} from '../input/input-container'; import {Observable} from 'rxjs/Observable'; import {Subject} from 'rxjs/Subject'; import {createKeyboardEvent, dispatchFakeEvent, typeInElement} from '@angular/cdk/testing'; @@ -1417,7 +1417,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/datepicker/datepicker-input.ts b/src/lib/datepicker/datepicker-input.ts index 156e481f9746..0b7eaff6da01 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 '../input/input-container'; 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/input/index.ts b/src/lib/input/index.ts index b616a558408d..47aaea5706d4 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -8,10 +8,10 @@ import {NgModule} from '@angular/core'; import { - MdErrorDirective, + MdError, MdHint, - MdInputContainer, - MdInputDirective, + MdFormField, + MdInput, MdPlaceholder, MdPrefix, MdSuffix @@ -23,10 +23,10 @@ import {PlatformModule} from '../core/platform/index'; @NgModule({ declarations: [ - MdErrorDirective, + MdError, MdHint, - MdInputContainer, - MdInputDirective, + MdFormField, + MdInput, MdPlaceholder, MdPrefix, MdSuffix, @@ -37,10 +37,10 @@ import {PlatformModule} from '../core/platform/index'; PlatformModule, ], exports: [ - MdErrorDirective, + MdError, MdHint, - MdInputContainer, - MdInputDirective, + MdFormField, + MdInput, MdPlaceholder, MdPrefix, MdSuffix, diff --git a/src/lib/input/input-container-errors.ts b/src/lib/input/input-container-errors.ts index a5de9f863a37..f7c9529194b7 100644 --- a/src/lib/input/input-container-errors.ts +++ b/src/lib/input/input-container-errors.ts @@ -7,22 +7,22 @@ */ /** @docs-private */ -export function getMdInputContainerPlaceholderConflictError(): Error { +export function getMdFormFieldPlaceholderConflictError(): 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.`); +export function getMdInputUnsupportedTypeError(type: string): Error { + return Error(`Input type "${type}" isn't supported by mdInput.`); } /** @docs-private */ -export function getMdInputContainerDuplicatedHintError(align: string): Error { +export function getMdFormFieldDuplicatedHintError(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?'); +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/input/input-container.html b/src/lib/input/input-container.html index 6fd6b64c4f98..8feede51f445 100644 --- a/src/lib/input/input-container.html +++ b/src/lib/input/input-container.html @@ -5,22 +5,22 @@
- +
@@ -31,7 +31,7 @@
+ [class.mat-disabled]="_control.isDisabled()"> diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index ec3e69467f9f..c37243e90a59 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -12,14 +12,14 @@ 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 {MdFormField, MdInput} from './input-container'; import {Platform} from '../core/platform/platform'; import {PlatformModule} from '../core/platform/index'; import {wrappedErrorMessage, dispatchFakeEvent} from '@angular/cdk/testing'; import { - getMdInputContainerDuplicatedHintError, - getMdInputContainerMissingMdInputError, - getMdInputContainerPlaceholderConflictError + getMdFormFieldDuplicatedHintError, + getMdFormFieldMissingControlError, + getMdFormFieldPlaceholderConflictError } from './input-container-errors'; import {MD_PLACEHOLDER_GLOBAL_OPTIONS} from '../core/placeholder/placeholder-options'; import {MD_ERROR_GLOBAL_OPTIONS, showOnDirtyErrorStateMatcher} from '../core/error/error-options'; @@ -73,8 +73,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.'); }); @@ -96,8 +96,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.'); }); @@ -236,40 +236,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', () => { @@ -580,8 +580,8 @@ describe('MdInputContainer without forms', function () { fixture.detectChanges(); - const inputContainer = fixture.debugElement.query(By.directive(MdInputContainer)); - const containerInstance = inputContainer.componentInstance as MdInputContainer; + const inputContainer = fixture.debugElement.query(By.directive(MdFormField)); + const containerInstance = inputContainer.componentInstance as MdFormField; const placeholder = inputContainer.nativeElement.querySelector('.mat-input-placeholder'); expect(containerInstance.floatPlaceholder).toBe('auto'); @@ -638,8 +638,8 @@ describe('MdInputContainer without forms', function () { 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 @@ -944,8 +944,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(); diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 99028b35889b..4935224ef419 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -34,10 +34,10 @@ import {coerceBooleanProperty, Platform} from '../core'; import {FormControl, FormGroupDirective, NgControl, NgForm} from '@angular/forms'; import {getSupportedInputTypes} from '../core/platform/features'; import { - getMdInputContainerDuplicatedHintError, - getMdInputContainerMissingMdInputError, - getMdInputContainerPlaceholderConflictError, - getMdInputContainerUnsupportedTypeError + getMdFormFieldDuplicatedHintError, + getMdFormFieldMissingControlError, + getMdFormFieldPlaceholderConflictError, + getMdInputUnsupportedTypeError } from './input-container-errors'; import { FloatPlaceholderType, @@ -54,7 +54,7 @@ import {Subject} from 'rxjs/Subject'; import {startWith} from '@angular/cdk/rxjs'; import {Observable} from 'rxjs/Observable'; -// Invalid input type. Using one of these will throw an MdInputContainerUnsupportedTypeError. +// Invalid input type. Using one of these will throw an MdInputUnsupportedTypeError. const MD_INPUT_INVALID_TYPES = [ 'button', 'checkbox', @@ -94,12 +94,12 @@ 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++}`; + /** Unique ID for the hint. Used for the aria-describedby on the form field control. */ + @Input() id: string = `md-hint-${nextUniqueId++}`; } -/** Single error message to be shown underneath the input. */ +/** Single error message to be shown underneath the form field. */ @Directive({ selector: 'md-error, mat-error', host: { @@ -108,34 +108,37 @@ export class MdHint { '[attr.id]': 'id', } }) -export class MdErrorDirective { - @Input() id: string = `md-input-error-${nextUniqueId++}`; +export class MdError { + @Input() id: string = `md-error-${nextUniqueId++}`; } -/** Prefix to be placed the the front of the input. */ +/** Prefix to be placed the the front of the form field. */ @Directive({ - selector: '[mdPrefix], [matPrefix]' + selector: '[mdPrefix], [matPrefix]', }) export class MdPrefix {} -/** Suffix to be placed at the end of the input. */ +/** Suffix to be placed at the end of the form field. */ @Directive({ - selector: '[mdSuffix], [matSuffix]' + selector: '[mdSuffix], [matSuffix]', }) export class MdSuffix {} -/** An interface which allows a control to work inside of a md-text-field. */ -export abstract class MdTextFieldControl { - /** Stream that emits whenever the state of the control changes. */ +/** An interface which allows a control to work inside of a md-form-field. */ +export abstract class MdFormFieldControl { + /** + * Stream that emits whenever the state of the control changes such that the md-form-field needs + * to be change detected. + */ stateChanges: Observable; /** Gets the element ID for this control. */ abstract getId(): string; - /** Fets the placeholder for this contorl. */ + /** Fets the placeholder for this control. */ abstract getPlaceholder(): string; /** Gets the NgControl for this control. */ @@ -164,7 +167,7 @@ export abstract class MdTextFieldControl { } -/** Marker for the input element that `MdInputContainer` is wrapping. */ +/** Directive that allows a native input to work inside a `MdFormField`. */ @Directive({ selector: `input[mdInput], textarea[mdInput], input[matInput], textarea[matInput]`, host: { @@ -181,9 +184,9 @@ export abstract class MdTextFieldControl { '(focus)': '_focusChanged(true)', '(input)': '_onInput()', }, - providers: [{provide: MdTextFieldControl, useExisting: MdInputDirective}], + providers: [{provide: MdFormFieldControl, useExisting: MdInput}], }) -export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestroy, DoCheck { +export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoCheck { /** Variables used as cache for getters and setters. */ private _type = 'text'; private _disabled = false; @@ -203,8 +206,8 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro _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. + * Stream that emits whenever the state of the input changes such that the wrapping `MdFormField` + * needs to run change detection. */ stateChanges = new Subject(); @@ -313,11 +316,6 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro } } - /** 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) { @@ -362,11 +360,11 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro /** Make sure the input is a supported type. */ private _validateType() { if (MD_INPUT_INVALID_TYPES.indexOf(this._type) > -1) { - throw getMdInputContainerUnsupportedTypeError(this._type); + throw getMdInputUnsupportedTypeError(this._type); } } - /** Checks whether the input type isn't one of the types that are never empty. */ + /** Checks whether the input type is one of the types that are never empty. */ private _isNeverEmpty() { return this._neverEmptyInputTypes.indexOf(this._type) > -1; } @@ -389,19 +387,19 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro return nodeName ? nodeName.toLowerCase() === 'textarea' : false; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. getId(): string { return this.id; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. getPlaceholder(): string { return this.placeholder; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. getNgControl(): NgControl | null { return this._ngControl; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. isFocused(): boolean { return this._focused; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. isEmpty(): boolean { return !this._isNeverEmpty() && (this.value == null || this.value === '') && @@ -411,17 +409,20 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro !this._isBadInput(); } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. isRequired(): boolean { return this.required; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. isDisabled(): boolean { return this.disabled; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. isErrorState(): boolean { return this._isErrorState; } - // Implemented as part of MdTextFieldControl. + // Implemented as part of MdFormFieldControl. setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); } + + // Implemented as part of MdFormFieldControl. + focus() { this._elementRef.nativeElement.focus(); } } @@ -430,7 +431,7 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro */ @Component({ moduleId: module.id, - selector: 'md-input-container, mat-input-container', + selector: 'md-input-container, mat-input-container, md-form-field, mat-form-field', templateUrl: 'input-container.html', styleUrls: ['input-container.css'], animations: [ @@ -438,16 +439,16 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro 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)') - ]) - ]) + 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]': '_textFieldControl.isErrorState()', - '[class.mat-focused]': '_textFieldControl.isFocused()', + '[class.mat-input-invalid]': '_control.isErrorState()', + '[class.mat-focused]': '_control.isFocused()', '[class.ng-untouched]': '_shouldForward("untouched")', '[class.ng-touched]': '_shouldForward("touched")', '[class.ng-pristine]': '_shouldForward("pristine")', @@ -455,16 +456,16 @@ export class MdInputDirective implements MdTextFieldControl, OnChanges, OnDestro '[class.ng-valid]': '_shouldForward("valid")', '[class.ng-invalid]': '_shouldForward("invalid")', '[class.ng-pending]': '_shouldForward("pending")', - '(click)': '_focusInput()', + '(click)': '_control.focus()', }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterContentChecked { +export class MdFormField implements AfterViewInit, AfterContentInit, AfterContentChecked { private _placeholderOptions: PlaceholderOptions; - /** Color of the input divider, based on the theme. */ + /** Color of the form field underline, based on the theme. */ @Input() color: 'primary' | 'accent' | 'warn' = 'primary'; /** @deprecated Use `color` instead. */ @@ -489,7 +490,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC /** State of the md-hint and md-error animations. */ _subscriptAnimationState: string = ''; - /** Text for the input hint. */ + /** Text for the form field hint. */ @Input() get hintLabel() { return this._hintLabel; } set hintLabel(value: string) { @@ -499,7 +500,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC private _hintLabel = ''; // Unique id for the hint label. - _hintLabelId: string = `md-input-hint-${nextUniqueId++}`; + _hintLabelId: string = `md-hint-${nextUniqueId++}`; /** Whether the placeholder should always float, never float or float as the user types. */ @Input() @@ -512,35 +513,34 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC } private _floatPlaceholder: FloatPlaceholderType; - /** Reference to the input's underline element. */ + /** Reference to the form field's underline element. */ @ViewChild('underline') underlineRef: ElementRef; @ViewChild('connectionContainer') _connectionContainerRef: ElementRef; - @ContentChild(MdTextFieldControl) _textFieldControl: MdTextFieldControl; + @ContentChild(MdFormFieldControl) _control: MdFormFieldControl; @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; - @ContentChildren(MdErrorDirective) _errorChildren: QueryList; + @ContentChildren(MdError) _errorChildren: QueryList; @ContentChildren(MdHint) _hintChildren: QueryList; @ContentChildren(MdPrefix) _prefixChildren: QueryList; @ContentChildren(MdSuffix) _suffixChildren: QueryList; constructor( - public _elementRef: ElementRef, - private _changeDetectorRef: ChangeDetectorRef, + 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(); + this._validateControlChild(); - // Subscribe to changes in the child input state in order to update the container UI. - startWith.call(this._textFieldControl.stateChanges, null).subscribe(() => { + // 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._syncAriaDescribedby(); + this._syncDescribedByIds(); this._changeDetectorRef.markForCheck(); }); - let ngControl = this._textFieldControl.getNgControl(); + let ngControl = this._control.getNgControl(); if (ngControl && ngControl.valueChanges) { ngControl.valueChanges.subscribe(() => { this._changeDetectorRef.markForCheck(); @@ -555,13 +555,13 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC // Update the aria-described by when the number of errors changes. startWith.call(this._errorChildren.changes, null).subscribe(() => { - this._syncAriaDescribedby(); + this._syncDescribedByIds(); this._changeDetectorRef.markForCheck(); }); } ngAfterContentChecked() { - this._validateInputChild(); + this._validateControlChild(); } ngAfterViewInit() { @@ -572,24 +572,19 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC /** Determines whether a class from the NgControl should be forwarded to the host element. */ _shouldForward(prop: string): boolean { - let ngControl = this._textFieldControl ? this._textFieldControl.getNgControl : null; + let ngControl = this._control ? this._control.getNgControl : null; return ngControl && (ngControl as any)[prop]; } - /** Whether the input has a placeholder. */ + /** Whether the form field has a placeholder. */ _hasPlaceholder() { - return !!(this._textFieldControl.getPlaceholder() || this._placeholderChild); - } - - /** Focuses the underlying input. */ - _focusInput() { - this._textFieldControl.focus(); + return !!(this._control.getPlaceholder() || this._placeholderChild); } /** Determines whether to display hints or errors. */ _getDisplayedMessages(): 'error' | 'hint' { return (this._errorChildren && this._errorChildren.length > 0 && - this._textFieldControl.isErrorState()) ? 'error' : 'hint'; + this._control.isErrorState()) ? 'error' : 'hint'; } /** @@ -597,8 +592,8 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC * `md-placeholder` attribute. */ private _validatePlaceholders() { - if (this._textFieldControl.getPlaceholder() && this._placeholderChild) { - throw getMdInputContainerPlaceholderConflictError(); + if (this._control.getPlaceholder() && this._placeholderChild) { + throw getMdFormFieldPlaceholderConflictError(); } } @@ -607,7 +602,7 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC */ private _processHints() { this._validateHints(); - this._syncAriaDescribedby(); + this._syncDescribedByIds(); } /** @@ -621,12 +616,12 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC this._hintChildren.forEach((hint: MdHint) => { if (hint.align == 'start') { if (startHint || this.hintLabel) { - throw getMdInputContainerDuplicatedHintError('start'); + throw getMdFormFieldDuplicatedHintError('start'); } startHint = hint; } else if (hint.align == 'end') { if (endHint) { - throw getMdInputContainerDuplicatedHintError('end'); + throw getMdFormFieldDuplicatedHintError('end'); } endHint = hint; } @@ -635,11 +630,11 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC } /** - * 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. + * Sets the list of element IDs that describe the child control. This allows the control to update + * its `aria-describedby` attribute accordingly. */ - private _syncAriaDescribedby() { - if (this._textFieldControl) { + private _syncDescribedByIds() { + if (this._control) { let ids: string[] = []; if (this._getDisplayedMessages() === 'hint') { @@ -661,16 +656,16 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit, AfterC ids = this._errorChildren.map(mdError => mdError.id); } - this._textFieldControl.setDescribedByIds(ids); + this._control.setDescribedByIds(ids); } } /** * Throws an error if the container's input child was removed. */ - protected _validateInputChild() { - if (!this._textFieldControl) { - throw getMdInputContainerMissingMdInputError(); + protected _validateControlChild() { + if (!this._control) { + throw getMdFormFieldMissingControlError(); } } } From bb2dca512a56fb6421676fb780ac6e87d264c61c Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Sat, 5 Aug 2017 22:53:03 -0700 Subject: [PATCH 3/8] alias mat-input-* classes with mat-form-field-* classes --- .../a11y/datepicker/datepicker-a11y.scss | 2 +- .../autocomplete/autocomplete-demo.scss | 2 +- src/demo-app/tabs/tabs-demo.scss | 2 +- src/lib/autocomplete/autocomplete.spec.ts | 6 +- src/lib/chips/chips.scss | 4 +- src/lib/core/compatibility/compatibility.ts | 2 + src/lib/datepicker/datepicker.spec.ts | 2 +- src/lib/input/_input-theme.scss | 121 +++++++++--------- src/lib/input/input-container.html | 30 +++-- src/lib/input/input-container.scss | 78 +++++------ src/lib/input/input-container.spec.ts | 102 ++++++++------- src/lib/input/input-container.ts | 43 +++---- .../table-filtering-example.css | 2 +- .../table-overview/table-overview-example.css | 2 +- 14 files changed, 199 insertions(+), 199 deletions(-) diff --git a/src/demo-app/a11y/datepicker/datepicker-a11y.scss b/src/demo-app/a11y/datepicker/datepicker-a11y.scss index 5c3e663a9d46..946c2ec3a9a6 100644 --- a/src/demo-app/a11y/datepicker/datepicker-a11y.scss +++ b/src/demo-app/a11y/datepicker/datepicker-a11y.scss @@ -1,3 +1,3 @@ -.mat-input-container { +.mat-form-field { width: 250px; } diff --git a/src/demo-app/autocomplete/autocomplete-demo.scss b/src/demo-app/autocomplete/autocomplete-demo.scss index 8b4132523acf..f2018cc19e80 100644 --- a/src/demo-app/autocomplete/autocomplete-demo.scss +++ b/src/demo-app/autocomplete/autocomplete-demo.scss @@ -7,7 +7,7 @@ margin: 24px; } - .mat-input-container { + .mat-form-field { margin-top: 16px; } } 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/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index d582cc519a03..7a82f18adc95 100644 --- a/src/lib/autocomplete/autocomplete.spec.ts +++ b/src/lib/autocomplete/autocomplete.spec.ts @@ -517,7 +517,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 +1319,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'); })); }); 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/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 8b7309003719..a19212cd6950 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -596,7 +596,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'); }); }); diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index e4b9ea76a6d8..498ed259f513 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -13,35 +13,33 @@ $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); + $form-field-placeholder-color: mat-color($foreground, secondary-text); + $form-field-floating-placeholder-color: mat-color($primary); + $form-field-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); + $form-field-underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + $form-field-underline-color-accent: mat-color($accent); + $form-field-underline-color-warn: mat-color($warn); + $form-field-underline-focused-color: mat-color($primary); - .mat-input-placeholder { - color: $input-placeholder-color; + .mat-form-field-placeholder { + color: $form-field-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-focused .mat-form-field-placeholder { + color: $form-field-floating-placeholder-color; &.mat-accent { - color: $input-underline-color-accent; + color: $form-field-underline-color-accent; } &.mat-warn { - color: $input-underline-color-warn; + color: $form-field-underline-color-warn; } } @@ -49,60 +47,60 @@ 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; + // See _mat-form-field-placeholder-floating mixin in form-field.scss + input.mat-input-element:-webkit-autofill + .mat-form-field-placeholder, + .mat-focused .mat-form-field-placeholder.mat-form-field-float { + .mat-form-field-required-marker { + color: $form-field-required-placeholder-color; } } - .mat-input-underline { - background-color: $input-underline-color; + .mat-form-field-underline { + background-color: $form-field-underline-color; &.mat-disabled { - @include mat-control-disabled-underline($input-underline-color); + @include mat-control-disabled-underline($form-field-underline-color); } } - .mat-input-ripple { - background-color: $input-underline-focused-color; + .mat-form-field-ripple { + background-color: $form-field-underline-focused-color; &.mat-accent { - background-color: $input-underline-color-accent; + background-color: $form-field-underline-color-accent; } &.mat-warn { - background-color: $input-underline-color-warn; + background-color: $form-field-underline-color-warn; } } - // Styling for the error state of the input container. Note that while the same can be + // 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-input-invalid { - .mat-input-placeholder { - color: $input-underline-color-warn; + .mat-form-field-invalid { + .mat-form-field-placeholder { + color: $form-field-underline-color-warn; &.mat-accent, - &.mat-float .mat-placeholder-required { - color: $input-underline-color-warn; + &.mat-form-field-float .mat-form-field-required-marker { + color: $form-field-underline-color-warn; } } - .mat-input-ripple { - background-color: $input-underline-color-warn; + .mat-form-field-ripple { + background-color: $form-field-underline-color-warn; } } - .mat-input-error { - color: $input-underline-color-warn; + .mat-error { + color: $form-field-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: +// 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. @@ -132,36 +130,36 @@ // 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. + // 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 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. + // 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 input-wrapper to reserve space for the subscript, since it's + // 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-input-container { + .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-input-wrapper { + .mat-form-field-wrapper { padding-bottom: $wrapper-padding-bottom; } - .mat-input-prefix, - .mat-input-suffix { + .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; @@ -180,15 +178,15 @@ } } - .mat-input-infix { + .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-input-element { - &:-webkit-autofill + .mat-input-placeholder-wrapper .mat-float { - @include _mat-input-placeholder-floating($subscript-font-scale, + &:-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); } } @@ -199,28 +197,29 @@ margin-top: -$line-spacing * 1em; } - .mat-input-placeholder-wrapper { + .mat-form-field-placeholder-wrapper { top: -$infix-margin-top; padding-top: $infix-margin-top; } - .mat-input-placeholder { + .mat-form-field-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, + // 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-input-underline { + .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-input-subscript-wrapper { + .mat-form-field-subscript-wrapper { font-size: $subscript-font-size; margin-top: $subscript-margin-top; diff --git a/src/lib/input/input-container.html b/src/lib/input/input-container.html index 8feede51f445..be2ea21d007d 100644 --- a/src/lib/input/input-container.html +++ b/src/lib/input/input-container.html @@ -1,52 +1,56 @@ -
-
-
+
+
+
-
+
- -
-
+
-
-
-
+
-
+
{{hintLabel}}
-
+
diff --git a/src/lib/input/input-container.scss b/src/lib/input/input-container.scss index 0154daac6635..26079b104163 100644 --- a/src/lib/input/input-container.scss +++ b/src/lib/input/input-container.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,7 +56,7 @@ $mat-input-underline-height: 1px !default; } } -.mat-input-infix { +.mat-form-field-infix { display: block; position: relative; flex: auto; @@ -80,7 +80,7 @@ $mat-input-underline-height: 1px !default; margin: 0; width: 100%; - // Prevent textareas from being resized outside the container. + // Prevent textareas from being resized outside the form field. max-width: 100%; resize: vertical; @@ -96,17 +96,17 @@ $mat-input-underline-height: 1px !default; // 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 + // 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. - &:-webkit-autofill + .mat-input-placeholder-wrapper { + &:-webkit-autofill + .mat-form-field-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 { + .mat-form-field-placeholder { display: none; } - .mat-float { + .mat-form-field-float { display: block; transition: none; } @@ -124,7 +124,7 @@ $mat-input-underline-height: 1px !default; // 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; @@ -141,9 +141,9 @@ textarea.mat-input-element { // 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; @@ -166,12 +166,12 @@ textarea.mat-input-element { 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 +182,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 +200,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 +212,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 +226,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 +244,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/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index c37243e90a59..b14eae09a468 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -110,7 +110,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 +123,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 +133,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 +142,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 +151,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 +161,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 +169,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 +179,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 +188,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', () => { @@ -381,7 +382,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 +405,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')) @@ -511,14 +513,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 +528,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 +539,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 +550,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 +564,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 +573,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', () => { @@ -582,24 +584,25 @@ describe('MdInputContainer without forms', function () { const inputContainer = fixture.debugElement.query(By.directive(MdFormField)); const containerInstance = inputContainer.componentInstance as MdFormField; - const placeholder = inputContainer.nativeElement.querySelector('.mat-input-placeholder'); + 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 +612,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,15 +626,15 @@ 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', () => { @@ -704,7 +707,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')) @@ -723,7 +726,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')) @@ -756,7 +759,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')) @@ -770,7 +773,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) @@ -780,7 +783,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.'); @@ -823,7 +826,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'); @@ -958,7 +961,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) @@ -982,7 +986,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-container.ts b/src/lib/input/input-container.ts index 4935224ef419..28e7e268929c 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -81,7 +81,7 @@ let nextUniqueId = 0; export class MdPlaceholder {} -/** Hint text to be shown underneath the input. */ +/** Hint text to be shown underneath the form field control. */ @Directive({ selector: 'md-hint, mat-hint', host: { @@ -103,7 +103,7 @@ export class MdHint { @Directive({ selector: 'md-error, mat-error', host: { - 'class': 'mat-input-error', + 'class': 'mat-error', 'role': 'alert', '[attr.id]': 'id', } @@ -127,18 +127,18 @@ export class MdPrefix {} export class MdSuffix {} -/** An interface which allows a control to work inside of a md-form-field. */ +/** An interface which allows a control to work inside of a `MdFormField`. */ export abstract class MdFormFieldControl { /** - * Stream that emits whenever the state of the control changes such that the md-form-field needs - * to be change detected. + * Stream that emits whenever the state of the control changes such that the parent `MdFormField` + * needs to run change detection. */ stateChanges: Observable; /** Gets the element ID for this control. */ abstract getId(): string; - /** Fets the placeholder for this control. */ + /** Gets the placeholder for this control. */ abstract getPlaceholder(): string; /** Gets the NgControl for this control. */ @@ -196,13 +196,9 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoChec private _errorOptions: ErrorOptions; private _previousNativeValue = this.value; private _focused = false; + private _isErrorState = false; - /** Whether the input is in an error state. */ - _isErrorState = false; - - /** Whether the element is focused or not. */ - - /** Sets the aria-describedby attribute on the input for improved a11y. */ + /** The aria-describedby attribute on the input for improved a11y. */ _ariaDescribedby: string; /** @@ -337,9 +333,9 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoChec /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { const oldState = this._isErrorState; - const control = this._ngControl; + const ngControl = this._ngControl; const parent = this._parentFormGroup || this._parentForm; - const newState = control && this.errorStateMatcher(control.control as FormControl, parent); + const newState = ngControl && this.errorStateMatcher(ngControl.control as FormControl, parent); if (newState !== oldState) { this._isErrorState = newState; @@ -426,9 +422,7 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoChec } -/** - * Container for text inputs that applies Material Design styling and behavior. - */ +/** Container for form controls that applies Material Design styling and behavior. */ @Component({ moduleId: module.id, selector: 'md-input-container, mat-input-container, md-form-field, mat-form-field', @@ -446,8 +440,9 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoChec host: { // Remove align attribute to prevent it from interfering with layout. '[attr.align]': 'null', - 'class': 'mat-input-container', + 'class': 'mat-input-container mat-form-field', '[class.mat-input-invalid]': '_control.isErrorState()', + '[class.mat-form-field-invalid]': '_control.isErrorState()', '[class.mat-focused]': '_control.isFocused()', '[class.ng-untouched]': '_shouldForward("untouched")', '[class.ng-touched]': '_shouldForward("touched")', @@ -588,8 +583,8 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten } /** - * Ensure that there is only one placeholder (either `input` attribute or child element with the - * `md-placeholder` attribute. + * 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.getPlaceholder() && this._placeholderChild) { @@ -597,9 +592,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten } } - /** - * Does any extra processing that is required when handling the hints. - */ + /** Does any extra processing that is required when handling the hints. */ private _processHints() { this._validateHints(); this._syncDescribedByIds(); @@ -660,9 +653,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten } } - /** - * Throws an error if the container's input child was removed. - */ + /** Throws an error if the form field's control is missing. */ protected _validateControlChild() { if (!this._control) { throw getMdFormFieldMissingControlError(); 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; From dfddd97c54df4c4037bc3d27a44513114162e6bd Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 7 Aug 2017 10:32:46 -0700 Subject: [PATCH 4/8] separate md-form-field into separate directory --- src/demo-app/demo-material-module.ts | 2 + src/e2e-app/e2e-app-module.ts | 23 +- src/lib/autocomplete/autocomplete-trigger.ts | 3 +- src/lib/autocomplete/autocomplete.spec.ts | 3 +- src/lib/chips/chip-list.spec.ts | 3 +- src/lib/core/theming/_all-theme.scss | 2 + src/lib/core/typography/_all-typography.scss | 2 + src/lib/datepicker/datepicker-input.ts | 2 +- src/lib/datepicker/datepicker.spec.ts | 11 +- src/lib/form-field/_form-field-theme.scss | 217 ++++++++++++ .../form-field-errors.ts} | 7 +- .../form-field.html} | 0 .../form-field.scss} | 76 +---- .../form-field.ts} | 308 +----------------- src/lib/form-field/index.ts | 49 +++ src/lib/input/_input-theme.scss | 201 ------------ src/lib/input/index.ts | 32 +- src/lib/input/input-errors.ts | 12 + src/lib/input/input.scss | 49 +++ ...{input-container.spec.ts => input.spec.ts} | 13 +- src/lib/input/input.ts | 303 +++++++++++++++++ src/lib/module.ts | 2 + src/lib/public_api.ts | 1 + src/material-examples/material-module.ts | 3 +- .../kitchen-sink/kitchen-sink.ts | 2 + 25 files changed, 727 insertions(+), 599 deletions(-) create mode 100644 src/lib/form-field/_form-field-theme.scss rename src/lib/{input/input-container-errors.ts => form-field/form-field-errors.ts} (73%) rename src/lib/{input/input-container.html => form-field/form-field.html} (100%) rename src/lib/{input/input-container.scss => form-field/form-field.scss} (68%) rename src/lib/{input/input-container.ts => form-field/form-field.ts} (50%) create mode 100644 src/lib/form-field/index.ts create mode 100644 src/lib/input/input-errors.ts create mode 100644 src/lib/input/input.scss rename src/lib/input/{input-container.spec.ts => input.spec.ts} (99%) create mode 100644 src/lib/input/input.ts diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts index 15f17c50c210..54eeb484f9ec 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/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 5d94b20df688..b29972a920e5 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -40,7 +40,8 @@ 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 {MdFormField, MdInput} from '../input/input-container'; +import {MdFormField} from '../form-field/index'; +import {MdInput} from '../input/input'; import {Subscription} from 'rxjs/Subscription'; import {merge} from 'rxjs/observable/merge'; import {fromEvent} from 'rxjs/observable/fromEvent'; diff --git a/src/lib/autocomplete/autocomplete.spec.ts b/src/lib/autocomplete/autocomplete.spec.ts index 7a82f18adc95..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 {MdFormField} 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, 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/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 0b7eaff6da01..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 {MdFormField} 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'; diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index a19212cd6950..638253b99190 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, @@ -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..16a79aa761c3 --- /dev/null +++ b/src/lib/form-field/_form-field-theme.scss @@ -0,0 +1,217 @@ +@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. + $form-field-placeholder-color: mat-color($foreground, secondary-text); + $form-field-floating-placeholder-color: mat-color($primary); + $form-field-required-placeholder-color: mat-color($accent); + + // Underline colors. + $form-field-underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); + $form-field-underline-color-accent: mat-color($accent); + $form-field-underline-color-warn: mat-color($warn); + $form-field-underline-focused-color: mat-color($primary); + + .mat-form-field-placeholder { + color: $form-field-placeholder-color; + } + + .mat-hint { + color: mat-color($foreground, secondary-text); + } + + .mat-focused .mat-form-field-placeholder { + color: $form-field-floating-placeholder-color; + + &.mat-accent { + color: $form-field-underline-color-accent; + } + + &.mat-warn { + color: $form-field-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: $form-field-required-placeholder-color; + } + } + + .mat-form-field-underline { + background-color: $form-field-underline-color; + + &.mat-disabled { + @include mat-control-disabled-underline($form-field-underline-color); + } + } + + .mat-form-field-ripple { + background-color: $form-field-underline-focused-color; + + &.mat-accent { + background-color: $form-field-underline-color-accent; + } + + &.mat-warn { + background-color: $form-field-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: $form-field-underline-color-warn; + + &.mat-accent, + &.mat-form-field-float .mat-form-field-required-marker { + color: $form-field-underline-color-warn; + } + } + + .mat-form-field-ripple { + background-color: $form-field-underline-color-warn; + } + } + + .mat-error { + color: $form-field-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/input/input-container-errors.ts b/src/lib/form-field/form-field-errors.ts similarity index 73% rename from src/lib/input/input-container-errors.ts rename to src/lib/form-field/form-field-errors.ts index f7c9529194b7..008393915e4c 100644 --- a/src/lib/input/input-container-errors.ts +++ b/src/lib/form-field/form-field-errors.ts @@ -11,11 +11,6 @@ export function getMdFormFieldPlaceholderConflictError(): Error { return Error('Placeholder attribute and child element were both specified.'); } -/** @docs-private */ -export function getMdInputUnsupportedTypeError(type: string): Error { - return Error(`Input type "${type}" isn't supported by mdInput.`); -} - /** @docs-private */ export function getMdFormFieldDuplicatedHintError(align: string): Error { return Error(`A hint was already declared for 'align="${align}"'.`); @@ -24,5 +19,5 @@ export function getMdFormFieldDuplicatedHintError(align: string): Error { /** @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?'); + '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/form-field/form-field.html similarity index 100% rename from src/lib/input/input-container.html rename to src/lib/form-field/form-field.html diff --git a/src/lib/input/input-container.scss b/src/lib/form-field/form-field.scss similarity index 68% rename from src/lib/input/input-container.scss rename to src/lib/form-field/form-field.scss index 26079b104163..d4e76aedded5 100644 --- a/src/lib/input/input-container.scss +++ b/src/lib/form-field/form-field.scss @@ -62,63 +62,20 @@ $mat-form-field-underline-height: 1px !default; 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 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; +// 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-form-field - // classes take over to fulfill this behaviour. - // Assumes the autofill is non-empty. - &:-webkit-autofill + .mat-form-field-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-form-field-placeholder { - display: none; - } - - .mat-form-field-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; } } @@ -134,11 +91,6 @@ $mat-form-field-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` property. @@ -163,8 +115,8 @@ 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 control is empty. display: none; diff --git a/src/lib/input/input-container.ts b/src/lib/form-field/form-field.ts similarity index 50% rename from src/lib/input/input-container.ts rename to src/lib/form-field/form-field.ts index 28e7e268929c..54fc87a2a537 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/form-field/form-field.ts @@ -16,58 +16,30 @@ import { 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 {coerceBooleanProperty} from '../core'; +import {NgControl} from '@angular/forms'; import { getMdFormFieldDuplicatedHintError, getMdFormFieldMissingControlError, getMdFormFieldPlaceholderConflictError, - getMdInputUnsupportedTypeError -} from './input-container-errors'; +} from './form-field-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'; import {Observable} from 'rxjs/Observable'; -// 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; @@ -167,267 +139,15 @@ export abstract class MdFormFieldControl { } -/** 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]': '_isErrorState', - '(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; - private _focused = false; - private _isErrorState = 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._isErrorState; - const ngControl = this._ngControl; - const parent = this._parentFormGroup || this._parentForm; - const newState = ngControl && this.errorStateMatcher(ngControl.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 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. - getId(): string { return this.id; } - - // Implemented as part of MdFormFieldControl. - getPlaceholder(): string { return this.placeholder; } - - // Implemented as part of MdFormFieldControl. - getNgControl(): NgControl | null { return this._ngControl; } - - // Implemented as part of MdFormFieldControl. - isFocused(): boolean { return this._focused; } - - // Implemented as part of MdFormFieldControl. - isEmpty(): 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. - isRequired(): boolean { return this.required; } - - // Implemented as part of MdFormFieldControl. - isDisabled(): boolean { return this.disabled; } - - // Implemented as part of MdFormFieldControl. - isErrorState(): boolean { return this._isErrorState; } - - // Implemented as part of MdFormFieldControl. - setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); } - - // Implemented as part of MdFormFieldControl. - focus() { this._elementRef.nativeElement.focus(); } -} - - /** Container for form controls that applies Material Design styling and behavior. */ @Component({ moduleId: module.id, selector: 'md-input-container, mat-input-container, md-form-field, mat-form-field', - templateUrl: 'input-container.html', - styleUrls: ['input-container.css'], + 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: [ trigger('transitionMessages', [ state('enter', style({ opacity: 1, transform: 'translateY(0%)' })), @@ -519,11 +239,11 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten @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'; - } + 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(); @@ -579,7 +299,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten /** Determines whether to display hints or errors. */ _getDisplayedMessages(): 'error' | 'hint' { return (this._errorChildren && this._errorChildren.length > 0 && - this._control.isErrorState()) ? 'error' : 'hint'; + this._control.isErrorState()) ? 'error' : 'hint'; } /** diff --git a/src/lib/form-field/index.ts b/src/lib/form-field/index.ts new file mode 100644 index 000000000000..04cbc1533f63 --- /dev/null +++ b/src/lib/form-field/index.ts @@ -0,0 +1,49 @@ +/** + * @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, + MdHint, + MdFormField, + MdPlaceholder, + MdPrefix, + MdSuffix +} from './form-field'; +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 './form-field'; +export * from './form-field-errors'; + diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index 498ed259f513..9cd15260e3c2 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -5,226 +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. - $form-field-placeholder-color: mat-color($foreground, secondary-text); - $form-field-floating-placeholder-color: mat-color($primary); - $form-field-required-placeholder-color: mat-color($accent); - - // Underline colors. - $form-field-underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); - $form-field-underline-color-accent: mat-color($accent); - $form-field-underline-color-warn: mat-color($warn); - $form-field-underline-focused-color: mat-color($primary); - - .mat-form-field-placeholder { - color: $form-field-placeholder-color; - } - - .mat-hint { - color: mat-color($foreground, secondary-text); - } - - .mat-focused .mat-form-field-placeholder { - color: $form-field-floating-placeholder-color; - - &.mat-accent { - color: $form-field-underline-color-accent; - } - - &.mat-warn { - color: $form-field-underline-color-warn; - } - } - .mat-input-element:disabled { color: mat-color($foreground, secondary-text, if($is-dark-theme, 0.7, 0.42)); } - - // See _mat-form-field-placeholder-floating mixin in form-field.scss - input.mat-input-element:-webkit-autofill + .mat-form-field-placeholder, - .mat-focused .mat-form-field-placeholder.mat-form-field-float { - .mat-form-field-required-marker { - color: $form-field-required-placeholder-color; - } - } - - .mat-form-field-underline { - background-color: $form-field-underline-color; - - &.mat-disabled { - @include mat-control-disabled-underline($form-field-underline-color); - } - } - - .mat-form-field-ripple { - background-color: $form-field-underline-focused-color; - - &.mat-accent { - background-color: $form-field-underline-color-accent; - } - - &.mat-warn { - background-color: $form-field-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: $form-field-underline-color-warn; - - &.mat-accent, - &.mat-form-field-float .mat-form-field-required-marker { - color: $form-field-underline-color-warn; - } - } - - .mat-form-field-ripple { - background-color: $form-field-underline-color-warn; - } - } - - .mat-error { - color: $form-field-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-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 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-input-element { - &:-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); - } - } // 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-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/input/index.ts b/src/lib/input/index.ts index 47aaea5706d4..55caf81f7187 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -7,43 +7,29 @@ */ import {NgModule} from '@angular/core'; -import { - MdError, - MdHint, - MdFormField, - MdInput, - 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: [ - MdError, - MdHint, - MdFormField, MdInput, - MdPlaceholder, - MdPrefix, - MdSuffix, MdTextareaAutosize, ], imports: [ CommonModule, + MdFormFieldModule, PlatformModule, ], exports: [ - MdError, - MdHint, - MdFormField, + // TODO(mmalerba): We import and re-export the form field module since all existing users of + // `MdInput` will need this to continue using `md-input-container`. We may want to keep this + // long term since the `MdInput` directive will almost always be used with `md-form-field`. + MdFormFieldModule, MdInput, - MdPlaceholder, - MdPrefix, - MdSuffix, MdTextareaAutosize, ], }) @@ -51,6 +37,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-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 99% rename from src/lib/input/input-container.spec.ts rename to src/lib/input/input.spec.ts index b14eae09a468..7db414504ceb 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 {MdFormField, MdInput} 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 { + MdFormField, + MdFormFieldModule, getMdFormFieldDuplicatedHintError, getMdFormFieldMissingControlError, - getMdFormFieldPlaceholderConflictError -} from './input-container-errors'; + 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, @@ -84,6 +87,7 @@ describe('MdInputContainer without forms', function () { TestBed.configureTestingModule({ imports: [ FormsModule, + MdFormFieldModule, MdInputModule, NoopAnimationsModule ], @@ -660,6 +664,7 @@ describe('MdInputContainer with forms', () => { TestBed.configureTestingModule({ imports: [ FormsModule, + MdFormFieldModule, MdInputModule, NoopAnimationsModule, PlatformModule, @@ -874,6 +879,7 @@ describe('MdInputContainer with forms', () => { TestBed.configureTestingModule({ imports: [ FormsModule, + MdFormFieldModule, MdInputModule, NoopAnimationsModule, ReactiveFormsModule, @@ -905,6 +911,7 @@ describe('MdInputContainer with forms', () => { TestBed.configureTestingModule({ imports: [ FormsModule, + MdFormFieldModule, MdInputModule, NoopAnimationsModule, ReactiveFormsModule, diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts new file mode 100644 index 000000000000..e2732104e619 --- /dev/null +++ b/src/lib/input/input.ts @@ -0,0 +1,303 @@ +/** + * @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/form-field'; + +// 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]': '_isErrorState', + '(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; + private _focused = false; + private _isErrorState = 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._isErrorState; + const ngControl = this._ngControl; + const parent = this._parentFormGroup || this._parentForm; + const newState = ngControl && this.errorStateMatcher(ngControl.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 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. + getId(): string { return this.id; } + + // Implemented as part of MdFormFieldControl. + getPlaceholder(): string { return this.placeholder; } + + // Implemented as part of MdFormFieldControl. + getNgControl(): NgControl | null { return this._ngControl; } + + // Implemented as part of MdFormFieldControl. + isFocused(): boolean { return this._focused; } + + // Implemented as part of MdFormFieldControl. + isEmpty(): 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. + isRequired(): boolean { return this.required; } + + // Implemented as part of MdFormFieldControl. + isDisabled(): boolean { return this.disabled; } + + // Implemented as part of MdFormFieldControl. + isErrorState(): boolean { return this._isErrorState; } + + // 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/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, From b16ef28da949dbeee5c56967362cf6d8ae6a6090 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 7 Aug 2017 14:44:10 -0700 Subject: [PATCH 5/8] rebase and fix lint --- src/demo-app/demo-material-module.ts | 2 +- src/lib/datepicker/datepicker.spec.ts | 2 +- src/lib/form-field/_form-field-theme.scss | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/demo-app/demo-material-module.ts b/src/demo-app/demo-material-module.ts index 54eeb484f9ec..c05b4c6f9bd7 100644 --- a/src/demo-app/demo-material-module.ts +++ b/src/demo-app/demo-material-module.ts @@ -51,7 +51,7 @@ import {CdkTableModule} from '@angular/cdk/table'; MdDatepickerModule, MdDialogModule, MdExpansionModule, - //MdFormFieldModule, + MdFormFieldModule, MdGridListModule, MdIconModule, MdInputModule, diff --git a/src/lib/datepicker/datepicker.spec.ts b/src/lib/datepicker/datepicker.spec.ts index 638253b99190..1b2b44678c57 100644 --- a/src/lib/datepicker/datepicker.spec.ts +++ b/src/lib/datepicker/datepicker.spec.ts @@ -16,7 +16,7 @@ import { createKeyboardEvent, dispatchEvent, } from '@angular/cdk/testing'; -import {MdFormFieldModule} from "../form-field/index"; +import {MdFormFieldModule} from '../form-field/index'; describe('MdDatepicker', () => { afterEach(inject([OverlayContainer], (container: OverlayContainer) => { diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 16a79aa761c3..5a1277e71967 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -178,10 +178,11 @@ 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-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 { From 9baaaeb0a173f3a9e1ed8399ee8c694bfe5af400 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 7 Aug 2017 23:07:39 -0700 Subject: [PATCH 6/8] address comments --- src/lib/autocomplete/autocomplete-trigger.ts | 24 ++-- src/lib/form-field/error.ts | 26 +++++ src/lib/form-field/form-field-control.ts | 53 +++++++++ src/lib/form-field/form-field.ts | 109 ++----------------- src/lib/form-field/hint.ts | 32 ++++++ src/lib/form-field/index.ts | 20 ++-- src/lib/form-field/placeholder.ts | 16 +++ src/lib/form-field/prefix.ts | 16 +++ src/lib/form-field/suffix.ts | 16 +++ src/lib/input/index.ts | 5 +- src/lib/input/input.spec.ts | 4 +- src/lib/input/input.ts | 2 +- src/lib/public_api.ts | 3 + 13 files changed, 198 insertions(+), 128 deletions(-) create mode 100644 src/lib/form-field/error.ts create mode 100644 src/lib/form-field/form-field-control.ts create mode 100644 src/lib/form-field/hint.ts create mode 100644 src/lib/form-field/placeholder.ts create mode 100644 src/lib/form-field/prefix.ts create mode 100644 src/lib/form-field/suffix.ts diff --git a/src/lib/autocomplete/autocomplete-trigger.ts b/src/lib/autocomplete/autocomplete-trigger.ts index b29972a920e5..234927443da4 100644 --- a/src/lib/autocomplete/autocomplete-trigger.ts +++ b/src/lib/autocomplete/autocomplete-trigger.ts @@ -41,7 +41,6 @@ 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 {MdFormField} from '../form-field/index'; -import {MdInput} from '../input/input'; import {Subscription} from 'rxjs/Subscription'; import {merge} from 'rxjs/observable/merge'; import {fromEvent} from 'rxjs/observable/fromEvent'; @@ -154,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: MdFormField, + @Optional() @Host() private _formField: MdFormField, @Optional() @Inject(DOCUMENT) private _document: any) {} ngOnDestroy() { @@ -247,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 && @@ -330,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; } } @@ -339,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; } } @@ -409,11 +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._control instanceof MdInput) { - this._inputContainer._control.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; } @@ -471,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/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..5af51b1f31ac --- /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 { + /** + * Stream that emits whenever the state of the control changes such that the parent `MdFormField` + * needs to run change detection. + */ + stateChanges: Observable; + + /** The value of the control. */ + value: any; + + /** Gets the element ID for this control. */ + abstract getId(): string; + + /** Gets the placeholder for this control. */ + abstract getPlaceholder(): string; + + /** Gets the NgControl for this control. */ + abstract getNgControl(): NgControl | null; + + /** Whether the control is focused. */ + abstract isFocused(): boolean; + + /** Whether the control is empty. */ + abstract isEmpty(): boolean; + + /** Whether the control is required. */ + abstract isRequired(): boolean; + + /** Whether the control is disabled. */ + abstract isDisabled(): boolean; + + /** Whether the control is in an error state. */ + abstract isErrorState(): 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.ts b/src/lib/form-field/form-field.ts index 54fc87a2a537..8cf704c675d0 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -15,7 +15,6 @@ import { Component, ContentChild, ContentChildren, - Directive, ElementRef, Inject, Input, @@ -26,7 +25,6 @@ import { } from '@angular/core'; import {animate, state, style, transition, trigger} from '@angular/animations'; import {coerceBooleanProperty} from '../core'; -import {NgControl} from '@angular/forms'; import { getMdFormFieldDuplicatedHintError, getMdFormFieldMissingControlError, @@ -38,110 +36,21 @@ import { PlaceholderOptions } from '../core/placeholder/placeholder-options'; import {startWith} from '@angular/cdk/rxjs'; -import {Observable} from 'rxjs/Observable'; - -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 form field control. */ -@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 form field control. */ - @Input() id: string = `md-hint-${nextUniqueId++}`; -} - - -/** 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 = `md-error-${nextUniqueId++}`; -} +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'; -/** Prefix to be placed the the front of the form field. */ -@Directive({ - selector: '[mdPrefix], [matPrefix]', -}) -export class MdPrefix {} - - -/** Suffix to be placed at the end of the form field. */ -@Directive({ - selector: '[mdSuffix], [matSuffix]', -}) -export class MdSuffix {} - - -/** An interface which allows a control to work inside of a `MdFormField`. */ -export abstract class MdFormFieldControl { - /** - * Stream that emits whenever the state of the control changes such that the parent `MdFormField` - * needs to run change detection. - */ - stateChanges: Observable; - - /** Gets the element ID for this control. */ - abstract getId(): string; - - /** Gets the placeholder for this control. */ - abstract getPlaceholder(): string; - - /** Gets the NgControl for this control. */ - abstract getNgControl(): NgControl | null; - - /** Whether the control is focused. */ - abstract isFocused(): boolean; - - /** Whether the control is empty. */ - abstract isEmpty(): boolean; - - /** Whether the control is required. */ - abstract isRequired(): boolean; - - /** Whether the control is disabled. */ - abstract isDisabled(): boolean; - - /** Whether the control is in an error state. */ - abstract isErrorState(): boolean; - - /** Sets the list of element IDs that currently describe this control. */ - abstract setDescribedByIds(ids: string[]): void; - - /** Focuses this control. */ - abstract focus(): void; -} +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. @@ -158,8 +67,6 @@ export abstract class MdFormFieldControl { ]), ], host: { - // Remove align attribute to prevent it from interfering with layout. - '[attr.align]': 'null', 'class': 'mat-input-container mat-form-field', '[class.mat-input-invalid]': '_control.isErrorState()', '[class.mat-form-field-invalid]': '_control.isErrorState()', 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 index 04cbc1533f63..71d8b6164677 100644 --- a/src/lib/form-field/index.ts +++ b/src/lib/form-field/index.ts @@ -7,14 +7,12 @@ */ import {NgModule} from '@angular/core'; -import { - MdError, - MdHint, - MdFormField, - MdPlaceholder, - MdPrefix, - MdSuffix -} from './form-field'; +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'; @@ -44,6 +42,12 @@ import {PlatformModule} from '../core/platform/index'; 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/index.ts b/src/lib/input/index.ts index 55caf81f7187..495d4e8c37f7 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -25,9 +25,8 @@ import {MdFormFieldModule} from '../form-field/index'; PlatformModule, ], exports: [ - // TODO(mmalerba): We import and re-export the form field module since all existing users of - // `MdInput` will need this to continue using `md-input-container`. We may want to keep this - // long term since the `MdInput` directive will almost always be used with `md-form-field`. + // We re-export the `MdFormFieldModule` since `MdInput` will almost always be used together with + // `MdFormField`. MdFormFieldModule, MdInput, MdTextareaAutosize, diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 7db414504ceb..83ba4e6b7666 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -502,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'); diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index e2732104e619..2692e14f104d 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -29,7 +29,7 @@ import { MD_ERROR_GLOBAL_OPTIONS } from '../core/error/error-options'; import {Subject} from 'rxjs/Subject'; -import {MdFormFieldControl} from '../form-field/form-field'; +import {MdFormFieldControl} from '../form-field/index'; // Invalid input type. Using one of these will throw an MdInputUnsupportedTypeError. const MD_INPUT_INVALID_TYPES = [ diff --git a/src/lib/public_api.ts b/src/lib/public_api.ts index 7c4bdffb425a..76c73f7f60a0 100644 --- a/src/lib/public_api.ts +++ b/src/lib/public_api.ts @@ -46,3 +46,6 @@ export * from './tabs/index'; export * from './tabs/tab-nav-bar/index'; export * from './toolbar/index'; export * from './tooltip/index'; + +// TODO(mmalerba): Temporary alias to avoid breakages, cleanup later. +export {MdFormField as MdInputContainer} from './form-field/index'; From a1d5ce9661602529cf132c4e893f06153039c5d5 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 8 Aug 2017 16:41:14 -0700 Subject: [PATCH 7/8] addressed comments --- src/lib/form-field/_form-field-theme.scss | 42 +++++++++++------------ src/lib/form-field/form-field-control.ts | 4 +-- src/lib/form-field/form-field.ts | 3 +- src/lib/input/input.ts | 2 +- src/lib/public_api.ts | 3 -- 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 5a1277e71967..8f475fd4f96f 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -13,18 +13,18 @@ $is-dark-theme: map-get($theme, is-dark); // Placeholder colors. Required is used for the `*` star shown in the placeholder. - $form-field-placeholder-color: mat-color($foreground, secondary-text); - $form-field-floating-placeholder-color: mat-color($primary); - $form-field-required-placeholder-color: mat-color($accent); + $placeholder-color: mat-color($foreground, secondary-text); + $floating-placeholder-color: mat-color($primary); + $required-placeholder-color: mat-color($accent); // Underline colors. - $form-field-underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); - $form-field-underline-color-accent: mat-color($accent); - $form-field-underline-color-warn: mat-color($warn); - $form-field-underline-focused-color: mat-color($primary); + $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: $form-field-placeholder-color; + color: $placeholder-color; } .mat-hint { @@ -32,41 +32,41 @@ } .mat-focused .mat-form-field-placeholder { - color: $form-field-floating-placeholder-color; + color: $floating-placeholder-color; &.mat-accent { - color: $form-field-underline-color-accent; + color: $underline-color-accent; } &.mat-warn { - color: $form-field-underline-color-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: $form-field-required-placeholder-color; + color: $required-placeholder-color; } } .mat-form-field-underline { - background-color: $form-field-underline-color; + background-color: $underline-color; &.mat-disabled { - @include mat-control-disabled-underline($form-field-underline-color); + @include mat-control-disabled-underline($underline-color); } } .mat-form-field-ripple { - background-color: $form-field-underline-focused-color; + background-color: $underline-focused-color; &.mat-accent { - background-color: $form-field-underline-color-accent; + background-color: $underline-color-accent; } &.mat-warn { - background-color: $form-field-underline-color-warn; + background-color: $underline-color-warn; } } @@ -75,21 +75,21 @@ // logic is used to style the error state and to show the error messages. .mat-form-field-invalid { .mat-form-field-placeholder { - color: $form-field-underline-color-warn; + color: $underline-color-warn; &.mat-accent, &.mat-form-field-float .mat-form-field-required-marker { - color: $form-field-underline-color-warn; + color: $underline-color-warn; } } .mat-form-field-ripple { - background-color: $form-field-underline-color-warn; + background-color: $underline-color-warn; } } .mat-error { - color: $form-field-underline-color-warn; + color: $underline-color-warn; } } diff --git a/src/lib/form-field/form-field-control.ts b/src/lib/form-field/form-field-control.ts index 5af51b1f31ac..32d94aacb120 100644 --- a/src/lib/form-field/form-field-control.ts +++ b/src/lib/form-field/form-field-control.ts @@ -11,7 +11,7 @@ import {NgControl} from '@angular/forms'; /** An interface which allows a control to work inside of a `MdFormField`. */ -export abstract class MdFormFieldControl { +export abstract class MdFormFieldControl { /** * Stream that emits whenever the state of the control changes such that the parent `MdFormField` * needs to run change detection. @@ -19,7 +19,7 @@ export abstract class MdFormFieldControl { stateChanges: Observable; /** The value of the control. */ - value: any; + value: T; /** Gets the element ID for this control. */ abstract getId(): string; diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index 8cf704c675d0..9e16b7cc88c5 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -58,6 +58,7 @@ let nextUniqueId = 0; // 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', [ @@ -138,7 +139,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten /** Reference to the form field's underline element. */ @ViewChild('underline') underlineRef: ElementRef; @ViewChild('connectionContainer') _connectionContainerRef: ElementRef; - @ContentChild(MdFormFieldControl) _control: MdFormFieldControl; + @ContentChild(MdFormFieldControl) _control: MdFormFieldControl; @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; @ContentChildren(MdError) _errorChildren: QueryList; @ContentChildren(MdHint) _hintChildren: QueryList; diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 2692e14f104d..bfc2662f16b9 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -67,7 +67,7 @@ let nextUniqueId = 0; }, providers: [{provide: MdFormFieldControl, useExisting: MdInput}], }) -export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoCheck { +export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, DoCheck { /** Variables used as cache for getters and setters. */ private _type = 'text'; private _disabled = false; diff --git a/src/lib/public_api.ts b/src/lib/public_api.ts index 76c73f7f60a0..7c4bdffb425a 100644 --- a/src/lib/public_api.ts +++ b/src/lib/public_api.ts @@ -46,6 +46,3 @@ export * from './tabs/index'; export * from './tabs/tab-nav-bar/index'; export * from './toolbar/index'; export * from './tooltip/index'; - -// TODO(mmalerba): Temporary alias to avoid breakages, cleanup later. -export {MdFormField as MdInputContainer} from './form-field/index'; From 5a957bd416b754b2a110e64819c6939c07fe5b07 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 8 Aug 2017 17:02:37 -0700 Subject: [PATCH 8/8] getter methods --> readonly properties --- src/lib/form-field/form-field-control.ts | 28 +++++++------- src/lib/form-field/form-field.html | 12 +++--- src/lib/form-field/form-field.ts | 16 ++++---- src/lib/input/input.ts | 49 ++++++++---------------- 4 files changed, 44 insertions(+), 61 deletions(-) diff --git a/src/lib/form-field/form-field-control.ts b/src/lib/form-field/form-field-control.ts index 32d94aacb120..98a6ea02089b 100644 --- a/src/lib/form-field/form-field-control.ts +++ b/src/lib/form-field/form-field-control.ts @@ -12,38 +12,38 @@ 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. */ - stateChanges: Observable; - - /** The value of the control. */ - value: T; + readonly stateChanges: Observable; - /** Gets the element ID for this control. */ - abstract getId(): string; + /** The element ID for this control. */ + readonly id: string; - /** Gets the placeholder for this control. */ - abstract getPlaceholder(): string; + /** The placeholder for this control. */ + readonly placeholder: string; /** Gets the NgControl for this control. */ - abstract getNgControl(): NgControl | null; + readonly ngControl: NgControl | null; /** Whether the control is focused. */ - abstract isFocused(): boolean; + readonly focused: boolean; /** Whether the control is empty. */ - abstract isEmpty(): boolean; + readonly empty: boolean; /** Whether the control is required. */ - abstract isRequired(): boolean; + readonly required: boolean; /** Whether the control is disabled. */ - abstract isDisabled(): boolean; + readonly disabled: boolean; /** Whether the control is in an error state. */ - abstract isErrorState(): boolean; + readonly errorState: boolean; /** Sets the list of element IDs that currently describe this control. */ abstract setDescribedByIds(ids: string[]): void; diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html index be2ea21d007d..cfb7bfedab83 100644 --- a/src/lib/form-field/form-field.html +++ b/src/lib/form-field/form-field.html @@ -9,20 +9,20 @@
@@ -33,7 +33,7 @@
+ [class.mat-disabled]="_control.disabled"> diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index 9e16b7cc88c5..eba04bde9791 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -69,9 +69,9 @@ let nextUniqueId = 0; ], host: { 'class': 'mat-input-container mat-form-field', - '[class.mat-input-invalid]': '_control.isErrorState()', - '[class.mat-form-field-invalid]': '_control.isErrorState()', - '[class.mat-focused]': '_control.isFocused()', + '[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")', @@ -163,7 +163,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten this._changeDetectorRef.markForCheck(); }); - let ngControl = this._control.getNgControl(); + let ngControl = this._control.ngControl; if (ngControl && ngControl.valueChanges) { ngControl.valueChanges.subscribe(() => { this._changeDetectorRef.markForCheck(); @@ -195,19 +195,19 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten /** Determines whether a class from the NgControl should be forwarded to the host element. */ _shouldForward(prop: string): boolean { - let ngControl = this._control ? this._control.getNgControl : null; + 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.getPlaceholder() || this._placeholderChild); + 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.isErrorState()) ? 'error' : 'hint'; + this._control.errorState) ? 'error' : 'hint'; } /** @@ -215,7 +215,7 @@ export class MdFormField implements AfterViewInit, AfterContentInit, AfterConten * or child element with the `md-placeholder` directive). */ private _validatePlaceholders() { - if (this._control.getPlaceholder() && this._placeholderChild) { + if (this._control.placeholder && this._placeholderChild) { throw getMdFormFieldPlaceholderConflictError(); } } diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index bfc2662f16b9..f0449a9bc994 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -60,7 +60,7 @@ let nextUniqueId = 0; '[disabled]': 'disabled', '[required]': 'required', '[attr.aria-describedby]': '_ariaDescribedby || null', - '[attr.aria-invalid]': '_isErrorState', + '[attr.aria-invalid]': 'errorState', '(blur)': '_focusChanged(false)', '(focus)': '_focusChanged(true)', '(input)': '_onInput()', @@ -76,8 +76,12 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D private _uid = `md-input-${nextUniqueId++}`; private _errorOptions: ErrorOptions; private _previousNativeValue = this.value; - private _focused = false; - private _isErrorState = false; + + /** 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; @@ -90,7 +94,7 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D /** Whether the element is disabled. */ @Input() - get disabled() { return this._ngControl ? this._ngControl.disabled : this._disabled; } + get disabled() { return this.ngControl ? this.ngControl.disabled : this._disabled; } set disabled(value: any) { this._disabled = coerceBooleanProperty(value); } /** Unique id of the element. */ @@ -145,7 +149,7 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D constructor(private _elementRef: ElementRef, private _renderer: Renderer2, private _platform: Platform, - @Optional() @Self() public _ngControl: NgControl, + @Optional() @Self() public ngControl: NgControl, @Optional() private _parentForm: NgForm, @Optional() private _parentFormGroup: FormGroupDirective, @Optional() @Inject(MD_ERROR_GLOBAL_OPTIONS) errorOptions: ErrorOptions) { @@ -181,7 +185,7 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D } ngDoCheck() { - if (this._ngControl) { + 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. @@ -195,8 +199,8 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D /** Callback for the cases where the focused state of the input changes. */ _focusChanged(isFocused: boolean) { - if (isFocused !== this._focused) { - this._focused = isFocused; + if (isFocused !== this.focused) { + this.focused = isFocused; this.stateChanges.next(); } } @@ -213,13 +217,13 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D /** Re-evaluates the error state. This is only relevant with @angular/forms. */ private _updateErrorState() { - const oldState = this._isErrorState; - const ngControl = this._ngControl; + 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._isErrorState = newState; + this.errorState = newState; this.stateChanges.next(); } } @@ -265,19 +269,7 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D } // Implemented as part of MdFormFieldControl. - getId(): string { return this.id; } - - // Implemented as part of MdFormFieldControl. - getPlaceholder(): string { return this.placeholder; } - - // Implemented as part of MdFormFieldControl. - getNgControl(): NgControl | null { return this._ngControl; } - - // Implemented as part of MdFormFieldControl. - isFocused(): boolean { return this._focused; } - - // Implemented as part of MdFormFieldControl. - isEmpty(): boolean { + 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 @@ -286,15 +278,6 @@ export class MdInput implements MdFormFieldControl, OnChanges, OnDestroy, D !this._isBadInput(); } - // Implemented as part of MdFormFieldControl. - isRequired(): boolean { return this.required; } - - // Implemented as part of MdFormFieldControl. - isDisabled(): boolean { return this.disabled; } - - // Implemented as part of MdFormFieldControl. - isErrorState(): boolean { return this._isErrorState; } - // Implemented as part of MdFormFieldControl. setDescribedByIds(ids: string[]) { this._ariaDescribedby = ids.join(' '); }