feat(form): implemented an imperative way of updating the view by updating the value of a control

This commit is contained in:
vsavkin 2015-06-01 10:41:50 -07:00
parent 559f54e92b
commit 652ed0cf6d
9 changed files with 145 additions and 75 deletions

View File

@ -1,4 +1,5 @@
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, Ancestor, onChange} from 'angular2/angular2'; import {Directive, Ancestor, onChange} from 'angular2/angular2';
@ -53,20 +54,24 @@ const formControlBinding =
export class FormControlDirective extends ControlDirective { export class FormControlDirective extends ControlDirective {
control: Control; control: Control;
ngModel: EventEmitter; ngModel: EventEmitter;
_added: boolean;
model: any;
constructor() { constructor() {
super(); super();
this.ngModel = new EventEmitter(); this.ngModel = new EventEmitter();
this._added = false;
} }
onChange(_) { onChange(c) {
if (!this._added) {
setUpControl(this.control, this); setUpControl(this.control, this);
this.control.updateValidity(); this.control.updateValidity();
this._added = true;
}
if (StringMapWrapper.contains(c, "model")) {
this.control.updateValue(this.model);
} }
set model(value) {
this.control.updateValue(value);
this.valueAccessor.writeValue(value);
} }
viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); } viewToModelUpdate(newValue: any): void { ObservableWrapper.callNext(this.ngModel, newValue); }

View File

@ -89,8 +89,7 @@ export class FormModelDirective extends ControlContainerDirective implements For
updateModel(dir: ControlDirective, value: any): void { updateModel(dir: ControlDirective, value: any): void {
var c  = <Control>this.form.find(dir.path); var c  = <Control>this.form.find(dir.path);
c.value = value; c.updateValue(value);
dir.valueAccessor.writeValue(value);
} }
_updateDomValue() { _updateDomValue() {

View File

@ -40,8 +40,7 @@ export class NgModelDirective extends ControlDirective {
}; };
if (StringMapWrapper.contains(c, "model")) { if (StringMapWrapper.contains(c, "model")) {
this.control.value = this.model; this.control.updateValue(this.model);
this.valueAccessor.writeValue(this.model);
} }
} }

View File

@ -18,10 +18,15 @@ export function setUpControl(c: Control, dir: ControlDirective) {
c.validator = Validators.compose([c.validator, dir.validator]); c.validator = Validators.compose([c.validator, dir.validator]);
dir.valueAccessor.writeValue(c.value); dir.valueAccessor.writeValue(c.value);
// view -> model
dir.valueAccessor.registerOnChange(newValue => { dir.valueAccessor.registerOnChange(newValue => {
dir.viewToModelUpdate(newValue); dir.viewToModelUpdate(newValue);
c.updateValue(newValue); c.updateValue(newValue);
}); });
// model -> view
c.registerOnChange(newValue => dir.valueAccessor.writeValue(newValue));
} }
function _throwError(dir: ControlDirective, message: string): void { function _throwError(dir: ControlDirective, message: string): void {

View File

@ -65,8 +65,7 @@ export class TemplateDrivenFormDirective extends ControlContainerDirective imple
updateModel(dir: ControlDirective, value: any): void { updateModel(dir: ControlDirective, value: any): void {
this._later(_ => { this._later(_ => {
var c = <Control>this.form.find(dir.path); var c = <Control>this.form.find(dir.path);
c.value = value; c.updateValue(value);
dir.valueAccessor.writeValue(value);
}); });
} }

View File

@ -42,8 +42,6 @@ export class AbstractControl {
get value(): any { return this._value; } get value(): any { return this._value; }
set value(v) { this._value = v; }
get status(): string { return this._status; } get status(): string { return this._status; }
get valid(): boolean { return this._status === VALID; } get valid(): boolean { return this._status === VALID; }
@ -58,19 +56,36 @@ export class AbstractControl {
setParent(parent) { this._parent = parent; } setParent(parent) { this._parent = parent; }
_updateParent() { updateValidity({onlySelf}: {onlySelf?: boolean} = {}): void {
if (isPresent(this._parent)) { onlySelf = isPresent(onlySelf) ? onlySelf : false;
this._parent._updateValue();
this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID;
if (isPresent(this._parent) && !onlySelf) {
this._parent.updateValidity({onlySelf: onlySelf});
} }
} }
updateValidity() { updateValueAndValidity({onlySelf, emitEvent}: {onlySelf?: boolean,
emitEvent?: boolean} = {}): void {
onlySelf = isPresent(onlySelf) ? onlySelf : false;
emitEvent = isPresent(emitEvent) ? emitEvent : true;
this._updateValue();
this._pristine = false;
if (emitEvent) {
ObservableWrapper.callNext(this._valueChanges, this._value);
}
this._errors = this.validator(this); this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID; this._status = isPresent(this._errors) ? INVALID : VALID;
if (isPresent(this._parent)) { if (isPresent(this._parent) && !onlySelf) {
this._parent.updateValidity(); this._parent.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
} }
} }
_updateValue(): void {}
} }
/** /**
@ -83,26 +98,23 @@ export class AbstractControl {
* @exportedAs angular2/forms * @exportedAs angular2/forms
*/ */
export class Control extends AbstractControl { export class Control extends AbstractControl {
_onChange: Function;
constructor(value: any, validator: Function = Validators.nullValidator) { constructor(value: any, validator: Function = Validators.nullValidator) {
super(validator); super(validator);
this._setValueErrorsStatus(value); this._value = value;
this.updateValidity({onlySelf: true});
this._valueChanges = new EventEmitter(); this._valueChanges = new EventEmitter();
} }
updateValue(value: any): void { updateValue(value: any,
this._setValueErrorsStatus(value); {onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._pristine = false;
ObservableWrapper.callNext(this._valueChanges, this._value);
this._updateParent();
}
_setValueErrorsStatus(value) {
this._value = value; this._value = value;
this._errors = this.validator(this); if (isPresent(this._onChange)) this._onChange(this._value);
this._status = isPresent(this._errors) ? INVALID : VALID; this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
} }
registerOnChange(fn: Function): void { this._onChange = fn; }
} }
/** /**
@ -136,7 +148,8 @@ export class ControlGroup extends AbstractControl {
this._valueChanges = new EventEmitter(); this._valueChanges = new EventEmitter();
this._setParentForControls(); this._setParentForControls();
this._setValueErrorsStatus(); this._value = this._reduceValue();
this.updateValidity({onlySelf: true});
} }
addControl(name: string, c: AbstractControl) { this.controls[name] = c; } addControl(name: string, c: AbstractControl) { this.controls[name] = c; }
@ -145,12 +158,12 @@ export class ControlGroup extends AbstractControl {
include(controlName: string): void { include(controlName: string): void {
StringMapWrapper.set(this._optionals, controlName, true); StringMapWrapper.set(this._optionals, controlName, true);
this._updateValue(); this.updateValueAndValidity();
} }
exclude(controlName: string): void { exclude(controlName: string): void {
StringMapWrapper.set(this._optionals, controlName, false); StringMapWrapper.set(this._optionals, controlName, false);
this._updateValue(); this.updateValueAndValidity();
} }
contains(controlName: string): boolean { contains(controlName: string): boolean {
@ -174,20 +187,7 @@ export class ControlGroup extends AbstractControl {
StringMapWrapper.forEach(this.controls, (control, name) => { control.setParent(this); }); StringMapWrapper.forEach(this.controls, (control, name) => { control.setParent(this); });
} }
_updateValue() { _updateValue() { this._value = this._reduceValue(); }
this._setValueErrorsStatus();
this._pristine = false;
ObservableWrapper.callNext(this._valueChanges, this._value);
this._updateParent();
}
_setValueErrorsStatus() {
this._value = this._reduceValue();
this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID;
}
_reduceValue() { _reduceValue() {
return this._reduceChildren({}, (acc, control, name) => { return this._reduceChildren({}, (acc, control, name) => {
@ -239,7 +239,8 @@ export class ControlArray extends AbstractControl {
this._valueChanges = new EventEmitter(); this._valueChanges = new EventEmitter();
this._setParentForControls(); this._setParentForControls();
this._setValueErrorsStatus(); this._updateValue();
this.updateValidity({onlySelf: true});
} }
at(index: number): AbstractControl { return this.controls[index]; } at(index: number): AbstractControl { return this.controls[index]; }
@ -247,38 +248,25 @@ export class ControlArray extends AbstractControl {
push(control: AbstractControl): void { push(control: AbstractControl): void {
ListWrapper.push(this.controls, control); ListWrapper.push(this.controls, control);
control.setParent(this); control.setParent(this);
this._updateValue(); this.updateValueAndValidity();
} }
insert(index: number, control: AbstractControl): void { insert(index: number, control: AbstractControl): void {
ListWrapper.insert(this.controls, index, control); ListWrapper.insert(this.controls, index, control);
control.setParent(this); control.setParent(this);
this._updateValue(); this.updateValueAndValidity();
} }
removeAt(index: number): void { removeAt(index: number): void {
ListWrapper.removeAt(this.controls, index); ListWrapper.removeAt(this.controls, index);
this._updateValue(); this.updateValueAndValidity();
} }
get length(): number { return this.controls.length; } get length(): number { return this.controls.length; }
_updateValue() { _updateValue() { this._value = ListWrapper.map(this.controls, (c) => c.value); }
this._setValueErrorsStatus();
this._pristine = false;
ObservableWrapper.callNext(this._valueChanges, this._value);
this._updateParent();
}
_setParentForControls() { _setParentForControls() {
ListWrapper.forEach(this.controls, (control) => { control.setParent(this); }); ListWrapper.forEach(this.controls, (control) => { control.setParent(this); });
} }
_setValueErrorsStatus() {
this._value = ListWrapper.map(this.controls, (c) => c.value);
this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID;
}
} }

View File

@ -79,7 +79,7 @@ export function main() {
}); });
it("should write value to the DOM", () => { it("should write value to the DOM", () => {
formModel.find(["login"]).value = "initValue"; formModel.find(["login"]).updateValue("initValue");
form.addControl(loginControlDir); form.addControl(loginControlDir);
@ -104,7 +104,7 @@ export function main() {
it("should update dom values of all the directives", () => { it("should update dom values of all the directives", () => {
form.addControl(loginControlDir); form.addControl(loginControlDir);
formModel.find(["login"]).value = "new value"; formModel.find(["login"]).updateValue("new value");
form.onChange(null); form.onChange(null);
@ -180,7 +180,7 @@ export function main() {
expect(control.valid).toBe(true); expect(control.valid).toBe(true);
// this will add the required validator and recalculate the validity // this will add the required validator and recalculate the validity
controlDir.onChange(null); controlDir.onChange({});
expect(control.valid).toBe(false); expect(control.valid).toBe(false);
}); });

View File

@ -114,6 +114,30 @@ export function main() {
}); });
})); }));
it("should update DOM elements when updating the value of a control",
inject([TestBed, AsyncTestCompleter], (tb, async) => {
var login = new Control("oldValue");
var form = new ControlGroup({"login": login});
var ctx = MyComp.create({form: form});
var t = `<div [form-model]="form">
<input type="text" control="login">
</div>`;
tb.createView(MyComp, {context: ctx, html: t})
.then((view) => {
view.detectChanges();
login.updateValue("newValue");
view.detectChanges();
var input = view.querySelector("input");
expect(input.value).toEqual("newValue");
async.done();
});
}));
describe("different control types", () => { describe("different control types", () => {
it("should support <input type=text>", inject([TestBed, AsyncTestCompleter], (tb, async) => { it("should support <input type=text>", inject([TestBed, AsyncTestCompleter], (tb, async) => {
var ctx = MyComp.create({form: new ControlGroup({"text": new Control("old")})}); var ctx = MyComp.create({form: new ControlGroup({"text": new Control("old")})});

View File

@ -9,6 +9,9 @@ import {
afterEach, afterEach,
el, el,
AsyncTestCompleter, AsyncTestCompleter,
fakeAsync,
flushMicrotasks,
tick,
inject inject
} from 'angular2/test_lib'; } from 'angular2/test_lib';
import {ControlGroup, Control, ControlArray, Validators} from 'angular2/forms'; import {ControlGroup, Control, ControlArray, Validators} from 'angular2/forms';
@ -62,6 +65,54 @@ export function main() {
}); });
}); });
describe("updateValue", () => {
var g, c;
beforeEach(() => {
c = new Control("oldValue");
g = new ControlGroup({"one": c});
});
it("should update the value of the control", () => {
c.updateValue("newValue");
expect(c.value).toEqual("newValue");
});
it("should invoke onChange if it is present", () => {
var onChange;
c.registerOnChange((v) => onChange = ["invoked", v]);
c.updateValue("newValue");
expect(onChange).toEqual(["invoked", "newValue"]);
});
it("should update the parent", () => {
c.updateValue("newValue");
expect(g.value).toEqual({"one": "newValue"});
});
it("should not update the parent when explicitly specified", () => {
c.updateValue("newValue", {onlySelf: true});
expect(g.value).toEqual({"one": "oldValue"});
});
it("should fire an event", fakeAsync(() => {
ObservableWrapper.subscribe(c.valueChanges,
(value) => { expect(value).toEqual("newValue"); });
c.updateValue("newValue");
tick();
}));
it("should not fire an event when explicitly specified", fakeAsync(() => {
ObservableWrapper.subscribe(c.valueChanges, (value) => { throw "Should not happen"; });
c.updateValue("newValue", {emitEvent: false});
tick();
}));
});
describe("valueChanges", () => { describe("valueChanges", () => {
var c; var c;
@ -302,7 +353,7 @@ export function main() {
if (loggedValues.length == 2) { if (loggedValues.length == 2) {
expect(loggedValues) expect(loggedValues)
.toEqual([{"one": "new1", "two": "old2"}, {"one": "new1", "two": "new2"}]) .toEqual([{"one": "new1", "two": "old2"}, {"one": "new1", "two": "new2"}]);
async.done(); async.done();
} }
}); });