From 6e0f9b257e46750570d906c6bac6e058adc2ffb2 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 29 Dec 2016 06:08:15 +0100 Subject: [PATCH] fix(input): properly determine input value Right now the MdInputDirective tries to cache the `value` of the input. > For this the MdInputDirective needs to listen for NgControl value :changes and for native `(change)` events. This caching will be problematic when a value is set directly to the input element (using `[value]` property binding) because Angular is only able to recognize this change in the first ChangeDetection. > The solution of updating the value in the `ngAfterViewInit` lifecycle hook, will result in a binding change (`[class.md-empty]` in the view) while being in a ChangeDetection, which leads to a ChangeDetection error. Fixes #2441. Fixes #2363 --- src/demo-app/input/input-container-demo.html | 5 ++ src/demo-app/input/input-container-demo.ts | 1 + src/lib/input/input-container.spec.ts | 51 +++++++++++++++++++- src/lib/input/input-container.ts | 22 +++------ 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/demo-app/input/input-container-demo.html b/src/demo-app/input/input-container-demo.html index 617a000e5c78..6d1f756d284d 100644 --- a/src/demo-app/input/input-container-demo.html +++ b/src/demo-app/input/input-container-demo.html @@ -1,6 +1,11 @@ Basic + + + + +
diff --git a/src/demo-app/input/input-container-demo.ts b/src/demo-app/input/input-container-demo.ts index 73e5d3308804..cbd038e701a1 100644 --- a/src/demo-app/input/input-container-demo.ts +++ b/src/demo-app/input/input-container-demo.ts @@ -13,6 +13,7 @@ let max = 5; export class InputContainerDemo { dividerColor: boolean; requiredField: boolean; + value: string = 'Initial'; floatingLabel: boolean; name: string; items: any[] = [ diff --git a/src/lib/input/input-container.spec.ts b/src/lib/input/input-container.spec.ts index 335acfaf3c40..72d8688744d6 100644 --- a/src/lib/input/input-container.spec.ts +++ b/src/lib/input/input-container.spec.ts @@ -1,9 +1,9 @@ import {async, TestBed, inject} from '@angular/core/testing'; import {Component} from '@angular/core'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms'; import {By} from '@angular/platform-browser'; import {MdInputModule} from './input'; -import {MdInputContainer} from './input-container'; +import {MdInputContainer, MdInputDirective} from './input-container'; import {Platform} from '../core/platform/platform'; import {PlatformModule} from '../core/platform/index'; import { @@ -41,6 +41,8 @@ describe('MdInputContainer', function () { MdInputContainerZeroTestController, MdTextareaWithBindings, MdInputContainerWithDisabled, + MdInputContainerWithValueBinding, + MdInputContainerWithFormControl, MdInputContainerMissingMdInputTestController ], }); @@ -128,6 +130,20 @@ describe('MdInputContainer', function () { expect(el.classList.contains('md-empty')).toBe(false, 'should not be empty'); })); + 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('.md-input-placeholder')).nativeElement; + + expect(placeholderEl.classList).not.toContain('md-empty'); + + fixture.componentInstance.value = ''; + fixture.detectChanges(); + + expect(placeholderEl.classList).toContain('md-empty'); + })); + it('should not treat the number 0 as empty', async(() => { let fixture = TestBed.createComponent(MdInputContainerZeroTestController); fixture.detectChanges(); @@ -141,6 +157,20 @@ describe('MdInputContainer', function () { }); })); + it('should update the value when using FormControl.setValue', () => { + let fixture = TestBed.createComponent(MdInputContainerWithFormControl); + fixture.detectChanges(); + + let input = fixture.debugElement.query(By.directive(MdInputDirective)) + .injector.get(MdInputDirective) as MdInputDirective; + + expect(input.value).toBeFalsy(); + + fixture.componentInstance.formControl.setValue('something'); + + expect(input.value).toBe('something'); + }); + it('should add id', () => { let fixture = TestBed.createComponent(MdInputContainerTextTestController); fixture.detectChanges(); @@ -326,6 +356,13 @@ class MdInputContainerPlaceholderElementTestComponent { placeholder: string = 'Default Placeholder'; } +@Component({ + template: `` +}) +class MdInputContainerWithFormControl { + formControl = new FormControl(); +} + @Component({ template: `` }) @@ -429,6 +466,16 @@ class MdInputContainerZeroTestController { value = 0; } +@Component({ + template: ` + + + ` +}) +class MdInputContainerWithValueBinding { + value: string = 'Initial'; +} + @Component({ template: ` diff --git a/src/lib/input/input-container.ts b/src/lib/input/input-container.ts index 37d18f2ff0db..d770677c4a25 100644 --- a/src/lib/input/input-container.ts +++ b/src/lib/input/input-container.ts @@ -74,10 +74,10 @@ export class MdHint { '[id]': 'id', '(blur)': '_onBlur()', '(focus)': '_onFocus()', - '(input)': '_onInput()', } }) -export class MdInputDirective implements AfterContentInit { +export class MdInputDirective { + /** Whether the element is disabled. */ @Input() get disabled() { return this._disabled; } @@ -116,8 +116,9 @@ export class MdInputDirective implements AfterContentInit { } private _type = 'text'; - /** The element's value. */ - value: any; + /** The input element's value. */ + get value() { return this._elementRef.nativeElement.value; } + set value(value: string) { this._elementRef.nativeElement.value = value; } /** * Emits an event when the placeholder changes so that the `md-input-container` can re-validate. @@ -143,18 +144,9 @@ export class MdInputDirective implements AfterContentInit { constructor(private _elementRef: ElementRef, private _renderer: Renderer, @Optional() public _ngControl: NgControl) { + // Force setter to be called in case id was not specified. this.id = this.id; - - if (this._ngControl && this._ngControl.valueChanges) { - this._ngControl.valueChanges.subscribe((value) => { - this.value = value; - }); - } - } - - ngAfterContentInit() { - this.value = this._elementRef.nativeElement.value; } /** Focuses the input element. */ @@ -164,8 +156,6 @@ export class MdInputDirective implements AfterContentInit { _onBlur() { this.focused = false; } - _onInput() { this.value = this._elementRef.nativeElement.value; } - /** Make sure the input is a supported type. */ private _validateType() { if (MD_INPUT_INVALID_TYPES.indexOf(this._type) != -1) {