Skip to content

Commit ded83e5

Browse files
committed
feat(forms): add support for validations
1 parent 65ebff0 commit ded83e5

File tree

10 files changed

+283
-8
lines changed

10 files changed

+283
-8
lines changed

modules/angular2/angular2.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
export * from './change_detection';
55
export * from './core';
66
export * from './directives';
7+
export * from './forms';

modules/angular2/forms.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * from './src/forms/model';
22
export * from './src/forms/directives';
3+
export * from './src/forms/validators';
4+
export * from './src/forms/validator_directives';

modules/angular2/src/facade/collection.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ class StringMapWrapper {
6767
static void forEach(Map m, fn(v, k)) {
6868
m.forEach((k, v) => fn(v, k));
6969
}
70+
static HashMap merge(HashMap a, HashMap b) {
71+
var m = {};
72+
73+
a.forEach((k, v) => m[k] = v);
74+
b.forEach((k, v) => m[k] = v);
75+
76+
return m;
77+
}
7078
static bool isEmpty(Map m) => m.isEmpty;
7179
}
7280

modules/angular2/src/forms/directives.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {DOM} from 'angular2/src/facade/dom';
33
import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang';
44
import {StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
55
import {ControlGroup, Control} from './model';
6+
import * as validators from './validators';
67

78
class ControlGroupDirectiveBase {
89
addDirective(directive):void {}
@@ -68,16 +69,25 @@ export class ControlDirectiveBase {
6869
type:string;
6970
valueAccessor:ControlValueAccessor;
7071

72+
validator:Function;
73+
7174
constructor(groupDecorator, el:NgElement) {
7275
this._groupDecorator = groupDecorator;
7376
this._el = el;
77+
this.validator = validators.nullValidator;
7478
}
7579

7680
_initialize() {
81+
this._groupDecorator.addDirective(this);
82+
83+
if (isPresent(this.validator)) {
84+
var c = this._control();
85+
c.validator = validators.compose([c.validator, this.validator]);
86+
}
87+
7788
if (isBlank(this.valueAccessor)) {
7889
this.valueAccessor = controlValueAccessorFor(this.type);
7990
}
80-
this._groupDecorator.addDirective(this);
8191
this._updateDomValue();
8292
DOM.on(this._el.domElement, "change", (_) => this._updateControlValue());
8393
}
@@ -87,7 +97,7 @@ export class ControlDirectiveBase {
8797
}
8898

8999
_updateControlValue() {
90-
this._control().value = this.valueAccessor.readValue(this._el.domElement);
100+
this._control().updateValue(this.valueAccessor.readValue(this._el.domElement));
91101
}
92102

93103
_control() {
@@ -205,6 +215,14 @@ export class NewControlGroupDirective extends ControlGroupDirectiveBase {
205215
get value() {
206216
return this._controlGroup.value;
207217
}
218+
219+
get errors() {
220+
return this._controlGroup.errors;
221+
}
222+
223+
get valid() {
224+
return this._controlGroup.valid;
225+
}
208226
}
209227

210228
export var FormDirectives = [
Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,56 @@
1-
import {StringMapWrapper, StringMap} from 'angular2/src/facade/collection';
1+
import {isPresent} from 'angular2/src/facade/lang';
2+
import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
3+
import {nullValidator, controlGroupValidator} from './validators';
4+
5+
export const VALID = "VALID";
6+
export const INVALID = "INVALID";
27

38
export class Control {
49
value:any;
10+
validator:Function;
11+
status:string;
12+
errors;
13+
_parent:ControlGroup;
514

6-
constructor(value:any) {
15+
constructor(value:any, validator:Function = nullValidator) {
716
this.value = value;
17+
this.validator = validator;
18+
this._updateStatus();
19+
}
20+
21+
updateValue(value:any) {
22+
this.value = value;
23+
this._updateStatus();
24+
this._updateParent();
25+
}
26+
27+
get valid() {
28+
return this.status === VALID;
29+
}
30+
31+
_updateStatus() {
32+
this.errors = this.validator(this);
33+
this.status = isPresent(this.errors) ? INVALID : VALID;
34+
}
35+
36+
_updateParent() {
37+
if (isPresent(this._parent)){
38+
this._parent._controlChanged();
39+
}
840
}
941
}
1042

1143
export class ControlGroup {
12-
controls: StringMap;
44+
controls;
45+
validator:Function;
46+
status:string;
47+
errors;
1348

14-
constructor(controls:StringMap) {
49+
constructor(controls, validator:Function = controlGroupValidator) {
1550
this.controls = controls;
51+
this.validator = validator;
52+
this._setParentForControls();
53+
this._updateStatus();
1654
}
1755

1856
get value() {
@@ -22,4 +60,23 @@ export class ControlGroup {
2260
});
2361
return res;
2462
}
63+
64+
get valid() {
65+
return this.status === VALID;
66+
}
67+
68+
_setParentForControls() {
69+
StringMapWrapper.forEach(this.controls, (control, name) => {
70+
control._parent = this;
71+
});
72+
}
73+
74+
_updateStatus() {
75+
this.errors = this.validator(this);
76+
this.status = isPresent(this.errors) ? INVALID : VALID;
77+
}
78+
79+
_controlChanged() {
80+
this._updateStatus();
81+
}
2582
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
2+
import {List, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
3+
import {Decorator} from 'angular2/core';
4+
5+
import {ControlGroup, Control, ControlDirective} from 'angular2/forms';
6+
import * as validators from 'angular2/forms';
7+
8+
@Decorator({
9+
selector: '[required]'
10+
})
11+
export class RequiredValidatorDirective {
12+
constructor(c:ControlDirective) {
13+
c.validator = validators.compose([c.validator, validators.required]);
14+
}
15+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {isBlank, isPresent} from 'angular2/src/facade/lang';
2+
import {List, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
3+
4+
import {ControlGroup, Control} from 'angular2/forms';
5+
6+
export function required(c:Control) {
7+
return isBlank(c.value) || c.value === "" ? {"required" : true} : null;
8+
}
9+
10+
export function nullValidator(c:Control) {
11+
return null;
12+
}
13+
14+
export function compose(validators:List<Function>):Function {
15+
return function(c:Control) {
16+
return ListWrapper.reduce(validators, (res, validator) => {
17+
var errors = validator(c);
18+
return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res;
19+
}, {});
20+
}
21+
}
22+
23+
export function controlGroupValidator(c:ControlGroup) {
24+
var res = {};
25+
StringMapWrapper.forEach(c.controls, (control, name) => {
26+
if (isPresent(control.errors)) {
27+
res[name] = control.errors;
28+
}
29+
});
30+
return StringMapWrapper.isEmpty(res) ? null : res;
31+
}

modules/angular2/test/forms/integration_spec.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {Injector} from 'angular2/di';
1717
import {Component, Decorator, Template} from 'angular2/core';
1818
import {ControlGroupDirective, ControlNameDirective,
1919
ControlDirective, NewControlGroupDirective,
20-
Control, ControlGroup, ControlValueAccessor} from 'angular2/forms';
20+
Control, ControlGroup, ControlValueAccessor,
21+
RequiredValidatorDirective} from 'angular2/forms';
2122

2223
export function main() {
2324
function detectChanges(view) {
@@ -210,11 +211,37 @@ export function main() {
210211
});
211212
});
212213

214+
it("should support validators",(done) => {
215+
var t = `<div #form [new-control-group]="{'login': 'loginValue'}">
216+
<input type="text" control="login" required>
217+
</div>`;
218+
219+
compile(MyComp, t, new MyComp(), (view) => {
220+
var form = view.contextWithLocals.get("form");
221+
expect(form.valid).toEqual(true);
222+
223+
var input = queryView(view, "input");
224+
225+
input.value = "";
226+
dispatchEvent(input, "change");
227+
228+
expect(form.valid).toEqual(false);
229+
done();
230+
});
231+
});
213232
});
214233
});
215234
}
216235

217-
@Component({selector: "my-comp"})
236+
@Component({
237+
selector: "my-comp"
238+
})
239+
@Template({
240+
inline: "",
241+
directives: [ControlGroupDirective, ControlNameDirective,
242+
ControlDirective, NewControlGroupDirective, RequiredValidatorDirective,
243+
WrappedValue]
244+
})
218245
class MyComp {
219246
form:ControlGroup;
220247
name:string;

modules/angular2/test/forms/model_spec.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el} from 'angular2/test_lib';
22
import {ControlGroup, Control} from 'angular2/forms';
3+
import * as validations from 'angular2/forms';
34

45
export function main() {
6+
describe("Control", () => {
7+
describe("validator", () => {
8+
it("should run validator with the initial value", () => {
9+
var c = new Control("value", validations.required);
10+
expect(c.valid).toEqual(true);
11+
});
12+
13+
it("should rerun the validator when the value changes", () => {
14+
var c = new Control("value", validations.required);
15+
c.updateValue(null);
16+
expect(c.valid).toEqual(false);
17+
});
18+
19+
it("should return errors", () => {
20+
var c = new Control(null, validations.required);
21+
expect(c.errors).toEqual({"required" : true});
22+
});
23+
});
24+
});
25+
526
describe("ControlGroup", () => {
627
describe("value", () => {
728
it("should be the reduced value of the child controls", () => {
@@ -17,5 +38,27 @@ export function main() {
1738
expect(g.value).toEqual({})
1839
});
1940
});
41+
42+
describe("validator", () => {
43+
it("should run the validator with the initial value", () => {
44+
var g = new ControlGroup({
45+
"one": new Control(null, validations.required)
46+
});
47+
48+
expect(g.valid).toEqual(false);
49+
50+
expect(g.errors).toEqual({"one": {"required" : true}});
51+
});
52+
53+
it("should run the validator with the value changes", () => {
54+
var c = new Control(null, validations.required);
55+
var g = new ControlGroup({"one": c});
56+
57+
c.updateValue("some value");
58+
59+
expect(g.valid).toEqual(true);
60+
expect(g.errors).toEqual(null);
61+
});
62+
});
2063
});
2164
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {ddescribe, describe, it, iit, xit, expect, beforeEach, afterEach, el} from 'angular2/test_lib';
2+
import {ControlGroup, Control, required, compose, controlGroupValidator} from 'angular2/forms';
3+
4+
export function main() {
5+
function validator(key:string, error:any){
6+
return function(c:Control) {
7+
var r = {};
8+
r[key] = error;
9+
return r;
10+
}
11+
}
12+
13+
describe("Validators", () => {
14+
describe("required", () => {
15+
it("should error on an empty string", () => {
16+
expect(required(new Control(""))).toEqual({"required" : true});
17+
});
18+
19+
it("should error on null", () => {
20+
expect(required(new Control(null))).toEqual({"required" : true});
21+
});
22+
23+
it("should not error on a non-empty string", () => {
24+
expect(required(new Control("not empty"))).toEqual(null);
25+
});
26+
});
27+
28+
describe("compose", () => {
29+
it("should collect errors from all the validators", () => {
30+
var c = compose([validator("a", true), validator("b", true)]);
31+
expect(c(new Control(""))).toEqual({"a" : true, "b" : true});
32+
});
33+
34+
it("should run validators left to right", () => {
35+
var c = compose([validator("a", 1), validator("a", 2)]);
36+
expect(c(new Control(""))).toEqual({"a" : 2});
37+
});
38+
});
39+
40+
describe("controlGroupValidator", () => {
41+
it("should collect errors from the child controls", () => {
42+
var g = new ControlGroup({
43+
"one" : new Control("one", validator("a", true)),
44+
"two" : new Control("two", validator("b", true))
45+
});
46+
47+
expect(controlGroupValidator(g)).toEqual({
48+
"one" : {"a" : true},
49+
"two" : {"b" : true}
50+
});
51+
});
52+
53+
it("should not include keys for controls that have no errors", () => {
54+
var g = new ControlGroup({
55+
"one" : new Control("one", validator("a", true)),
56+
"two" : new Control("one")
57+
});
58+
59+
expect(controlGroupValidator(g)).toEqual({
60+
"one" : {"a" : true}
61+
});
62+
});
63+
64+
it("should return null when no errors", () => {
65+
var g = new ControlGroup({
66+
"one" : new Control("one")
67+
});
68+
69+
expect(controlGroupValidator(g)).toEqual(null);
70+
});
71+
});
72+
});
73+
}

0 commit comments

Comments
 (0)