feat(form): implemented an imperative way of updating the view by updating the value of a control
This commit is contained in:
parent
559f54e92b
commit
652ed0cf6d
|
@ -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); }
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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")})});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue