From 4b247348556b029a2310b76db55b0bd54b7e320e Mon Sep 17 00:00:00 2001 From: vsavkin Date: Sat, 7 Feb 2015 14:14:07 -0800 Subject: [PATCH] feat(forms): add support for checkbox --- modules/angular2/src/forms/directives.js | 134 +++++++++++++----- .../angular2/test/forms/integration_spec.js | 92 +++++++++--- 2 files changed, 169 insertions(+), 57 deletions(-) diff --git a/modules/angular2/src/forms/directives.js b/modules/angular2/src/forms/directives.js index 9afd196caa..a037d4951c 100644 --- a/modules/angular2/src/forms/directives.js +++ b/modules/angular2/src/forms/directives.js @@ -1,7 +1,7 @@ -import {TemplateConfig, Component, Decorator, NgElement, Ancestor} from 'angular2/core'; +import {TemplateConfig, Component, Decorator, NgElement, Ancestor, onChange} from 'angular2/core'; import {DOM} from 'angular2/src/facade/dom'; -import {isBlank, isPresent} from 'angular2/src/facade/lang'; -import {ListWrapper} from 'angular2/src/facade/collection'; +import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang'; +import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {ControlGroup, Control} from './model'; class ControlGroupDirectiveBase { @@ -9,73 +9,126 @@ class ControlGroupDirectiveBase { findControl(name:string):Control { return null; } } -export class ControlDirectiveBase { - _groupDecorator:ControlGroupDirectiveBase; - _el:NgElement; - _controlName:string; +@CONST() +export class ControlValueAccessor { + readValue(el){} + writeValue(el, value):void {} +} - constructor(groupDecorator, el:NgElement) { - this._groupDecorator = groupDecorator; - this._el = el; - DOM.on(el.domElement, "change", (_) => this._updateControl()); +@CONST() +class DefaultControlValueAccessor extends ControlValueAccessor { + constructor() { + super(); } - set controlName(name:string) { - this._controlName = name; - this._groupDecorator.addDirective(this); - this._updateDOM(); + readValue(el) { + return el.value; } - get controlName() { - return this._controlName; + writeValue(el, value):void { + el.value = value; + } +} + +@CONST() +class CheckboxControlValueAccessor extends ControlValueAccessor { + constructor() { + super(); } - //TODO:vsavkin: Remove it once change detection lifecycle callbacks are available - isInitialized():boolean { - return isPresent(this._controlName); + readValue(el):boolean { + return el.checked; } - _updateDOM() { - // remove it once all DOM write go through a queue - if (this.isInitialized()) { - var inputElement:any = this._el.domElement; - inputElement.value = this._control().value; - } + writeValue(el, value:boolean):void { + el.checked = value; } +} - _updateControl() { - var inputElement:any = this._el.domElement; - this._control().value = inputElement.value; - } +var controlValueAccessors = { + "checkbox" : new CheckboxControlValueAccessor(), + "text" : new DefaultControlValueAccessor() +}; - _control() { - return this._groupDecorator.findControl(this._controlName); +function controlValueAccessorFor(controlType:string):ControlValueAccessor { + var accessor = StringMapWrapper.get(controlValueAccessors, controlType); + if (isPresent(accessor)) { + return accessor; + } else { + return StringMapWrapper.get(controlValueAccessors, "text"); } } +export class ControlDirectiveBase { + _groupDecorator:ControlGroupDirectiveBase; + _el:NgElement; + + controlName:string; + type:string; + valueAccessor:ControlValueAccessor; + + constructor(groupDecorator, el:NgElement) { + this._groupDecorator = groupDecorator; + this._el = el; + } + + _initialize() { + if (isBlank(this.valueAccessor)) { + this.valueAccessor = controlValueAccessorFor(this.type); + } + this._groupDecorator.addDirective(this); + this._updateDomValue(); + DOM.on(this._el.domElement, "change", (_) => this._updateControlValue()); + } + + _updateDomValue() { + this.valueAccessor.writeValue(this._el.domElement, this._control().value); + } + + _updateControlValue() { + this._control().value = this.valueAccessor.readValue(this._el.domElement); + } + + _control() { + return this._groupDecorator.findControl(this.controlName); + } +} + @Decorator({ + lifecycle: [onChange], selector: '[control-name]', bind: { - 'control-name' : 'controlName' + 'control-name' : 'controlName', + 'type' : 'type' } }) export class ControlNameDirective extends ControlDirectiveBase { constructor(@Ancestor() groupDecorator:ControlGroupDirective, el:NgElement) { super(groupDecorator, el); } + + onChange(_) { + this._initialize(); + } } @Decorator({ + lifecycle: [onChange], selector: '[control]', bind: { - 'control' : 'controlName' + 'control' : 'controlName', + 'type' : 'type' } }) export class ControlDirective extends ControlDirectiveBase { constructor(@Ancestor() groupDecorator:NewControlGroupDirective, el:NgElement) { super(groupDecorator, el); } + + onChange(_) { + this._initialize(); + } } @Decorator({ @@ -95,7 +148,7 @@ export class ControlGroupDirective extends ControlGroupDirectiveBase { set controlGroup(controlGroup:ControlGroup) { this._controlGroup = controlGroup; - ListWrapper.forEach(this._directives, (cd) => cd._updateDOM()); + ListWrapper.forEach(this._directives, (cd) => cd._updateDomValue()); } addDirective(c:ControlNameDirective) { @@ -144,10 +197,8 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase { _createControlGroup():ControlGroup { var controls = ListWrapper.reduce(this._directives, (memo, cd) => { - if (cd.isInitialized()) { - var initControlValue = this._initData[cd.controlName]; - memo[cd.controlName] = new Control(initControlValue); - } + var initControlValue = this._initData[cd.controlName]; + memo[cd.controlName] = new Control(initControlValue); return memo; }, {}); return new ControlGroup(controls); @@ -157,3 +208,8 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase { return this._controlGroup.value; } } + +export var FormDirectives = [ + ControlGroupDirective, ControlNameDirective, + ControlDirective, NewControlGroupDirective +]; diff --git a/modules/angular2/test/forms/integration_spec.js b/modules/angular2/test/forms/integration_spec.js index 1884e3bdaf..013c32f945 100644 --- a/modules/angular2/test/forms/integration_spec.js +++ b/modules/angular2/test/forms/integration_spec.js @@ -8,9 +8,10 @@ import {NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_str import {Injector} from 'angular2/di'; import {DOM} from 'angular2/src/facade/dom'; -import {Component, TemplateConfig} from 'angular2/core'; -import {ControlDirective, ControlNameDirective, ControlGroupDirective, NewControlGroupDirective, - Control, ControlGroup} from 'angular2/forms'; +import {Component, Decorator, TemplateConfig} from 'angular2/core'; +import {ControlGroupDirective, ControlNameDirective, + ControlDirective, NewControlGroupDirective, + Control, ControlGroup, ControlValueAccessor} from 'angular2/forms'; import {TemplateLoader} from 'angular2/src/core/compiler/template_loader'; import {XHRMock} from 'angular2/src/mock/xhr_mock'; @@ -36,11 +37,6 @@ export function main() { }); } - function formComponent(view) { - // TODO: vsavkin remove when view variables work - return view.elementInjectors[0].getComponent(); - } - describe("integration tests", () => { it("should initialize DOM elements with the given form object", (done) => { var ctx = new MyComp(new ControlGroup({ @@ -48,7 +44,7 @@ export function main() { })); var t = `
- +
`; compile(MyComp, t, ctx, (view) => { @@ -65,7 +61,7 @@ export function main() { var ctx = new MyComp(form); var t = `
- +
`; compile(MyComp, t, ctx, (view) => { @@ -86,7 +82,7 @@ export function main() { var ctx = new MyComp(form); var t = `
- +
`; compile(MyComp, t, ctx, (view) => { @@ -108,7 +104,7 @@ export function main() { }), "one"); var t = `
- +
`; compile(MyComp, t, ctx, (view) => { @@ -123,11 +119,51 @@ export function main() { }); }); + describe("different control types", () => { + it("should support type=checkbox", (done) => { + var ctx = new MyComp(new ControlGroup({"checkbox": new Control(true)})); + + var t = `
+ +
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + expect(input.checked).toBe(true); + + input.checked = false; + dispatchEvent(input, "change"); + + expect(ctx.form.value).toEqual({"checkbox" : false}); + done(); + }); + }); + + it("should support custom value accessors", (done) => { + var ctx = new MyComp(new ControlGroup({"name": new Control("aa")})); + + var t = `
+ +
`; + + compile(MyComp, t, ctx, (view) => { + var input = queryView(view, "input") + expect(input.value).toEqual("!aa!"); + + input.value = "!bb!"; + dispatchEvent(input, "change"); + + expect(ctx.form.value).toEqual({"name" : "bb"}); + done(); + }); + }); + }); + describe("declarative forms", () => { it("should initialize dom elements", (done) => { var t = `
- - + +
`; compile(MyComp, t, new MyComp(), (view) => { @@ -142,8 +178,8 @@ export function main() { }); it("should update the control group values on DOM change", (done) => { - var t = `
- + var t = `
+
`; compile(MyComp, t, new MyComp(), (view) => { @@ -152,7 +188,8 @@ export function main() { input.value = "updatedValue"; dispatchEvent(input, "change"); - expect(formComponent(view).value).toEqual({'login': 'updatedValue'}); + var form = view.contextWithLocals.get("form"); + expect(form.value).toEqual({'login': 'updatedValue'}); done(); }); }); @@ -166,7 +203,7 @@ export function main() { template: new TemplateConfig({ inline: "", directives: [ControlGroupDirective, ControlNameDirective, - ControlDirective, NewControlGroupDirective] + ControlDirective, NewControlGroupDirective, WrappedValue] }) }) class MyComp { @@ -178,3 +215,22 @@ class MyComp { this.name = name; } } + +class WrappedValueAccessor extends ControlValueAccessor { + readValue(el){ + return el.value.substring(1, el.value.length - 1); + } + + writeValue(el, value):void { + el.value = `!${value}!`; + } +} + +@Decorator({ + selector:'[wrapped-value]' +}) +class WrappedValue { + constructor(cd:ControlNameDirective) { + cd.valueAccessor = new WrappedValueAccessor(); + } +} \ No newline at end of file