Skip to content

Commit 4b24734

Browse files
committed
feat(forms): add support for checkbox
1 parent 74f92c6 commit 4b24734

File tree

2 files changed

+165
-53
lines changed

2 files changed

+165
-53
lines changed

modules/angular2/src/forms/directives.js

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,134 @@
1-
import {TemplateConfig, Component, Decorator, NgElement, Ancestor} from 'angular2/core';
1+
import {TemplateConfig, Component, Decorator, NgElement, Ancestor, onChange} from 'angular2/core';
22
import {DOM} from 'angular2/src/facade/dom';
3-
import {isBlank, isPresent} from 'angular2/src/facade/lang';
4-
import {ListWrapper} from 'angular2/src/facade/collection';
3+
import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang';
4+
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
55
import {ControlGroup, Control} from './model';
66

77
class ControlGroupDirectiveBase {
88
addDirective(directive):void {}
99
findControl(name:string):Control { return null; }
1010
}
1111

12+
@CONST()
13+
export class ControlValueAccessor {
14+
readValue(el){}
15+
writeValue(el, value):void {}
16+
}
17+
18+
@CONST()
19+
class DefaultControlValueAccessor extends ControlValueAccessor {
20+
constructor() {
21+
super();
22+
}
23+
24+
readValue(el) {
25+
return el.value;
26+
}
27+
28+
writeValue(el, value):void {
29+
el.value = value;
30+
}
31+
}
32+
33+
@CONST()
34+
class CheckboxControlValueAccessor extends ControlValueAccessor {
35+
constructor() {
36+
super();
37+
}
38+
39+
readValue(el):boolean {
40+
return el.checked;
41+
}
42+
43+
writeValue(el, value:boolean):void {
44+
el.checked = value;
45+
}
46+
}
47+
48+
var controlValueAccessors = {
49+
"checkbox" : new CheckboxControlValueAccessor(),
50+
"text" : new DefaultControlValueAccessor()
51+
};
52+
53+
function controlValueAccessorFor(controlType:string):ControlValueAccessor {
54+
var accessor = StringMapWrapper.get(controlValueAccessors, controlType);
55+
if (isPresent(accessor)) {
56+
return accessor;
57+
} else {
58+
return StringMapWrapper.get(controlValueAccessors, "text");
59+
}
60+
}
61+
62+
1263
export class ControlDirectiveBase {
1364
_groupDecorator:ControlGroupDirectiveBase;
1465
_el:NgElement;
15-
_controlName:string;
66+
67+
controlName:string;
68+
type:string;
69+
valueAccessor:ControlValueAccessor;
1670

1771
constructor(groupDecorator, el:NgElement) {
1872
this._groupDecorator = groupDecorator;
1973
this._el = el;
20-
DOM.on(el.domElement, "change", (_) => this._updateControl());
2174
}
2275

23-
set controlName(name:string) {
24-
this._controlName = name;
76+
_initialize() {
77+
if (isBlank(this.valueAccessor)) {
78+
this.valueAccessor = controlValueAccessorFor(this.type);
79+
}
2580
this._groupDecorator.addDirective(this);
26-
this._updateDOM();
81+
this._updateDomValue();
82+
DOM.on(this._el.domElement, "change", (_) => this._updateControlValue());
2783
}
2884

29-
get controlName() {
30-
return this._controlName;
85+
_updateDomValue() {
86+
this.valueAccessor.writeValue(this._el.domElement, this._control().value);
3187
}
3288

33-
//TODO:vsavkin: Remove it once change detection lifecycle callbacks are available
34-
isInitialized():boolean {
35-
return isPresent(this._controlName);
36-
}
37-
38-
_updateDOM() {
39-
// remove it once all DOM write go through a queue
40-
if (this.isInitialized()) {
41-
var inputElement:any = this._el.domElement;
42-
inputElement.value = this._control().value;
43-
}
44-
}
45-
46-
_updateControl() {
47-
var inputElement:any = this._el.domElement;
48-
this._control().value = inputElement.value;
89+
_updateControlValue() {
90+
this._control().value = this.valueAccessor.readValue(this._el.domElement);
4991
}
5092

5193
_control() {
52-
return this._groupDecorator.findControl(this._controlName);
94+
return this._groupDecorator.findControl(this.controlName);
5395
}
5496
}
5597

56-
5798
@Decorator({
99+
lifecycle: [onChange],
58100
selector: '[control-name]',
59101
bind: {
60-
'control-name' : 'controlName'
102+
'control-name' : 'controlName',
103+
'type' : 'type'
61104
}
62105
})
63106
export class ControlNameDirective extends ControlDirectiveBase {
64107
constructor(@Ancestor() groupDecorator:ControlGroupDirective, el:NgElement) {
65108
super(groupDecorator, el);
66109
}
110+
111+
onChange(_) {
112+
this._initialize();
113+
}
67114
}
68115

69116
@Decorator({
117+
lifecycle: [onChange],
70118
selector: '[control]',
71119
bind: {
72-
'control' : 'controlName'
120+
'control' : 'controlName',
121+
'type' : 'type'
73122
}
74123
})
75124
export class ControlDirective extends ControlDirectiveBase {
76125
constructor(@Ancestor() groupDecorator:NewControlGroupDirective, el:NgElement) {
77126
super(groupDecorator, el);
78127
}
128+
129+
onChange(_) {
130+
this._initialize();
131+
}
79132
}
80133

81134
@Decorator({
@@ -95,7 +148,7 @@ export class ControlGroupDirective extends ControlGroupDirectiveBase {
95148

96149
set controlGroup(controlGroup:ControlGroup) {
97150
this._controlGroup = controlGroup;
98-
ListWrapper.forEach(this._directives, (cd) => cd._updateDOM());
151+
ListWrapper.forEach(this._directives, (cd) => cd._updateDomValue());
99152
}
100153

101154
addDirective(c:ControlNameDirective) {
@@ -144,10 +197,8 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase {
144197

145198
_createControlGroup():ControlGroup {
146199
var controls = ListWrapper.reduce(this._directives, (memo, cd) => {
147-
if (cd.isInitialized()) {
148-
var initControlValue = this._initData[cd.controlName];
149-
memo[cd.controlName] = new Control(initControlValue);
150-
}
200+
var initControlValue = this._initData[cd.controlName];
201+
memo[cd.controlName] = new Control(initControlValue);
151202
return memo;
152203
}, {});
153204
return new ControlGroup(controls);
@@ -157,3 +208,8 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase {
157208
return this._controlGroup.value;
158209
}
159210
}
211+
212+
export var FormDirectives = [
213+
ControlGroupDirective, ControlNameDirective,
214+
ControlDirective, NewControlGroupDirective
215+
];

modules/angular2/test/forms/integration_spec.js

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {NativeShadowDomStrategy} from 'angular2/src/core/compiler/shadow_dom_str
88
import {Injector} from 'angular2/di';
99
import {DOM} from 'angular2/src/facade/dom';
1010

11-
import {Component, TemplateConfig} from 'angular2/core';
12-
import {ControlDirective, ControlNameDirective, ControlGroupDirective, NewControlGroupDirective,
13-
Control, ControlGroup} from 'angular2/forms';
11+
import {Component, Decorator, TemplateConfig} from 'angular2/core';
12+
import {ControlGroupDirective, ControlNameDirective,
13+
ControlDirective, NewControlGroupDirective,
14+
Control, ControlGroup, ControlValueAccessor} from 'angular2/forms';
1415

1516
import {TemplateLoader} from 'angular2/src/core/compiler/template_loader';
1617
import {XHRMock} from 'angular2/src/mock/xhr_mock';
@@ -36,19 +37,14 @@ export function main() {
3637
});
3738
}
3839

39-
function formComponent(view) {
40-
// TODO: vsavkin remove when view variables work
41-
return view.elementInjectors[0].getComponent();
42-
}
43-
4440
describe("integration tests", () => {
4541
it("should initialize DOM elements with the given form object", (done) => {
4642
var ctx = new MyComp(new ControlGroup({
4743
"login": new Control("loginValue")
4844
}));
4945

5046
var t = `<div [control-group]="form">
51-
<input [control-name]="'login'">
47+
<input type="text" control-name="login">
5248
</div>`;
5349

5450
compile(MyComp, t, ctx, (view) => {
@@ -65,7 +61,7 @@ export function main() {
6561
var ctx = new MyComp(form);
6662

6763
var t = `<div [control-group]="form">
68-
<input [control-name]="'login'">
64+
<input type="text" control-name="login">
6965
</div>`;
7066

7167
compile(MyComp, t, ctx, (view) => {
@@ -86,7 +82,7 @@ export function main() {
8682
var ctx = new MyComp(form);
8783

8884
var t = `<div [control-group]="form">
89-
<input [control-name]="'login'">
85+
<input type="text" control-name="login">
9086
</div>`;
9187

9288
compile(MyComp, t, ctx, (view) => {
@@ -108,7 +104,7 @@ export function main() {
108104
}), "one");
109105

110106
var t = `<div [control-group]="form">
111-
<input [control-name]="name">
107+
<input type="text" [control-name]="name">
112108
</div>`;
113109

114110
compile(MyComp, t, ctx, (view) => {
@@ -123,11 +119,51 @@ export function main() {
123119
});
124120
});
125121

122+
describe("different control types", () => {
123+
it("should support type=checkbox", (done) => {
124+
var ctx = new MyComp(new ControlGroup({"checkbox": new Control(true)}));
125+
126+
var t = `<div [control-group]="form">
127+
<input type="checkbox" control-name="checkbox">
128+
</div>`;
129+
130+
compile(MyComp, t, ctx, (view) => {
131+
var input = queryView(view, "input")
132+
expect(input.checked).toBe(true);
133+
134+
input.checked = false;
135+
dispatchEvent(input, "change");
136+
137+
expect(ctx.form.value).toEqual({"checkbox" : false});
138+
done();
139+
});
140+
});
141+
142+
it("should support custom value accessors", (done) => {
143+
var ctx = new MyComp(new ControlGroup({"name": new Control("aa")}));
144+
145+
var t = `<div [control-group]="form">
146+
<input type="text" control-name="name" wrapped-value>
147+
</div>`;
148+
149+
compile(MyComp, t, ctx, (view) => {
150+
var input = queryView(view, "input")
151+
expect(input.value).toEqual("!aa!");
152+
153+
input.value = "!bb!";
154+
dispatchEvent(input, "change");
155+
156+
expect(ctx.form.value).toEqual({"name" : "bb"});
157+
done();
158+
});
159+
});
160+
});
161+
126162
describe("declarative forms", () => {
127163
it("should initialize dom elements", (done) => {
128164
var t = `<div [new-control-group]="{'login': 'loginValue', 'password':'passValue'}">
129-
<input id="login" [control]="'login'">
130-
<input id="password" [control]="'password'">
165+
<input type="text" id="login" control="login">
166+
<input type="password" id="password" control="password">
131167
</div>`;
132168

133169
compile(MyComp, t, new MyComp(), (view) => {
@@ -142,8 +178,8 @@ export function main() {
142178
});
143179

144180
it("should update the control group values on DOM change", (done) => {
145-
var t = `<div [new-control-group]="{'login': 'loginValue'}">
146-
<input [control]="'login'">
181+
var t = `<div #form [new-control-group]="{'login': 'loginValue'}">
182+
<input type="text" control="login">
147183
</div>`;
148184

149185
compile(MyComp, t, new MyComp(), (view) => {
@@ -152,7 +188,8 @@ export function main() {
152188
input.value = "updatedValue";
153189
dispatchEvent(input, "change");
154190

155-
expect(formComponent(view).value).toEqual({'login': 'updatedValue'});
191+
var form = view.contextWithLocals.get("form");
192+
expect(form.value).toEqual({'login': 'updatedValue'});
156193
done();
157194
});
158195
});
@@ -166,7 +203,7 @@ export function main() {
166203
template: new TemplateConfig({
167204
inline: "",
168205
directives: [ControlGroupDirective, ControlNameDirective,
169-
ControlDirective, NewControlGroupDirective]
206+
ControlDirective, NewControlGroupDirective, WrappedValue]
170207
})
171208
})
172209
class MyComp {
@@ -178,3 +215,22 @@ class MyComp {
178215
this.name = name;
179216
}
180217
}
218+
219+
class WrappedValueAccessor extends ControlValueAccessor {
220+
readValue(el){
221+
return el.value.substring(1, el.value.length - 1);
222+
}
223+
224+
writeValue(el, value):void {
225+
el.value = `!${value}!`;
226+
}
227+
}
228+
229+
@Decorator({
230+
selector:'[wrapped-value]'
231+
})
232+
class WrappedValue {
233+
constructor(cd:ControlNameDirective) {
234+
cd.valueAccessor = new WrappedValueAccessor();
235+
}
236+
}

0 commit comments

Comments
 (0)