From 855127488d0c2e122cd3fe8577c9b89ceeadb17c Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 1 Nov 2016 17:08:36 -0700 Subject: [PATCH 1/4] feature(md-input): refactor MdInput as an attribute The goal is to simplify the MdInput interface and allow it to evolve and collaborate with other directives on an . For example, Chips could be a directive on the `` as well. The next steps are going to be adding back MdPlaceholder as such a directive that collaborates with MdInput. --- src/demo-app/input/input-demo.html | 199 ++------ src/lib/input/_input-theme.scss | 2 +- src/lib/input/input.html | 88 +--- src/lib/input/input.scss | 6 +- src/lib/input/input.spec.ts | 723 +---------------------------- src/lib/input/input.ts | 377 +++------------ 6 files changed, 150 insertions(+), 1245 deletions(-) diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 1bef8bfd2df4..92477deb6030 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -2,181 +2,80 @@ Basic
- - + + + - - + +

- - + +

- - - - -
- {{postalCode.characterCount}} / 5 -
- - Prefix + Suffix - - - - .00 - - - - - - Divider Colors - -

Input

- - - - -

Textarea

- - - - -
-
- - - Hints - -

Input

-

- - {{characterCountHintExample.characterCount}} / 100 - -

- -

Textarea

-

- - {{characterCountHintExample.characterCount}} / 100 - -

-
-
- - Hello , + Hello , how are you?

- - -

-

- -

-

- - {{input.characterCount}} / 100 - -

-

- -

+ + -

- - - I favorite bold placeholder - - - I also home italic hint labels - - -

-

- -

-

- Check to change the divider color: - -

-

- Check to make required: -

- Check to make floating label: - -

+ + -

- -

Example: 
- - - .00 € - + + +
+ + +
+ Both: - - email  -  @gmail.com - -

+ -

- Empty: - + +

-
- - - - - - - - - - - - -
Table - - - - -
{{i+1}} - - - - {{item.value}}
-
- -

textarea autosize

- + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index 0c424275b7ee..d3327e18c187 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -39,7 +39,7 @@ } // See md-input-placeholder-floating mixin in input.scss - md-input input:-webkit-autofill + .md-input-placeholder, .md-input-placeholder.md-float.md-focused { + .md-input-wrapper input:-webkit-autofill + .md-input-placeholder, .md-input-placeholder.md-float.md-focused { .md-placeholder-required { color: $input-required-placeholder-color; diff --git a/src/lib/input/input.html b/src/lib/input/input.html index 5b6924d94813..28915442d59b 100644 --- a/src/lib/input/input.html +++ b/src/lib/input/input.html @@ -1,86 +1,18 @@ -
+
-
+
+
- - - - - - +
-
+
+
has a system font. font: inherit; @@ -73,7 +73,7 @@ md-input, md-textarea { padding: 0; width: 100%; - &.md-end { + .md-end & { text-align: right; [dir='rtl'] & { diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index d1591c615210..071a0d2ab35e 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -6,57 +6,16 @@ import {Component} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {MdInput, MdInputModule} from './input'; - -function isInternetExplorer11() { - return 'ActiveXObject' in window; -} +import {ProjectionModule} from '../core/projection/projection'; describe('MdInput', function () { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MdInputModule.forRoot(), FormsModule], + imports: [MdInputModule.forRoot(), FormsModule, ProjectionModule.forRoot()], declarations: [ MdInputNumberTypeConservedTestComponent, - MdInputPlaceholderRequiredTestComponent, - MdInputPlaceholderElementTestComponent, - MdInputPlaceholderAttrTestComponent, - MdInputHintLabel2TestController, - MdInputHintLabelTestController, MdInputInvalidTypeTestController, - MdInputInvalidPlaceholderTestController, - MdInputInvalidHint2TestController, - MdInputInvalidHintTestController, MdInputBaseTestController, - MdInputAriaTestController, - MdInputWithBlurAndFocusEvents, - MdInputWithNameTestController, - MdInputWithId, - MdInputWithAutocomplete, - MdInputWithUnboundAutocomplete, - MdInputWithUnboundAutocompleteWithValue, - MdInputWithAutocorrect, - MdInputWithUnboundAutocorrect, - MdInputWithAutocapitalize, - MdInputWithUnboundAutocapitalize, - MdInputWithAutofocus, - MdInputWithUnboundAutofocus, - MdInputWithReadonly, - MdInputWithUnboundReadonly, - MdInputWithSpellcheck, - MdInputWithUnboundSpellcheck, - MdInputWithDisabled, - MdInputWithUnboundDisabled, - MdInputWithRequired, - MdInputWithUnboundRequired, - MdInputWithList, - MdInputWithMax, - MdInputWithMin, - MdInputWithStep, - MdInputWithTabindex, - MdInputDateTestController, - MdInputTextTestController, - MdInputPasswordTestController, - MdInputNumberTestController, MdTextareaWithBindings, ], }); @@ -71,67 +30,6 @@ describe('MdInput', function () { expect(fixture.debugElement.query(By.css('input'))).toBeTruthy(); }); - it('should default to flating placeholders', () => { - let fixture = TestBed.createComponent(MdInputBaseTestController); - fixture.detectChanges(); - - let mdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance as MdInput; - expect(mdInput.floatingPlaceholder) - .toBe(true, 'Expected MdInput to default to having floating placeholders turned on'); - }); - - it('should not be treated as empty if type is date', () => { - if (isInternetExplorer11()) { - return; - } - let fixture = TestBed.createComponent(MdInputDateTestController); - fixture.componentInstance.placeholder = 'Placeholder'; - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')).nativeElement; - expect(el).not.toBeNull(); - expect(el.className.includes('md-empty')).toBe(false); - }); - - it('should treat text input type as empty at init', () => { - if (isInternetExplorer11()) { - return; - } - let fixture = TestBed.createComponent(MdInputTextTestController); - fixture.componentInstance.placeholder = 'Placeholder'; - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')).nativeElement; - expect(el).not.toBeNull(); - expect(el.className.includes('md-empty')).toBe(true); - }); - - it('should treat password input type as empty at init', () => { - if (isInternetExplorer11()) { - return; - } - let fixture = TestBed.createComponent(MdInputPasswordTestController); - fixture.componentInstance.placeholder = 'Placeholder'; - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')).nativeElement; - expect(el).not.toBeNull(); - expect(el.className.includes('md-empty')).toBe(true); - }); - - it('should treat number input type as empty at init', () => { - if (isInternetExplorer11()) { - return; - } - let fixture = TestBed.createComponent(MdInputNumberTestController); - fixture.componentInstance.placeholder = 'Placeholder'; - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')).nativeElement; - expect(el).not.toBeNull(); - expect(el.className.includes('md-empty')).toBe(true); - }); - // TODO(kara): update when core/testing adds fix it('support ngModel', async(() => { let fixture = TestBed.createComponent(MdInputBaseTestController); @@ -151,20 +49,6 @@ describe('MdInput', function () { }); })); - it('should have a different ID for outer element and internal input', () => { - let fixture = TestBed.createComponent(MdInputWithId); - fixture.detectChanges(); - - const componentElement: HTMLElement = - fixture.debugElement.query(By.directive(MdInput)).nativeElement; - const inputElement: HTMLInputElement = - fixture.debugElement.query(By.css('input')).nativeElement; - - expect(componentElement.id).toBe('test-id'); - expect(inputElement.id).toBeTruthy(); - expect(inputElement.id).not.toBe(componentElement.id); - }); - it('counts characters', async(() => { let fixture = TestBed.createComponent(MdInputBaseTestController); let instance = fixture.componentInstance; @@ -179,44 +63,6 @@ describe('MdInput', function () { }); })); - it('copies aria attributes to the inner input', () => { - let fixture = TestBed.createComponent(MdInputAriaTestController); - let instance = fixture.componentInstance; - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - expect(el.getAttribute('aria-label')).toEqual('label'); - instance.ariaLabel = 'label 2'; - fixture.detectChanges(); - expect(el.getAttribute('aria-label')).toEqual('label 2'); - - expect(el.getAttribute('aria-disabled')).toBeTruthy(); - }); - - it(`validates there's only one hint label per side`, () => { - let fixture = TestBed.createComponent(MdInputInvalidHintTestController); - - expect(() => fixture.detectChanges()).toThrow(); - // TODO(jelbourn): .toThrow(new MdInputDuplicatedHintError('start')); - // See https://github.com/angular/angular/issues/8348 - }); - - it(`validates there's only one hint label per side (attribute)`, () => { - let fixture = TestBed.createComponent(MdInputInvalidHint2TestController); - - expect(() => fixture.detectChanges()).toThrow(); - // TODO(jelbourn): .toThrow(new MdInputDuplicatedHintError('start')); - // See https://github.com/angular/angular/issues/8348 - }); - - it('validates there\'s only one placeholder', () => { - let fixture = TestBed.createComponent(MdInputInvalidPlaceholderTestController); - - expect(() => fixture.detectChanges()).toThrow(); - // TODO(jelbourn): .toThrow(new MdInputPlaceholderConflictError()); - // See https://github.com/angular/angular/issues/8348 - }); - it('validates the type', () => { let fixture = TestBed.createComponent(MdInputInvalidTypeTestController); @@ -227,400 +73,6 @@ describe('MdInput', function () { expect(() => fixture.detectChanges()).toThrow(/* new MdInputUnsupportedTypeError('file') */); }); - it('supports hint labels attribute', () => { - let fixture = TestBed.createComponent(MdInputHintLabelTestController); - fixture.detectChanges(); - - // If the hint label is empty, expect no label. - expect(fixture.debugElement.query(By.css('.md-hint'))).toBeNull(); - - fixture.componentInstance.label = 'label'; - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('.md-hint'))).not.toBeNull(); - }); - - it('supports hint labels elements', () => { - let fixture = TestBed.createComponent(MdInputHintLabel2TestController); - fixture.detectChanges(); - - // In this case, we should have an empty . - let el = fixture.debugElement.query(By.css('md-hint')).nativeElement; - expect(el.textContent).toBeFalsy(); - - fixture.componentInstance.label = 'label'; - fixture.detectChanges(); - el = fixture.debugElement.query(By.css('md-hint')).nativeElement; - expect(el.textContent).toBe('label'); - }); - - it('supports placeholder attribute', () => { - let fixture = TestBed.createComponent(MdInputPlaceholderAttrTestComponent); - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')); - expect(el).toBeNull(); - - fixture.componentInstance.placeholder = 'Other placeholder'; - fixture.detectChanges(); - el = fixture.debugElement.query(By.css('label')); - expect(el).not.toBeNull(); - expect(el.nativeElement.textContent).toMatch('Other placeholder'); - expect(el.nativeElement.textContent).not.toMatch(/\*/g); - }); - - it('supports placeholder element', () => { - let fixture = TestBed.createComponent(MdInputPlaceholderElementTestComponent); - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')); - expect(el).not.toBeNull(); - expect(el.nativeElement.textContent).toMatch('Default Placeholder'); - - fixture.componentInstance.placeholder = 'Other placeholder'; - fixture.detectChanges(); - el = fixture.debugElement.query(By.css('label')); - expect(el).not.toBeNull(); - expect(el.nativeElement.textContent).toMatch('Other placeholder'); - expect(el.nativeElement.textContent).not.toMatch(/\*/g); - }); - - it('supports placeholder required star', () => { - let fixture = TestBed.createComponent(MdInputPlaceholderRequiredTestComponent); - fixture.detectChanges(); - - let el = fixture.debugElement.query(By.css('label')); - expect(el).not.toBeNull(); - expect(el.nativeElement.textContent).toMatch(/hello\s+\*/g); - }); - - it('supports number types and conserved its value type from Angular', () => { - let fixture = TestBed.createComponent(MdInputNumberTypeConservedTestComponent); - fixture.detectChanges(); - - const input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - const inputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - // Fake a `change` event being triggered. - inputElement.value = '3'; - input._handleChange( {target: inputElement}); - - fixture.detectChanges(); - expect(fixture.componentInstance.value).toBe(3); - expect(typeof fixture.componentInstance.value).toBe('number'); - }); - - it('supports blur and focus events', () => { - let fixture = TestBed.createComponent(MdInputWithBlurAndFocusEvents); - const testComponent = fixture.componentInstance; - const inputComponent = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - const fakeEvent = {}; - - spyOn(testComponent, 'onFocus'); - spyOn(testComponent, 'onBlur'); - - expect(testComponent.onFocus).not.toHaveBeenCalled(); - expect(testComponent.onBlur).not.toHaveBeenCalled(); - - inputComponent._handleFocus(fakeEvent); - expect(testComponent.onFocus).toHaveBeenCalledWith(fakeEvent); - - inputComponent._handleBlur(fakeEvent); - expect(testComponent.onBlur).toHaveBeenCalledWith(fakeEvent); - }); - - it('supports the autoComplete attribute', () => { - let fixture = TestBed.createComponent(MdInputWithAutocomplete); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autocomplete')).toBeNull(); - - input.autocomplete = 'on'; - fixture.detectChanges(); - expect(el.getAttribute('autocomplete')).toEqual('on'); - }); - - it('supports the autoCorrect attribute', () => { - let fixture = TestBed.createComponent(MdInputWithAutocorrect); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autocorrect')).toBeNull(); - - input.autocorrect = 'on'; - fixture.detectChanges(); - expect(el.getAttribute('autocorrect')).toEqual('on'); - }); - - it('supports the autoCapitalize attribute', () => { - let fixture = TestBed.createComponent(MdInputWithAutocapitalize); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autocapitalize')).toBeNull(); - - input.autocapitalize = 'on'; - fixture.detectChanges(); - expect(el.getAttribute('autocapitalize')).toEqual('on'); - }); - - it('supports the autoComplete attribute as an unbound attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundAutocomplete); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autocomplete')).toEqual(''); - }); - - it('supports the autoComplete attribute as an unbound value attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundAutocompleteWithValue); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autocomplete')).toEqual('name'); - }); - - it('supports the autoFocus attribute', () => { - let fixture = TestBed.createComponent(MdInputWithAutofocus); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autofocus')).toBeNull(); - - input.autofocus = true; - fixture.detectChanges(); - expect(el.getAttribute('autofocus')).toEqual(''); - }); - - it('supports the autoFocus attribute as an unbound attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundAutofocus); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('autofocus')).toEqual(''); - }); - - it('supports the disabled attribute', () => { - let fixture = TestBed.createComponent(MdInputWithDisabled); - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - input.disabled = false; - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - expect(el).not.toBeNull(); - - fixture.detectChanges(); - expect(el.getAttribute('disabled')).toEqual(null); - - fixture.componentInstance.disabled = true; - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(el.getAttribute('disabled')).toEqual(''); - }); - }); - - it('supports the disabled attribute as an unbound attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundDisabled); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - fixture.whenStable().then(() => { - expect(el.getAttribute('disabled')).toEqual(''); - }); - }); - - it('supports the list attribute', () => { - let fixture = TestBed.createComponent(MdInputWithList); - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - input.disabled = false; - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - fixture.detectChanges(); - expect(el.getAttribute('list')).toEqual(null); - - input.list = 'datalist-id'; - fixture.detectChanges(); - expect(el.getAttribute('list')).toEqual('datalist-id'); - }); - - it('supports the max attribute', () => { - let fixture = TestBed.createComponent(MdInputWithMax); - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - input.disabled = false; - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - expect(el).not.toBeNull(); - - fixture.detectChanges(); - expect(el.getAttribute('max')).toEqual(null); - - input.max = 10; - fixture.detectChanges(); - expect(el.getAttribute('max')).toEqual('10'); - - input.max = '2000-01-02'; - fixture.detectChanges(); - expect(el.getAttribute('max')).toEqual('2000-01-02'); - }); - - it('supports the min attribute', () => { - let fixture = TestBed.createComponent(MdInputWithMin); - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - input.disabled = false; - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - expect(el).not.toBeNull(); - fixture.detectChanges(); - expect(el.getAttribute('min')).toEqual(null); - - input.min = 10; - fixture.detectChanges(); - expect(el.getAttribute('min')).toEqual('10'); - - input.min = '2000-01-02'; - fixture.detectChanges(); - expect(el.getAttribute('min')).toEqual('2000-01-02'); - }); - - it('supports the readOnly attribute', () => { - let fixture = TestBed.createComponent(MdInputWithReadonly); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - - expect(el).not.toBeNull(); - expect(el.getAttribute('readonly')).toBeNull(); - - input.readonly = true; - fixture.detectChanges(); - expect(el.getAttribute('readonly')).toEqual(''); - }); - - it('supports the readOnly attribute as an unbound attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundReadonly); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('readonly')).toEqual(''); - }); - - it('supports the required attribute', () => { - let fixture = TestBed.createComponent(MdInputWithRequired); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('required')).toBeNull(); - - input.required = true; - fixture.detectChanges(); - expect(el.getAttribute('required')).toEqual(''); - }); - - it('supports the required attribute as an unbound attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundRequired); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('required')).toEqual(''); - }); - - it('supports the spellCheck attribute', () => { - let fixture = TestBed.createComponent(MdInputWithSpellcheck); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('spellcheck')).toEqual('false'); - - input.spellcheck = true; - fixture.detectChanges(); - expect(el.getAttribute('spellcheck')).toEqual('true'); - }); - - it('supports the spellCheck attribute as an unbound attribute', () => { - let fixture = TestBed.createComponent(MdInputWithUnboundSpellcheck); - fixture.detectChanges(); - - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('spellcheck')).toEqual('true'); - }); - - it('supports the step attribute', () => { - let fixture = TestBed.createComponent(MdInputWithStep); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('step')).toEqual(null); - - input.step = 0.5; - fixture.detectChanges(); - expect(el.getAttribute('step')).toEqual('0.5'); - }); - - it('supports the tabIndex attribute', () => { - let fixture = TestBed.createComponent(MdInputWithTabindex); - fixture.detectChanges(); - - let input: MdInput = fixture.debugElement.query(By.directive(MdInput)).componentInstance; - let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; - - expect(el).not.toBeNull(); - expect(el.getAttribute('tabindex')).toEqual(null); - - input.tabindex = 1; - fixture.detectChanges(); - expect(el.getAttribute('tabindex')).toEqual('1'); - }); - - it('supports a name attribute', () => { - let fixture = TestBed.createComponent(MdInputWithNameTestController); - - fixture.detectChanges(); - - const inputElement: HTMLInputElement = fixture.debugElement.query(By.css('input')) - .nativeElement; - - expect(inputElement.name).toBe('some-name'); - }); - describe('md-textarea', () => { it('supports the rows, cols, and wrap attributes', () => { let fixture = TestBed.createComponent(MdTextareaWithBindings); @@ -636,179 +88,24 @@ describe('MdInput', function () { }); -@Component({template: ``}) -class MdInputWithId { - value: number = 0; -} - -@Component({template: ``}) +@Component({template: ``}) class MdInputNumberTypeConservedTestComponent { value: number = 0; } -@Component({template: ``}) -class MdInputPlaceholderRequiredTestComponent { -} - -@Component({template: ` {{placeholder}} `}) -class MdInputPlaceholderElementTestComponent { - placeholder: string = 'Default Placeholder'; -} - -@Component({template: ``}) -class MdInputPlaceholderAttrTestComponent { - placeholder: string = ''; -} - -@Component({template: ` {{label}} `}) -class MdInputHintLabel2TestController { - label: string = ''; -} - -@Component({template: ``}) -class MdInputHintLabelTestController { - label: string = ''; -} - -@Component({template: ``}) +@Component({template: ``}) class MdInputInvalidTypeTestController { } -@Component({ - template: ` - - World - ` -}) -class MdInputInvalidPlaceholderTestController { } - -@Component({ - template: ` - - World - ` -}) -class MdInputInvalidHint2TestController { } - -@Component({ - template: ` - - Hello - World - ` -}) -class MdInputInvalidHintTestController { } - -@Component({template: ``}) +@Component({template: ``}) class MdInputBaseTestController { model: any = ''; } -@Component({template: - ``}) -class MdInputAriaTestController { - ariaLabel: string = 'label'; - ariaDisabled: boolean = true; -} - -@Component({template: ``}) -class MdInputWithBlurAndFocusEvents { - onBlur(event: FocusEvent) {} - onFocus(event: FocusEvent) {} -} - -@Component({template: ``}) -class MdInputWithNameTestController { } - -@Component({template: ``}) -class MdInputWithAutocomplete { } - -@Component({template: ``}) -class MdInputWithUnboundAutocomplete { } - -@Component({template: ``}) -class MdInputWithUnboundAutocompleteWithValue { } - -@Component({template: ``}) -class MdInputWithAutocorrect { } - -@Component({template: ``}) -class MdInputWithUnboundAutocorrect { } - -@Component({template: ``}) -class MdInputWithAutocapitalize { } - -@Component({template: ``}) -class MdInputWithUnboundAutocapitalize { } - -@Component({template: ``}) -class MdInputWithAutofocus { } - -@Component({template: ``}) -class MdInputWithUnboundAutofocus { } - -@Component({template: ``}) -class MdInputWithReadonly { } - -@Component({template: ``}) -class MdInputWithUnboundReadonly { } - -@Component({template: ``}) -class MdInputWithSpellcheck { } - -@Component({template: ``}) -class MdInputWithUnboundSpellcheck { } - -@Component({template: ``}) -class MdInputWithDisabled { - disabled: boolean; -} - -@Component({template: ``}) -class MdInputWithUnboundDisabled { } - -@Component({template: ``}) -class MdInputWithRequired { } - -@Component({template: ``}) -class MdInputWithUnboundRequired { } - -@Component({template: ``}) -class MdInputWithList { } - -@Component({template: ``}) -class MdInputWithMax { } - -@Component({template: ``}) -class MdInputWithMin { } - -@Component({template: ``}) -class MdInputWithStep { } - -@Component({template: ``}) -class MdInputWithTabindex { } - -@Component({template: ``}) -class MdInputDateTestController { - placeholder: string = ''; -} - -@Component({template: ``}) -class MdInputTextTestController { - placeholder: string = ''; -} - -@Component({template: ``}) -class MdInputPasswordTestController { - placeholder: string = ''; -} - -@Component({template: ``}) -class MdInputNumberTestController { - placeholder: string = ''; -} - -@Component({template: - ``}) +@Component({ + template: ` + + `}) class MdTextareaWithBindings { rows: number = 4; cols: number = 8; diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 3003b3e3e71b..132e0fdb48f7 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -1,38 +1,34 @@ import { - forwardRef, Component, HostBinding, Input, - Directive, - AfterContentInit, - ContentChild, - SimpleChange, - ContentChildren, ViewChild, ElementRef, - QueryList, - OnChanges, - EventEmitter, - Output, + OnInit, NgModule, ModuleWithProviders, ViewEncapsulation, + HostListener, + TemplateRef, } from '@angular/core'; -import {NG_VALUE_ACCESSOR, ControlValueAccessor, FormsModule} from '@angular/forms'; import {CommonModule} from '@angular/common'; -import {MdError, coerceBooleanProperty} from '../core'; -import {Observable} from 'rxjs/Observable'; +import {DomSanitizer, SafeStyle} from '@angular/platform-browser'; +import { + ProjectionModule, + DomProjection, + DomProjectionHost, + MdError, + coerceBooleanProperty +} from '../core'; import {MdTextareaAutosize} from './autosize'; -const noop = () => {}; - +export class MdInputUnsupportedTypeError extends MdError { + constructor(type: string) { + super(`Input type "${type}" isn't supported by md-input.`); + } +} -export const MD_INPUT_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => MdInput), - multi: true -}; // Invalid input type. Using one of these will throw an MdInputUnsupportedTypeError. const MD_INPUT_INVALID_INPUT_TYPE = [ @@ -42,320 +38,101 @@ const MD_INPUT_INVALID_INPUT_TYPE = [ ]; -let nextUniqueId = 0; - - -export class MdInputPlaceholderConflictError extends MdError { - constructor() { - super('Placeholder attribute and child element were both specified.'); - } -} - -export class MdInputUnsupportedTypeError extends MdError { - constructor(type: string) { - super(`Input type "${type}" isn't supported by md-input.`); - } -} - -export class MdInputDuplicatedHintError extends MdError { - constructor(align: string) { - super(`A hint was already declared for 'align="${align}"'.`); - } -} - - - -/** - * The placeholder directive. The content can declare this to implement more - * complex placeholders. - */ -@Directive({ - selector: 'md-placeholder' -}) -export class MdPlaceholder {} - - -/** The hint directive, used to tag content as hint labels (going under the input). */ -@Directive({ - selector: 'md-hint', - host: { - '[class.md-right]': 'align == "end"', - '[class.md-hint]': 'true' - } -}) -export class MdHint { - // Whether to align the hint label at the start or end of the line. - @Input() align: 'start' | 'end' = 'start'; -} - /** - * Component that represents a text input. It encapsulates the HTMLElement and - * improve on its behaviour, along with styling it according to the Material Design. + * A component that can be attached to either an input or a textarea. This is the master */ @Component({ moduleId: module.id, - selector: 'md-input, md-textarea', + selector: 'input[md-input], textarea[md-input]', templateUrl: 'input.html', styleUrls: ['input.css'], - providers: [MD_INPUT_CONTROL_VALUE_ACCESSOR], - host: {'(click)' : 'focus()'}, encapsulation: ViewEncapsulation.None, + host: { + // This is to remove the properties of the `input md-input` itself. We still want to use them + // as an @Input though, so we use HostBinding. + 'class': '', + 'style': '', + 'attr.align': '' + } }) -export class MdInput implements ControlValueAccessor, AfterContentInit, OnChanges { +export class MdInput implements OnInit { + @ViewChild(DomProjectionHost) _host: DomProjectionHost; + @ViewChild('suffix') _suffix: TemplateRef; + @ViewChild('prefix') _prefix: TemplateRef; private _focused: boolean = false; - private _value: any = ''; - - /** Callback registered via registerOnTouched (ControlValueAccessor) */ - private _onTouchedCallback: () => void = noop; - /** Callback registered via registerOnChange (ControlValueAccessor) */ - private _onChangeCallback: (_: any) => void = noop; - - /** - * Aria related inputs. - */ - @Input('aria-label') ariaLabel: string; - @Input('aria-labelledby') ariaLabelledBy: string; - - private _ariaDisabled: boolean; - private _ariaRequired: boolean; - private _ariaInvalid: boolean; - @Input('aria-disabled') - get ariaDisabled(): boolean { return this._ariaDisabled; } - set ariaDisabled(value) { this._ariaDisabled = coerceBooleanProperty(value); } - - @Input('aria-required') - get ariaRequired(): boolean { return this._ariaRequired; } - set ariaRequired(value) { this._ariaRequired = coerceBooleanProperty(value); } - - @Input('aria-invalid') - get ariaInvalid(): boolean { return this._ariaInvalid; } - set ariaInvalid(value) { this._ariaInvalid = coerceBooleanProperty(value); } - - /** - * Content directives. - */ - @ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder; - @ContentChildren(MdHint) _hintChildren: QueryList; - - /** Readonly properties. */ - get focused() { return this._focused; } - get empty() { return (this._value == null || this._value === '') && this.type !== 'date'; } - get characterCount(): number { - return this.empty ? 0 : ('' + this._value).length; + @Input('class') _cssClass: string = ''; + @Input('style') _cssStyle: string = ''; + get _safeCssStyle(): SafeStyle { + return this._dom.bypassSecurityTrustStyle(this._cssStyle || ''); } - get inputId(): string { return `${this.id}-input`; } + @HostBinding('attr.class') get _attrClass(): any { return null; } + @HostBinding('attr.style') get _attrStyle(): any { return null; } - /** - * Bindings. - */ - @Input() align: 'start' | 'end' = 'start'; - @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary'; - @Input() hintLabel: string = ''; - - @Input() autocomplete: string; - @Input() autocorrect: string; - @Input() autocapitalize: string; - @Input() id: string = `md-input-${nextUniqueId++}`; - @Input() list: string = null; - @Input() max: string | number = null; - @Input() maxlength: number = null; - @Input() min: string | number = null; - @Input() minlength: number = null; - @Input() placeholder: string = null; - @Input() step: number = null; - @Input() tabindex: number = null; - @Input() type: string = 'text'; - @Input() name: string = null; + @Input('type') _type: string; - // textarea-specific - @Input() rows: number = null; - @Input() cols: number = null; - @Input() wrap: 'soft' | 'hard' = null; + constructor(private _dom: DomSanitizer, private _projection: DomProjection, + private _ref: ElementRef) {} - private _floatingPlaceholder: boolean = true; - private _autofocus: boolean = false; - private _disabled: boolean = false; - private _readonly: boolean = false; - private _required: boolean = false; - private _spellcheck: boolean = false; - - @Input() - get floatingPlaceholder(): boolean { return this._floatingPlaceholder; } - set floatingPlaceholder(value) { this._floatingPlaceholder = coerceBooleanProperty(value); } + _suffixTemplate(): TemplateRef { + if (typeof this.mdSuffix == 'string') { + return this._suffix; + } else if (this.mdSuffix instanceof TemplateRef) { + return this.mdSuffix; + } else { + return null; + } + } - @Input() - get autofocus(): boolean { return this._autofocus; } - set autofocus(value) { this._autofocus = coerceBooleanProperty(value); } + _prefixTemplate(): TemplateRef { + if (typeof this.mdPrefix == 'string') { + return this._prefix; + } else if (this.mdPrefix instanceof TemplateRef) { + return this.mdPrefix; + } else { + return null; + } + } @Input() get disabled(): boolean { return this._disabled; } set disabled(value) { this._disabled = coerceBooleanProperty(value); } + private _disabled: boolean = false; - @Input() - get readonly(): boolean { return this._readonly; } - set readonly(value) { this._readonly = coerceBooleanProperty(value); } - - @Input() - get required(): boolean { return this._required; } - set required(value) { this._required = coerceBooleanProperty(value); } - - @Input() - get spellcheck(): boolean { return this._spellcheck; } - set spellcheck(value) { this._spellcheck = coerceBooleanProperty(value); } - - - private _blurEmitter: EventEmitter = new EventEmitter(); - private _focusEmitter: EventEmitter = new EventEmitter(); - - @Output('blur') - get onBlur(): Observable { - return this._blurEmitter.asObservable(); + get _value() { return this._ref.nativeElement.value; } + get empty() { + return (this._value == null || this._value === '') + && this._ref.nativeElement.type !== 'date'; } - - @Output('focus') - get onFocus(): Observable { - return this._focusEmitter.asObservable(); + get characterCount(): number { + return this.empty ? 0 : ('' + this._value).length; } - get value(): any { return this._value; }; - @Input() set value(v: any) { - v = this._convertValueForInputType(v); - if (v !== this._value) { - this._value = v; - this._onChangeCallback(v); + ngOnInit() { + this._projection.project(this._ref, this._host); + + if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this._type) != -1) { + throw new MdInputUnsupportedTypeError(this._type); } } - // This is to remove the `align` property of the `md-input` itself. Otherwise HTML5 - // might place it as RTL when we don't want to. We still want to use `align` as an - // Input though, so we use HostBinding. - @HostBinding('attr.align') get _align(): any { return null; } - - - @ViewChild('input') _inputElement: ElementRef; + get focused() { return this._focused; } - _elementType: 'input' | 'textarea'; + @Input() dividerColor: 'primary' | 'accent' | 'warn' = 'primary'; + @Input() align: 'start' | 'end' = 'start'; + @Input() mdPrefix: string | TemplateRef; + @Input() mdSuffix: string | TemplateRef; - constructor(elementRef: ElementRef) { - // Set the element type depending on normalized selector used(md-input / md-textarea) - this._elementType = elementRef.nativeElement.nodeName.toLowerCase() === 'md-input' ? - 'input' : - 'textarea'; - } - /** Set focus on input */ - focus() { - this._inputElement.nativeElement.focus(); - } - - _handleFocus(event: FocusEvent) { + @HostListener('focus') _onFocus() { this._focused = true; - this._focusEmitter.emit(event); } - - _handleBlur(event: FocusEvent) { + @HostListener('blur') _onBlur() { this._focused = false; - this._onTouchedCallback(); - this._blurEmitter.emit(event); - } - - _handleChange(event: Event) { - this.value = (event.target).value; - this._onTouchedCallback(); } - _hasPlaceholder(): boolean { - return !!this.placeholder || this._placeholderChild != null; - } - - /** - * Implemented as part of ControlValueAccessor. - * TODO: internal - */ - writeValue(value: any) { - this._value = value; - } - - /** - * Implemented as part of ControlValueAccessor. - * TODO: internal - */ - registerOnChange(fn: any) { - this._onChangeCallback = fn; - } - - /** - * Implemented as part of ControlValueAccessor. - * TODO: internal - */ - registerOnTouched(fn: any) { - this._onTouchedCallback = fn; - } - - /** TODO: internal */ - ngAfterContentInit() { - this._validateConstraints(); - - // Trigger validation when the hint children change. - this._hintChildren.changes.subscribe(() => { - this._validateConstraints(); - }); - } - - /** TODO: internal */ - ngOnChanges(changes: {[key: string]: SimpleChange}) { - this._validateConstraints(); - } - - /** - * Convert the value passed in to a value that is expected from the type of the md-input. - * This is normally performed by the *_VALUE_ACCESSOR in forms, but since the type is bound - * on our internal input it won't work locally. - * @private - */ - private _convertValueForInputType(v: any): any { - switch (this.type) { - case 'number': return parseFloat(v); - default: return v; - } - } - - /** - * Ensure that all constraints defined by the API are validated, or throw errors otherwise. - * Constraints for now: - * - placeholder attribute and are mutually exclusive. - * - type attribute is not one of the forbidden types (see constant at the top). - * - Maximum one of each `` alignment specified, with the attribute being - * considered as align="start". - * @private - */ - private _validateConstraints() { - if (this.placeholder != '' && this.placeholder != null && this._placeholderChild != null) { - throw new MdInputPlaceholderConflictError(); - } - if (MD_INPUT_INVALID_INPUT_TYPE.indexOf(this.type) != -1) { - throw new MdInputUnsupportedTypeError(this.type); - } - - if (this._hintChildren) { - // Validate the hint labels. - let startHint: MdHint = null; - let endHint: MdHint = null; - this._hintChildren.forEach((hint: MdHint) => { - if (hint.align == 'start') { - if (startHint || this.hintLabel) { - throw new MdInputDuplicatedHintError('start'); - } - startHint = hint; - } else if (hint.align == 'end') { - if (endHint) { - throw new MdInputDuplicatedHintError('end'); - } - endHint = hint; - } - }); - } + focus() { + this._ref.nativeElement.focus(); } } From 92f48f9b7f5a2a4118a42e701a29a61c0fede270 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 15 Nov 2016 10:36:34 -0800 Subject: [PATCH 2/4] add test for attributes class and style --- src/demo-app/input/input-demo.html | 4 ++-- src/lib/input/input.spec.ts | 16 +++++++++++++++- src/lib/input/input.ts | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 92477deb6030..08eb069fcff4 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -11,8 +11,8 @@

- - + +

diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 071a0d2ab35e..a806723a1a83 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -13,6 +13,7 @@ describe('MdInput', function () { TestBed.configureTestingModule({ imports: [MdInputModule.forRoot(), FormsModule, ProjectionModule.forRoot()], declarations: [ + MdInputStyleClassTransferedTestComponent, MdInputNumberTypeConservedTestComponent, MdInputInvalidTypeTestController, MdInputBaseTestController, @@ -49,6 +50,15 @@ describe('MdInput', function () { }); })); + it('moves the class and style to the outer container', async(() => { + let fixture = TestBed.createComponent(MdInputStyleClassTransferedTestComponent); + fixture.detectChanges(); + + let el = fixture.debugElement.query(By.directive(MdInput)).nativeElement; + expect(el.getAttribute('class')).toBeNull(); + expect(el.getAttribute('style')).toBeNull(); + })); + it('counts characters', async(() => { let fixture = TestBed.createComponent(MdInputBaseTestController); let instance = fixture.componentInstance; @@ -88,6 +98,10 @@ describe('MdInput', function () { }); +@Component({template: ``}) +class MdInputStyleClassTransferedTestComponent { +} + @Component({template: ``}) class MdInputNumberTypeConservedTestComponent { value: number = 0; @@ -103,7 +117,7 @@ class MdInputBaseTestController { @Component({ template: ` - `}) class MdTextareaWithBindings { diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 132e0fdb48f7..58f95dd7c13b 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -43,7 +43,7 @@ const MD_INPUT_INVALID_INPUT_TYPE = [ */ @Component({ moduleId: module.id, - selector: 'input[md-input], textarea[md-input]', + selector: 'input[md-input], textarea[md-textarea]', templateUrl: 'input.html', styleUrls: ['input.css'], encapsulation: ViewEncapsulation.None, From 4a5f035b5547405aea180ad7f3a7aa498bcd2f66 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 16 Nov 2016 17:17:58 -0800 Subject: [PATCH 3/4] feat(md-input): add back placeholder as a portal directive --- src/demo-app/input/input-demo.html | 40 +++++++++++++++ src/demo-app/input/input-demo.ts | 1 + src/lib/input/_input-theme.scss | 4 +- src/lib/input/index.ts | 26 ++++++++++ src/lib/input/input.html | 5 +- src/lib/input/input.scss | 18 +++---- src/lib/input/input.spec.ts | 17 +++++-- src/lib/input/input.ts | 17 ++++--- src/lib/input/placeholder.html | 12 +++++ src/lib/input/placeholder.scss | 2 + src/lib/input/placeholder.spec.ts | 66 ++++++++++++++++++++++++ src/lib/input/placeholder.ts | 81 ++++++++++++++++++++++++++++++ 12 files changed, 266 insertions(+), 23 deletions(-) create mode 100644 src/lib/input/placeholder.html create mode 100644 src/lib/input/placeholder.scss create mode 100644 src/lib/input/placeholder.spec.ts create mode 100644 src/lib/input/placeholder.ts diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 08eb069fcff4..09b7dd946bff 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -78,4 +78,44 @@ + + + + Divider Colors + +

Input

+ + + + +

Textarea

+ + + + +
+
+ + + Placeholder Templates + + +

+ + + +

+ +

+ +
+ Value: + + +

+
diff --git a/src/demo-app/input/input-demo.ts b/src/demo-app/input/input-demo.ts index 49dcfe0f56a8..9e934d5c8d57 100644 --- a/src/demo-app/input/input-demo.ts +++ b/src/demo-app/input/input-demo.ts @@ -10,6 +10,7 @@ let max = 5; styleUrls: ['input-demo.css'], }) export class InputDemo { + value: string = 'test'; dividerColor: boolean; requiredField: boolean; floatingLabel: boolean; diff --git a/src/lib/input/_input-theme.scss b/src/lib/input/_input-theme.scss index d3327e18c187..d7051ac2d1a7 100644 --- a/src/lib/input/_input-theme.scss +++ b/src/lib/input/_input-theme.scss @@ -21,7 +21,7 @@ $input-underline-disabled-color: md-color($foreground, hint-text); $input-underline-focused-color: md-color($primary); - .md-input-placeholder { + .md-placeholder { color: $input-placeholder-color; // :focus is applied to the input, but we apply md-focused to the other elements @@ -39,7 +39,7 @@ } // See md-input-placeholder-floating mixin in input.scss - .md-input-wrapper input:-webkit-autofill + .md-input-placeholder, .md-input-placeholder.md-float.md-focused { + .md-input-wrapper input:-webkit-autofill + .md-placeholder, .md-placeholder.md-float.md-focused { .md-placeholder-required { color: $input-required-placeholder-color; diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts index e3365cb9065e..01f88ba7bfc4 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -1 +1,27 @@ +import {CommonModule} from '@angular/common'; +import {ModuleWithProviders, NgModule} from '@angular/core'; + +import {ProjectionModule, PortalModule} from '../core'; + +import {MdInput} from './input'; +import {MdPlaceholder, MdPlaceholderContent} from './placeholder'; + + export * from './input'; +export * from './placeholder'; + + +@NgModule({ + declarations: [MdPlaceholderContent, MdPlaceholder, MdInput], + imports: [PortalModule, ProjectionModule, CommonModule], + exports: [MdPlaceholder, MdInput], + entryComponents: [MdPlaceholderContent] +}) +export class MdInputModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: MdInputModule, + providers: [] + }; + } +} diff --git a/src/lib/input/input.html b/src/lib/input/input.html index 28915442d59b..edd958afd367 100644 --- a/src/lib/input/input.html +++ b/src/lib/input/input.html @@ -11,7 +11,10 @@
-
+
+ + +
diff --git a/src/lib/input/input.scss b/src/lib/input/input.scss index 7d987cecdf82..01013fa24fb9 100644 --- a/src/lib/input/input.scss +++ b/src/lib/input/input.scss @@ -2,18 +2,18 @@ @import '../core/style/form-common'; -$md-input-floating-placeholder-scale-factor: 0.75 !default; +$md-floating-placeholder-scale-factor: 0.75 !default; // Gradient for showing the dashed line when the input is disabled. $md-input-underline-disabled-background-image: linear-gradient(to right, rgba(0, 0, 0, 0.26) 0%, rgba(0, 0, 0, 0.26) 33%, transparent 0%); // Applies a floating placeholder above the input itself. -@mixin md-input-placeholder-floating { +@mixin md-placeholder-floating { display: block; padding-bottom: 5px; - transform: translateY(-100%) scale($md-input-floating-placeholder-scale-factor); - width: 100% / $md-input-floating-placeholder-scale-factor; + transform: translateY(-100%) scale($md-floating-placeholder-scale-factor); + width: 100% / $md-floating-placeholder-scale-factor; } .md-input-wrapper { @@ -57,7 +57,7 @@ $md-input-underline-disabled-background-image: } // The Input element proper. -input[md-input], textarea[md-input] { +input[md-input], textarea[md-textarea] { // Font needs to be inherited, because by default has a system font. font: inherit; @@ -93,15 +93,15 @@ input[md-input], textarea[md-input] { // Once the autofill is committed, a change event happen and the regular md-input // classes take over to fulfill this behaviour. // Assumes the autofill is non-empty. - &:-webkit-autofill + .md-input-placeholder.md-float { - @include md-input-placeholder-floating; + &:-webkit-autofill + .md-placeholder.md-float { + @include md-placeholder-floating; } } // 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. -.md-input-placeholder { +.md-placeholder { // The placeholder is after the , but needs to be aligned top-left of the // infix
. position: absolute; @@ -133,7 +133,7 @@ input[md-input], textarea[md-input] { // Show the placeholder above the input when it's not empty, or focused. &.md-float:not(.md-empty), &.md-float.md-focused { - @include md-input-placeholder-floating; + @include md-placeholder-floating; } [dir='rtl'] & { diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index a806723a1a83..485853f487ae 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -5,7 +5,7 @@ import { import {Component} from '@angular/core'; import {FormsModule} from '@angular/forms'; import {By} from '@angular/platform-browser'; -import {MdInput, MdInputModule} from './input'; +import {MdInput, MdInputModule} from './index'; import {ProjectionModule} from '../core/projection/projection'; describe('MdInput', function () { @@ -34,8 +34,8 @@ describe('MdInput', function () { // TODO(kara): update when core/testing adds fix it('support ngModel', async(() => { let fixture = TestBed.createComponent(MdInputBaseTestController); - fixture.detectChanges(); + let instance = fixture.componentInstance; let el: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; @@ -61,8 +61,9 @@ describe('MdInput', function () { it('counts characters', async(() => { let fixture = TestBed.createComponent(MdInputBaseTestController); - let instance = fixture.componentInstance; fixture.detectChanges(); + + let instance = fixture.componentInstance; let inputInstance = fixture.debugElement.query(By.directive(MdInput)).componentInstance; expect(inputInstance.characterCount).toEqual(0); @@ -95,7 +96,6 @@ describe('MdInput', function () { expect(textarea.wrap).toBe('hard'); }); }); - }); @Component({template: ``}) @@ -107,6 +107,15 @@ class MdInputNumberTypeConservedTestComponent { value: number = 0; } +@Component({template: ``}) +class MdInputPlaceholderStringTestController { } + +@Component({template: ` + + +`}) +class MdInputPlaceholderTemplateTestController { } + @Component({template: ``}) class MdInputInvalidTypeTestController { } diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 58f95dd7c13b..2b89d1ffe983 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -5,22 +5,21 @@ import { ViewChild, ElementRef, OnInit, - NgModule, - ModuleWithProviders, ViewEncapsulation, HostListener, - TemplateRef, + TemplateRef, forwardRef, } from '@angular/core'; -import {CommonModule} from '@angular/common'; import {DomSanitizer, SafeStyle} from '@angular/platform-browser'; import { - ProjectionModule, DomProjection, DomProjectionHost, MdError, coerceBooleanProperty } from '../core'; import {MdTextareaAutosize} from './autosize'; +import {MD_PLACEHOLDER_HOST_TOKEN, MdPlaceholderHost} from './placeholder'; +import {PortalHost} from '../core/portal/portal'; +import {PortalHostDirective} from '../core/portal/portal-directives'; export class MdInputUnsupportedTypeError extends MdError { @@ -47,6 +46,9 @@ const MD_INPUT_INVALID_INPUT_TYPE = [ templateUrl: 'input.html', styleUrls: ['input.css'], encapsulation: ViewEncapsulation.None, + providers: [ + { provide: MD_PLACEHOLDER_HOST_TOKEN, useExisting: forwardRef(() => MdInput) } + ], host: { // This is to remove the properties of the `input md-input` itself. We still want to use them // as an @Input though, so we use HostBinding. @@ -55,10 +57,12 @@ const MD_INPUT_INVALID_INPUT_TYPE = [ 'attr.align': '' } }) -export class MdInput implements OnInit { +export class MdInput implements OnInit, MdPlaceholderHost { @ViewChild(DomProjectionHost) _host: DomProjectionHost; @ViewChild('suffix') _suffix: TemplateRef; @ViewChild('prefix') _prefix: TemplateRef; + + @ViewChild('suffixWrapper', { read: PortalHostDirective }) placeholderPortalHost: PortalHost; private _focused: boolean = false; @Input('class') _cssClass: string = ''; @@ -123,7 +127,6 @@ export class MdInput implements OnInit { @Input() mdPrefix: string | TemplateRef; @Input() mdSuffix: string | TemplateRef; - @HostListener('focus') _onFocus() { this._focused = true; } diff --git a/src/lib/input/placeholder.html b/src/lib/input/placeholder.html new file mode 100644 index 000000000000..783b1c80f7c3 --- /dev/null +++ b/src/lib/input/placeholder.html @@ -0,0 +1,12 @@ + + + diff --git a/src/lib/input/placeholder.scss b/src/lib/input/placeholder.scss new file mode 100644 index 000000000000..139597f9cb07 --- /dev/null +++ b/src/lib/input/placeholder.scss @@ -0,0 +1,2 @@ + + diff --git a/src/lib/input/placeholder.spec.ts b/src/lib/input/placeholder.spec.ts new file mode 100644 index 000000000000..ef74d0d4d110 --- /dev/null +++ b/src/lib/input/placeholder.spec.ts @@ -0,0 +1,66 @@ +import { + async, + TestBed, +} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {By} from '@angular/platform-browser'; +import {MdInputModule} from './index'; +import {ProjectionModule} from '../core/projection/projection'; + +fdescribe('MdPlaceholder', function () { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MdInputModule.forRoot(), FormsModule, ProjectionModule.forRoot()], + declarations: [ + MdPlaceholderStringTestController, + MdPlaceholderTemplateTestController, + ], + }); + + TestBed.compileComponents(); + })); + + it('works with string placeholder', () => { + let fixture = TestBed.createComponent(MdPlaceholderStringTestController); + fixture.detectChanges(); + + let instance: MdPlaceholderStringTestController = fixture.componentInstance; + let el: HTMLElement = fixture.nativeElement; + let labelEl: HTMLLabelElement = el.querySelector('label'); + expect(labelEl).not.toBeNull(); + expect(labelEl.innerText).toEqual('string placeholder'); + + instance.required = true; + fixture.detectChanges(); + expect(labelEl.innerText).toEqual('string placeholder *'); + }); + + it('works with template placeholder', () => { + let fixture = TestBed.createComponent(MdPlaceholderTemplateTestController); + fixture.detectChanges(); + + let instance: MdPlaceholderTemplateTestController = fixture.componentInstance; + let el: HTMLElement = fixture.nativeElement; + let labelEl: HTMLLabelElement = el.querySelector('label'); + expect(labelEl).not.toBeNull(); + expect(labelEl.innerText).toEqual('template placeholder'); + + instance.required = true; + fixture.detectChanges(); + expect(labelEl.innerText).toEqual('template placeholder *'); + }); +}); + +@Component({template: ``}) +class MdPlaceholderStringTestController { + required: boolean = false; +} + +@Component({template: ` + + +`}) +class MdPlaceholderTemplateTestController { + required: boolean = false; +} diff --git a/src/lib/input/placeholder.ts b/src/lib/input/placeholder.ts new file mode 100644 index 000000000000..4912b5aad516 --- /dev/null +++ b/src/lib/input/placeholder.ts @@ -0,0 +1,81 @@ +import { + Directive, + Inject, + Input, + OpaqueToken, + TemplateRef, Component, ViewContainerRef, ComponentRef, HostBinding, ViewChild, HostListener, + AfterContentInit, ReflectiveInjector, Injector, +} from '@angular/core'; +import {PortalHost} from '../core'; +import {ComponentPortal} from '../core/portal/portal'; +import {coerceBooleanProperty} from '../core/coersion/boolean-property'; + + +export const MD_PLACEHOLDER_HOST_TOKEN = new OpaqueToken('mdPlaceholderHost'); + +export interface MdPlaceholderHost { + placeholderPortalHost: PortalHost; + readonly dividerColor: string; + readonly empty: boolean; +} + + +@Component({ + moduleId: module.id, + selector: '', + templateUrl: 'placeholder.html', + styleUrls: ['placeholder.css'], +}) +export class MdPlaceholderContent { + @ViewChild('stringTemplate') public _stringTemplate: TemplateRef; + public content: string | TemplateRef = ''; + public placeholder: MdPlaceholder = null; + + _template(): TemplateRef { + if (typeof this.content == 'string') { + return this._stringTemplate; + } else if (this.content instanceof TemplateRef) { + return this.content; + } else { + return null; + } + } +} + + +@Directive({ + selector: '[md-input][placeholder], [md-textarea][placeholder], [md-placeholder]:not(template)', +}) +export class MdPlaceholder { + @Input('required') set required(v: boolean | null) { + this._required = coerceBooleanProperty(v); + } + @Input('placeholder') placeholder: string | TemplateRef; + @Input('floatingPlaceholder') set floatingPlaceholder(v: boolean) { + this._floatingPlaceholder = coerceBooleanProperty(v); + } + get floatingPlaceholder(): boolean { return this._floatingPlaceholder; } + + get dividerColor() { return this._host.dividerColor; } + get empty() { return this._host.empty; } + + @HostBinding('attr.placeholder') _attrPlaceholder: any = null; + + @HostListener('focus') _onFocus() { this._focused = true; } + @HostListener('blur') _onBlur() { this._focused = false; } + + _focused: boolean = false; + _required: boolean = false; + _floatingPlaceholder: boolean = true; + + constructor(@Inject(MD_PLACEHOLDER_HOST_TOKEN) public _host: MdPlaceholderHost, + private _vcr: ViewContainerRef) {} + + ngOnInit() { + const portal = new ComponentPortal(MdPlaceholderContent, this._vcr); + const componentRef: ComponentRef = + this._host.placeholderPortalHost.attach(portal); + componentRef.instance.content = this.placeholder; + componentRef.instance.placeholder = this; + } +} From 3fe01d935cc8708329543da427188b26db6e408a Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Fri, 18 Nov 2016 10:47:32 -0800 Subject: [PATCH 4/4] comments, round 1 --- src/demo-app/input/input-demo.html | 2 +- src/lib/input/autosize.spec.ts | 2 +- src/lib/input/index.ts | 5 +++-- src/lib/input/input.html | 6 +++--- src/lib/input/input.scss | 2 +- src/lib/input/input.spec.ts | 3 ++- src/lib/input/input.ts | 31 ++++++++---------------------- src/lib/input/placeholder.html | 8 ++++---- src/lib/input/placeholder.spec.ts | 15 ++++++++------- src/lib/input/placeholder.ts | 21 ++++++++++++++++---- 10 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/demo-app/input/input-demo.html b/src/demo-app/input/input-demo.html index 09b7dd946bff..515ed101ea5e 100644 --- a/src/demo-app/input/input-demo.html +++ b/src/demo-app/input/input-demo.html @@ -12,7 +12,7 @@

- +

diff --git a/src/lib/input/autosize.spec.ts b/src/lib/input/autosize.spec.ts index 102ec41875db..ac27c462aad9 100644 --- a/src/lib/input/autosize.spec.ts +++ b/src/lib/input/autosize.spec.ts @@ -1,7 +1,7 @@ import {Component} from '@angular/core'; import {ComponentFixture, TestBed, async} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; -import {MdInputModule} from './input'; +import {MdInputModule} from './index'; import {MdTextareaAutosize} from './autosize'; diff --git a/src/lib/input/index.ts b/src/lib/input/index.ts index 01f88ba7bfc4..cfe5b5d90e57 100644 --- a/src/lib/input/index.ts +++ b/src/lib/input/index.ts @@ -3,6 +3,7 @@ import {ModuleWithProviders, NgModule} from '@angular/core'; import {ProjectionModule, PortalModule} from '../core'; +import {MdTextareaAutosize} from './autosize'; import {MdInput} from './input'; import {MdPlaceholder, MdPlaceholderContent} from './placeholder'; @@ -12,9 +13,9 @@ export * from './placeholder'; @NgModule({ - declarations: [MdPlaceholderContent, MdPlaceholder, MdInput], + declarations: [MdPlaceholderContent, MdPlaceholder, MdInput, MdTextareaAutosize], imports: [PortalModule, ProjectionModule, CommonModule], - exports: [MdPlaceholder, MdInput], + exports: [MdPlaceholder, MdInput, MdTextareaAutosize], entryComponents: [MdPlaceholderContent] }) export class MdInputModule { diff --git a/src/lib/input/input.html b/src/lib/input/input.html index edd958afd367..5d9fa1264e18 100644 --- a/src/lib/input/input.html +++ b/src/lib/input/input.html @@ -1,8 +1,8 @@
+ [class.md-end]="align == 'end'">
diff --git a/src/lib/input/input.scss b/src/lib/input/input.scss index 01013fa24fb9..f4a9578453f7 100644 --- a/src/lib/input/input.scss +++ b/src/lib/input/input.scss @@ -57,7 +57,7 @@ $md-input-underline-disabled-background-image: } // The Input element proper. -input[md-input], textarea[md-textarea] { +.md-input-element { // Font needs to be inherited, because by default has a system font. font: inherit; diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 485853f487ae..9b295b4a795f 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -55,7 +55,8 @@ describe('MdInput', function () { fixture.detectChanges(); let el = fixture.debugElement.query(By.directive(MdInput)).nativeElement; - expect(el.getAttribute('class')).toBeNull(); + // We set the input element's class. + expect(el.getAttribute('class')).toBe('md-input-element'); expect(el.getAttribute('style')).toBeNull(); })); diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 2b89d1ffe983..6f931dfdfdff 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -1,13 +1,14 @@ import { Component, + ElementRef, HostBinding, + HostListener, Input, - ViewChild, - ElementRef, OnInit, + TemplateRef, + ViewChild, ViewEncapsulation, - HostListener, - TemplateRef, forwardRef, + forwardRef, } from '@angular/core'; import {DomSanitizer, SafeStyle} from '@angular/platform-browser'; import { @@ -16,8 +17,7 @@ import { MdError, coerceBooleanProperty } from '../core'; -import {MdTextareaAutosize} from './autosize'; -import {MD_PLACEHOLDER_HOST_TOKEN, MdPlaceholderHost} from './placeholder'; +import {MD_PLACEHOLDER_HOST, MdPlaceholderHost} from './placeholder'; import {PortalHost} from '../core/portal/portal'; import {PortalHostDirective} from '../core/portal/portal-directives'; @@ -47,7 +47,7 @@ const MD_INPUT_INVALID_INPUT_TYPE = [ styleUrls: ['input.css'], encapsulation: ViewEncapsulation.None, providers: [ - { provide: MD_PLACEHOLDER_HOST_TOKEN, useExisting: forwardRef(() => MdInput) } + { provide: MD_PLACEHOLDER_HOST, useExisting: forwardRef(() => MdInput) } ], host: { // This is to remove the properties of the `input md-input` itself. We still want to use them @@ -70,7 +70,7 @@ export class MdInput implements OnInit, MdPlaceholderHost { get _safeCssStyle(): SafeStyle { return this._dom.bypassSecurityTrustStyle(this._cssStyle || ''); } - @HostBinding('attr.class') get _attrClass(): any { return null; } + @HostBinding('attr.class') get _attrClass(): any { return 'md-input-element'; } @HostBinding('attr.style') get _attrStyle(): any { return null; } @Input('type') _type: string; @@ -138,18 +138,3 @@ export class MdInput implements OnInit, MdPlaceholderHost { this._ref.nativeElement.focus(); } } - - -@NgModule({ - declarations: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize], - imports: [CommonModule, FormsModule], - exports: [MdPlaceholder, MdInput, MdHint, MdTextareaAutosize], -}) -export class MdInputModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: MdInputModule, - providers: [] - }; - } -} diff --git a/src/lib/input/placeholder.html b/src/lib/input/placeholder.html index 783b1c80f7c3..38b6c22380c0 100644 --- a/src/lib/input/placeholder.html +++ b/src/lib/input/placeholder.html @@ -4,9 +4,9 @@ [class.md-focused]="placeholder?._focused" [class.md-float]="placeholder?._floatingPlaceholder" [class.md-accent]="placeholder?.dividerColor == 'accent'" - [class.md-warn]="placeholder?.dividerColor == 'warn'"> - - * - + [class.md-warn]="placeholder?.dividerColor == 'warn'" + > * diff --git a/src/lib/input/placeholder.spec.ts b/src/lib/input/placeholder.spec.ts index ef74d0d4d110..5c373e717788 100644 --- a/src/lib/input/placeholder.spec.ts +++ b/src/lib/input/placeholder.spec.ts @@ -4,11 +4,10 @@ import { } from '@angular/core/testing'; import {Component} from '@angular/core'; import {FormsModule} from '@angular/forms'; -import {By} from '@angular/platform-browser'; import {MdInputModule} from './index'; import {ProjectionModule} from '../core/projection/projection'; -fdescribe('MdPlaceholder', function () { +describe('MdPlaceholder', function () { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MdInputModule.forRoot(), FormsModule, ProjectionModule.forRoot()], @@ -29,11 +28,11 @@ fdescribe('MdPlaceholder', function () { let el: HTMLElement = fixture.nativeElement; let labelEl: HTMLLabelElement = el.querySelector('label'); expect(labelEl).not.toBeNull(); - expect(labelEl.innerText).toEqual('string placeholder'); + expect(labelEl.textContent).toEqual('string placeholder'); instance.required = true; fixture.detectChanges(); - expect(labelEl.innerText).toEqual('string placeholder *'); + expect(labelEl.textContent).toEqual('string placeholder *'); }); it('works with template placeholder', () => { @@ -44,11 +43,13 @@ fdescribe('MdPlaceholder', function () { let el: HTMLElement = fixture.nativeElement; let labelEl: HTMLLabelElement = el.querySelector('label'); expect(labelEl).not.toBeNull(); - expect(labelEl.innerText).toEqual('template placeholder'); + expect(labelEl.textContent).toEqual('template placeholder'); + expect(labelEl.querySelector('b')).not.toBeNull(); + expect(labelEl.querySelector('b').textContent).toEqual('placeholder'); instance.required = true; fixture.detectChanges(); - expect(labelEl.innerText).toEqual('template placeholder *'); + expect(labelEl.textContent).toEqual('template placeholder *'); }); }); @@ -59,7 +60,7 @@ class MdPlaceholderStringTestController { @Component({template: ` - + `}) class MdPlaceholderTemplateTestController { required: boolean = false; diff --git a/src/lib/input/placeholder.ts b/src/lib/input/placeholder.ts index 4912b5aad516..10e2b03e8605 100644 --- a/src/lib/input/placeholder.ts +++ b/src/lib/input/placeholder.ts @@ -1,18 +1,31 @@ import { + Component, + ComponentRef, Directive, + HostBinding, + HostListener, Inject, Input, OpaqueToken, - TemplateRef, Component, ViewContainerRef, ComponentRef, HostBinding, ViewChild, HostListener, - AfterContentInit, ReflectiveInjector, Injector, + TemplateRef, + ViewChild, + ViewContainerRef, } from '@angular/core'; import {PortalHost} from '../core'; import {ComponentPortal} from '../core/portal/portal'; import {coerceBooleanProperty} from '../core/coersion/boolean-property'; -export const MD_PLACEHOLDER_HOST_TOKEN = new OpaqueToken('mdPlaceholderHost'); +/** + * A token to provide the host interface that MdPlaceholder will use to inject itself in + * the View tree. If this token isn't on the host of the directive, an error will be thrown. + * @type {OpaqueToken} + */ +export const MD_PLACEHOLDER_HOST = new OpaqueToken('mdPlaceholderHost'); +/** + * Interface for components that want to host an MdPlaceholder directive. + */ export interface MdPlaceholderHost { placeholderPortalHost: PortalHost; readonly dividerColor: string; @@ -68,7 +81,7 @@ export class MdPlaceholder { _required: boolean = false; _floatingPlaceholder: boolean = true; - constructor(@Inject(MD_PLACEHOLDER_HOST_TOKEN) public _host: MdPlaceholderHost, + constructor(@Inject(MD_PLACEHOLDER_HOST) public _host: MdPlaceholderHost, private _vcr: ViewContainerRef) {} ngOnInit() {