diff --git a/modules/angular2/src/change_detection/change_detection_util.ts b/modules/angular2/src/change_detection/change_detection_util.ts index 1e252a8c1e..b55c235c3a 100644 --- a/modules/angular2/src/change_detection/change_detection_util.ts +++ b/modules/angular2/src/change_detection/change_detection_util.ts @@ -9,6 +9,8 @@ export var uninitialized = new Object(); export class SimpleChange { constructor(public previousValue: any, public currentValue: any) {} + + isFirstChange(): boolean { return this.previousValue === uninitialized; } } var _simpleChangesIndex = 0; diff --git a/modules/angular2/src/forms/directives/checkbox_value_accessor.ts b/modules/angular2/src/forms/directives/checkbox_value_accessor.ts index 9042724278..33c4596cb5 100644 --- a/modules/angular2/src/forms/directives/checkbox_value_accessor.ts +++ b/modules/angular2/src/forms/directives/checkbox_value_accessor.ts @@ -21,7 +21,6 @@ import {setProperty} from './shared'; host: { '(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()', - '[checked]': 'checked', '[class.ng-untouched]': 'ngClassUntouched', '[class.ng-touched]': 'ngClassTouched', '[class.ng-pristine]': 'ngClassPristine', @@ -31,7 +30,6 @@ import {setProperty} from './shared'; } }) export class CheckboxControlValueAccessor implements ControlValueAccessor { - checked: boolean; onChange = (_) => {}; onTouched = () => {}; @@ -39,12 +37,7 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor { cd.valueAccessor = this; } - writeValue(value) { - // both this.checked and setProperty are required at the moment - // remove when a proper imperative API is provided - this.checked = value; - setProperty(this.renderer, this.elementRef, "checked", value); - } + writeValue(value) { setProperty(this.renderer, this.elementRef, "checked", value); } get ngClassUntouched(): boolean { return isPresent(this.cd.control) ? this.cd.control.untouched : false; diff --git a/modules/angular2/src/forms/directives/default_value_accessor.ts b/modules/angular2/src/forms/directives/default_value_accessor.ts index c8eb89cb75..66e4ec8dae 100644 --- a/modules/angular2/src/forms/directives/default_value_accessor.ts +++ b/modules/angular2/src/forms/directives/default_value_accessor.ts @@ -22,7 +22,6 @@ import {setProperty} from './shared'; '(change)': 'onChange($event.target.value)', '(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()', - '[value]': 'value', '[class.ng-untouched]': 'ngClassUntouched', '[class.ng-touched]': 'ngClassTouched', '[class.ng-pristine]': 'ngClassPristine', @@ -32,8 +31,6 @@ import {setProperty} from './shared'; } }) export class DefaultValueAccessor implements ControlValueAccessor { - value: string = null; - onChange = (_) => {}; onTouched = () => {}; @@ -44,8 +41,8 @@ export class DefaultValueAccessor implements ControlValueAccessor { writeValue(value) { // both this.value and setProperty are required at the moment // remove when a proper imperative API is provided - this.value = isBlank(value) ? '' : value; - setProperty(this.renderer, this.elementRef, 'value', this.value); + var normalizedValue = isBlank(value) ? '' : value; + setProperty(this.renderer, this.elementRef, 'value', normalizedValue); } get ngClassUntouched(): boolean { diff --git a/modules/angular2/src/forms/directives/ng_control_name.ts b/modules/angular2/src/forms/directives/ng_control_name.ts index e0d085ee55..d7f63b28f1 100644 --- a/modules/angular2/src/forms/directives/ng_control_name.ts +++ b/modules/angular2/src/forms/directives/ng_control_name.ts @@ -1,6 +1,6 @@ import {CONST_EXPR} from 'angular2/src/facade/lang'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; -import {List, StringMapWrapper, StringMap} from 'angular2/src/facade/collection'; +import {List, StringMap} from 'angular2/src/facade/collection'; import {Directive, LifecycleEvent, Query, QueryList} from 'angular2/angular2'; import {forwardRef, Ancestor, Binding, Inject} from 'angular2/di'; @@ -8,7 +8,7 @@ import {forwardRef, Ancestor, Binding, Inject} from 'angular2/di'; import {ControlContainer} from './control_container'; import {NgControl} from './ng_control'; import {NgValidator} from './validators'; -import {controlPath, composeNgValidator} from './shared'; +import {controlPath, composeNgValidator, isPropertyUpdated} from './shared'; import {Control} from '../model'; const controlNameBinding = @@ -82,6 +82,7 @@ export class NgControlName extends NgControl { _parent: ControlContainer; update = new EventEmitter(); model: any; + viewModel: any; ngValidators: QueryList; _added = false; @@ -98,14 +99,18 @@ export class NgControlName extends NgControl { this.formDirective.addControl(this); this._added = true; } - if (StringMapWrapper.contains(c, "model")) { + if (isPropertyUpdated(c, this.viewModel)) { + this.viewModel = this.model; this.formDirective.updateModel(this, this.model); } } onDestroy() { this.formDirective.removeControl(this); } - viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.update, newValue); } + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + ObservableWrapper.callNext(this.update, newValue); + } get path(): List { return controlPath(this.name, this._parent); } diff --git a/modules/angular2/src/forms/directives/ng_form_control.ts b/modules/angular2/src/forms/directives/ng_form_control.ts index 82364a7262..c67ac68eb9 100644 --- a/modules/angular2/src/forms/directives/ng_form_control.ts +++ b/modules/angular2/src/forms/directives/ng_form_control.ts @@ -1,5 +1,4 @@ import {CONST_EXPR} from 'angular2/src/facade/lang'; -import {StringMapWrapper} from 'angular2/src/facade/collection'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {Directive, LifecycleEvent, Query, QueryList} from 'angular2/angular2'; @@ -8,7 +7,7 @@ import {forwardRef, Ancestor, Binding} from 'angular2/di'; import {NgControl} from './ng_control'; import {Control} from '../model'; import {NgValidator} from './validators'; -import {setUpControl, composeNgValidator} from './shared'; +import {setUpControl, composeNgValidator, isPropertyUpdated} from './shared'; const formControlBinding = CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgFormControl)})); @@ -71,6 +70,7 @@ export class NgFormControl extends NgControl { update = new EventEmitter(); _added = false; model: any; + viewModel: any; ngValidators: QueryList; // Scope the query once https://github.com/angular/angular/issues/2603 is fixed @@ -85,7 +85,7 @@ export class NgFormControl extends NgControl { this.form.updateValidity(); this._added = true; } - if (StringMapWrapper.contains(c, "model")) { + if (isPropertyUpdated(c, this.viewModel)) { this.form.updateValue(this.model); } } @@ -96,5 +96,8 @@ export class NgFormControl extends NgControl { get validator(): Function { return composeNgValidator(this.ngValidators); } - viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.update, newValue); } + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + ObservableWrapper.callNext(this.update, newValue); + } } diff --git a/modules/angular2/src/forms/directives/ng_model.ts b/modules/angular2/src/forms/directives/ng_model.ts index 4cadab3282..9e05aa7cbe 100644 --- a/modules/angular2/src/forms/directives/ng_model.ts +++ b/modules/angular2/src/forms/directives/ng_model.ts @@ -1,6 +1,5 @@ import {CONST_EXPR} from 'angular2/src/facade/lang'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; -import {StringMapWrapper} from 'angular2/src/facade/collection'; import {Directive, LifecycleEvent, QueryList, Query} from 'angular2/angular2'; import {forwardRef, Ancestor, Binding} from 'angular2/di'; @@ -8,7 +7,7 @@ import {forwardRef, Ancestor, Binding} from 'angular2/di'; import {NgControl} from './ng_control'; import {Control} from '../model'; import {NgValidator} from './validators'; -import {setUpControl, composeNgValidator} from './shared'; +import {setUpControl, composeNgValidator, isPropertyUpdated} from './shared'; const formControlBinding = CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgModel)})); @@ -41,6 +40,7 @@ export class NgModel extends NgControl { _added = false; update = new EventEmitter(); model: any; + viewModel: any; ngValidators: QueryList; // Scope the query once https://github.com/angular/angular/issues/2603 is fixed @@ -56,7 +56,7 @@ export class NgModel extends NgControl { this._added = true; } - if (StringMapWrapper.contains(c, "model")) { + if (isPropertyUpdated(c, this.viewModel)) { this._control.updateValue(this.model); } } @@ -67,5 +67,8 @@ export class NgModel extends NgControl { get validator(): Function { return composeNgValidator(this.ngValidators); } - viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.update, newValue); } + viewToModelUpdate(newValue: any): void { + this.viewModel = newValue; + ObservableWrapper.callNext(this.update, newValue); + } } diff --git a/modules/angular2/src/forms/directives/select_control_value_accessor.ts b/modules/angular2/src/forms/directives/select_control_value_accessor.ts index d6eac0e031..ca393bf40a 100644 --- a/modules/angular2/src/forms/directives/select_control_value_accessor.ts +++ b/modules/angular2/src/forms/directives/select_control_value_accessor.ts @@ -29,7 +29,6 @@ export class NgSelectOption { '(change)': 'onChange($event.target.value)', '(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()', - '[value]': 'value', '[class.ng-untouched]': 'ngClassUntouched', '[class.ng-touched]': 'ngClassTouched', '[class.ng-pristine]': 'ngClassPristine', @@ -39,7 +38,7 @@ export class NgSelectOption { } }) export class SelectControlValueAccessor implements ControlValueAccessor { - value = ''; + value: string; onChange = (_) => {}; onTouched = () => {}; @@ -51,8 +50,6 @@ export class SelectControlValueAccessor implements ControlValueAccessor { } writeValue(value) { - // both this.value and setProperty are required at the moment - // remove when a proper imperative API is provided this.value = value; setProperty(this.renderer, this.elementRef, "value", value); } diff --git a/modules/angular2/src/forms/directives/shared.ts b/modules/angular2/src/forms/directives/shared.ts index 76bd1e3d2a..36d2d8dedf 100644 --- a/modules/angular2/src/forms/directives/shared.ts +++ b/modules/angular2/src/forms/directives/shared.ts @@ -1,5 +1,5 @@ -import {ListWrapper, iterableToList} from 'angular2/src/facade/collection'; -import {isBlank, BaseException} from 'angular2/src/facade/lang'; +import {ListWrapper, iterableToList, StringMapWrapper} from 'angular2/src/facade/collection'; +import {isBlank, BaseException, looseIdentical} from 'angular2/src/facade/lang'; import {ControlContainer} from './control_container'; import {NgControl} from './ng_control'; @@ -26,7 +26,7 @@ export function setUpControl(c: Control, dir: NgControl) { // view -> model dir.valueAccessor.registerOnChange(newValue => { dir.viewToModelUpdate(newValue); - c.updateValue(newValue); + c.updateValue(newValue, {emitModelToViewChange: false}); c.markAsDirty(); }); @@ -52,3 +52,11 @@ export function setProperty(renderer: Renderer, elementRef: ElementRef, propName propValue: any) { renderer.setElementProperty(elementRef, propName, propValue); } + +export function isPropertyUpdated(changes: StringMap, viewModel: any): boolean { + if (!StringMapWrapper.contains(changes, "model")) return false; + var change = changes["model"]; + + if (change.isFirstChange()) return true; + return !looseIdentical(viewModel, change.currentValue); +} \ No newline at end of file diff --git a/modules/angular2/src/forms/model.ts b/modules/angular2/src/forms/model.ts index eaca30f389..15d11dc192 100644 --- a/modules/angular2/src/forms/model.ts +++ b/modules/angular2/src/forms/model.ts @@ -151,10 +151,13 @@ export class Control extends AbstractControl { this._valueChanges = new EventEmitter(); } - updateValue(value: any, {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): + updateValue(value: any, + {onlySelf, emitEvent, emitModelToViewChange}: + {onlySelf?: boolean, emitEvent?: boolean, emitModelToViewChange?: boolean} = {}): void { + emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true; this._value = value; - if (isPresent(this._onChange)) this._onChange(this._value); + if (isPresent(this._onChange) && emitModelToViewChange) this._onChange(this._value); this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent}); } diff --git a/modules/angular2/test/forms/integration_spec.ts b/modules/angular2/test/forms/integration_spec.ts index d97d1c33b3..06d5a823a8 100644 --- a/modules/angular2/test/forms/integration_spec.ts +++ b/modules/angular2/test/forms/integration_spec.ts @@ -721,6 +721,33 @@ export function main() { }); })); }); + + describe("ng-model corner cases", () => { + it("should not update the view when the value initially came from the view", + inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => { + var form = new Control(""); + + var t = + `
`; + var rootTC; + tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then( + (root) => { rootTC = root; }); + tick(); + rootTC.componentInstance.form = form; + rootTC.detectChanges(); + + var input = rootTC.query(By.css("input")).nativeElement; + input.value = "aa"; + input.selectionStart = 1; + dispatchEvent(input, "change"); + + tick(); + rootTC.detectChanges(); + + // selection start has not changed because we did not reset the value + expect(input.selectionStart).toEqual(1); + }))); + }); }); } diff --git a/modules/angular2/test/forms/model_spec.ts b/modules/angular2/test/forms/model_spec.ts index a8740183b1..a1b543af1a 100644 --- a/modules/angular2/test/forms/model_spec.ts +++ b/modules/angular2/test/forms/model_spec.ts @@ -71,6 +71,15 @@ export function main() { expect(onChange).toEqual(["invoked", "newValue"]); }); + it("should not invoke on change when explicitly specified", () => { + var onChange = null; + c.registerOnChange((v) => onChange = ["invoked", v]); + + c.updateValue("newValue", {emitModelToViewChange: false}); + + expect(onChange).toBeNull(); + }); + it("should update the parent", () => { c.updateValue("newValue"); expect(g.value).toEqual({"one": "newValue"});