fix(forms): do not reset the value of the input when it came from the view
This commit is contained in:
parent
b1df54501a
commit
b1231593b6
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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); }
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
})));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"});
|
||||||
|
|
Loading…
Reference in New Issue