fix(forms): do not reset the value of the input when it came from the view

This commit is contained in:
vsavkin 2015-07-15 18:16:50 -07:00
parent b1df54501a
commit b1231593b6
11 changed files with 81 additions and 34 deletions

View File

@ -9,6 +9,8 @@ export var uninitialized = new Object();
export class SimpleChange { export class SimpleChange {
constructor(public previousValue: any, public currentValue: any) {} constructor(public previousValue: any, public currentValue: any) {}
isFirstChange(): boolean { return this.previousValue === uninitialized; }
} }
var _simpleChangesIndex = 0; var _simpleChangesIndex = 0;

View File

@ -21,7 +21,6 @@ import {setProperty} from './shared';
host: { host: {
'(change)': 'onChange($event.target.checked)', '(change)': 'onChange($event.target.checked)',
'(blur)': 'onTouched()', '(blur)': 'onTouched()',
'[checked]': 'checked',
'[class.ng-untouched]': 'ngClassUntouched', '[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'ngClassTouched', '[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'ngClassPristine', '[class.ng-pristine]': 'ngClassPristine',
@ -31,7 +30,6 @@ import {setProperty} from './shared';
} }
}) })
export class CheckboxControlValueAccessor implements ControlValueAccessor { export class CheckboxControlValueAccessor implements ControlValueAccessor {
checked: boolean;
onChange = (_) => {}; onChange = (_) => {};
onTouched = () => {}; onTouched = () => {};
@ -39,12 +37,7 @@ export class CheckboxControlValueAccessor implements ControlValueAccessor {
cd.valueAccessor = this; cd.valueAccessor = this;
} }
writeValue(value) { writeValue(value) { setProperty(this.renderer, this.elementRef, "checked", 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);
}
get ngClassUntouched(): boolean { get ngClassUntouched(): boolean {
return isPresent(this.cd.control) ? this.cd.control.untouched : false; return isPresent(this.cd.control) ? this.cd.control.untouched : false;

View File

@ -22,7 +22,6 @@ import {setProperty} from './shared';
'(change)': 'onChange($event.target.value)', '(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)', '(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()', '(blur)': 'onTouched()',
'[value]': 'value',
'[class.ng-untouched]': 'ngClassUntouched', '[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'ngClassTouched', '[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'ngClassPristine', '[class.ng-pristine]': 'ngClassPristine',
@ -32,8 +31,6 @@ import {setProperty} from './shared';
} }
}) })
export class DefaultValueAccessor implements ControlValueAccessor { export class DefaultValueAccessor implements ControlValueAccessor {
value: string = null;
onChange = (_) => {}; onChange = (_) => {};
onTouched = () => {}; onTouched = () => {};
@ -44,8 +41,8 @@ export class DefaultValueAccessor implements ControlValueAccessor {
writeValue(value) { writeValue(value) {
// both this.value and setProperty are required at the moment // both this.value and setProperty are required at the moment
// remove when a proper imperative API is provided // remove when a proper imperative API is provided
this.value = isBlank(value) ? '' : value; var normalizedValue = isBlank(value) ? '' : value;
setProperty(this.renderer, this.elementRef, 'value', this.value); setProperty(this.renderer, this.elementRef, 'value', normalizedValue);
} }
get ngClassUntouched(): boolean { get ngClassUntouched(): boolean {

View File

@ -1,6 +1,6 @@
import {CONST_EXPR} from 'angular2/src/facade/lang'; import {CONST_EXPR} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; 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 {Directive, LifecycleEvent, Query, QueryList} from 'angular2/angular2';
import {forwardRef, Ancestor, Binding, Inject} from 'angular2/di'; 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 {ControlContainer} from './control_container';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {NgValidator} from './validators'; import {NgValidator} from './validators';
import {controlPath, composeNgValidator} from './shared'; import {controlPath, composeNgValidator, isPropertyUpdated} from './shared';
import {Control} from '../model'; import {Control} from '../model';
const controlNameBinding = const controlNameBinding =
@ -82,6 +82,7 @@ export class NgControlName extends NgControl {
_parent: ControlContainer; _parent: ControlContainer;
update = new EventEmitter(); update = new EventEmitter();
model: any; model: any;
viewModel: any;
ngValidators: QueryList<NgValidator>; ngValidators: QueryList<NgValidator>;
_added = false; _added = false;
@ -98,14 +99,18 @@ export class NgControlName extends NgControl {
this.formDirective.addControl(this); this.formDirective.addControl(this);
this._added = true; this._added = true;
} }
if (StringMapWrapper.contains(c, "model")) { if (isPropertyUpdated(c, this.viewModel)) {
this.viewModel = this.model;
this.formDirective.updateModel(this, this.model); this.formDirective.updateModel(this, this.model);
} }
} }
onDestroy() { this.formDirective.removeControl(this); } 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<string> { return controlPath(this.name, this._parent); } get path(): List<string> { return controlPath(this.name, this._parent); }

View File

@ -1,5 +1,4 @@
import {CONST_EXPR} from 'angular2/src/facade/lang'; import {CONST_EXPR} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Directive, LifecycleEvent, Query, QueryList} from 'angular2/angular2'; 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 {NgControl} from './ng_control';
import {Control} from '../model'; import {Control} from '../model';
import {NgValidator} from './validators'; import {NgValidator} from './validators';
import {setUpControl, composeNgValidator} from './shared'; import {setUpControl, composeNgValidator, isPropertyUpdated} from './shared';
const formControlBinding = const formControlBinding =
CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgFormControl)})); CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgFormControl)}));
@ -71,6 +70,7 @@ export class NgFormControl extends NgControl {
update = new EventEmitter(); update = new EventEmitter();
_added = false; _added = false;
model: any; model: any;
viewModel: any;
ngValidators: QueryList<NgValidator>; ngValidators: QueryList<NgValidator>;
// Scope the query once https://github.com/angular/angular/issues/2603 is fixed // 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.form.updateValidity();
this._added = true; this._added = true;
} }
if (StringMapWrapper.contains(c, "model")) { if (isPropertyUpdated(c, this.viewModel)) {
this.form.updateValue(this.model); this.form.updateValue(this.model);
} }
} }
@ -96,5 +96,8 @@ export class NgFormControl extends NgControl {
get validator(): Function { return composeNgValidator(this.ngValidators); } 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);
}
} }

View File

@ -1,6 +1,5 @@
import {CONST_EXPR} from 'angular2/src/facade/lang'; import {CONST_EXPR} from 'angular2/src/facade/lang';
import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {Directive, LifecycleEvent, QueryList, Query} from 'angular2/angular2'; import {Directive, LifecycleEvent, QueryList, Query} from 'angular2/angular2';
import {forwardRef, Ancestor, Binding} from 'angular2/di'; import {forwardRef, Ancestor, Binding} from 'angular2/di';
@ -8,7 +7,7 @@ import {forwardRef, Ancestor, Binding} from 'angular2/di';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
import {Control} from '../model'; import {Control} from '../model';
import {NgValidator} from './validators'; 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)})); const formControlBinding = CONST_EXPR(new Binding(NgControl, {toAlias: forwardRef(() => NgModel)}));
@ -41,6 +40,7 @@ export class NgModel extends NgControl {
_added = false; _added = false;
update = new EventEmitter(); update = new EventEmitter();
model: any; model: any;
viewModel: any;
ngValidators: QueryList<NgValidator>; ngValidators: QueryList<NgValidator>;
// Scope the query once https://github.com/angular/angular/issues/2603 is fixed // 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; this._added = true;
} }
if (StringMapWrapper.contains(c, "model")) { if (isPropertyUpdated(c, this.viewModel)) {
this._control.updateValue(this.model); this._control.updateValue(this.model);
} }
} }
@ -67,5 +67,8 @@ export class NgModel extends NgControl {
get validator(): Function { return composeNgValidator(this.ngValidators); } 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);
}
} }

View File

@ -29,7 +29,6 @@ export class NgSelectOption {
'(change)': 'onChange($event.target.value)', '(change)': 'onChange($event.target.value)',
'(input)': 'onChange($event.target.value)', '(input)': 'onChange($event.target.value)',
'(blur)': 'onTouched()', '(blur)': 'onTouched()',
'[value]': 'value',
'[class.ng-untouched]': 'ngClassUntouched', '[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'ngClassTouched', '[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'ngClassPristine', '[class.ng-pristine]': 'ngClassPristine',
@ -39,7 +38,7 @@ export class NgSelectOption {
} }
}) })
export class SelectControlValueAccessor implements ControlValueAccessor { export class SelectControlValueAccessor implements ControlValueAccessor {
value = ''; value: string;
onChange = (_) => {}; onChange = (_) => {};
onTouched = () => {}; onTouched = () => {};
@ -51,8 +50,6 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
} }
writeValue(value) { writeValue(value) {
// both this.value and setProperty are required at the moment
// remove when a proper imperative API is provided
this.value = value; this.value = value;
setProperty(this.renderer, this.elementRef, "value", value); setProperty(this.renderer, this.elementRef, "value", value);
} }

View File

@ -1,5 +1,5 @@
import {ListWrapper, iterableToList} from 'angular2/src/facade/collection'; import {ListWrapper, iterableToList, StringMapWrapper} from 'angular2/src/facade/collection';
import {isBlank, BaseException} from 'angular2/src/facade/lang'; import {isBlank, BaseException, looseIdentical} from 'angular2/src/facade/lang';
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
import {NgControl} from './ng_control'; import {NgControl} from './ng_control';
@ -26,7 +26,7 @@ export function setUpControl(c: Control, dir: NgControl) {
// view -> model // view -> model
dir.valueAccessor.registerOnChange(newValue => { dir.valueAccessor.registerOnChange(newValue => {
dir.viewToModelUpdate(newValue); dir.viewToModelUpdate(newValue);
c.updateValue(newValue); c.updateValue(newValue, {emitModelToViewChange: false});
c.markAsDirty(); c.markAsDirty();
}); });
@ -52,3 +52,11 @@ export function setProperty(renderer: Renderer, elementRef: ElementRef, propName
propValue: any) { propValue: any) {
renderer.setElementProperty(elementRef, propName, propValue); renderer.setElementProperty(elementRef, propName, propValue);
} }
export function isPropertyUpdated(changes: StringMap<string, any>, viewModel: any): boolean {
if (!StringMapWrapper.contains(changes, "model")) return false;
var change = changes["model"];
if (change.isFirstChange()) return true;
return !looseIdentical(viewModel, change.currentValue);
}

View File

@ -151,10 +151,13 @@ export class Control extends AbstractControl {
this._valueChanges = new EventEmitter(); 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 { void {
emitModelToViewChange = isPresent(emitModelToViewChange) ? emitModelToViewChange : true;
this._value = value; 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}); this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
} }

View File

@ -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 =
`<div><input type="text" [ng-form-control]="form" [(ng-model)]="name"></div>`;
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);
})));
});
}); });
} }

View File

@ -71,6 +71,15 @@ export function main() {
expect(onChange).toEqual(["invoked", "newValue"]); 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", () => { it("should update the parent", () => {
c.updateValue("newValue"); c.updateValue("newValue");
expect(g.value).toEqual({"one": "newValue"}); expect(g.value).toEqual({"one": "newValue"});