From 28d88c5b128c494e3df2d4825ab039ac4ef68e79 Mon Sep 17 00:00:00 2001 From: Ted Sander Date: Fri, 16 Oct 2015 16:13:14 -0700 Subject: [PATCH] feat(validators): Allow errors at both the group/array level or their children Allow ControlGroups and ControlArrays to contain errors from their level, and errors from their children. [Design Doc](https://docs.google.com/document/d/1EnJ3-_iFpVKFz1ifN1LkXSGQ7h3A72OQGry2g8eo7IA/edit?pli=1#heading=h.j53rt81eegm4) BREAKING CHANGE: errors format has changed from validators. Now errors from a control or an array's children are prefixed with 'controls' while errors from the object itself are left at the root level. Example: Given a Control group as follows: var group = new ControlGroup({ login: new Control("", required), password: new Control("", required), passwordConfirm: new Control("", required) }); Before: group.errors { login: {required: true}, password: {required: true}, passwordConfirm: {required: true}, } After: group.errors { controls: { login: {required: true}, password: {required: true}, passwordConfirm: {required: true}, } } --- modules/angular2/src/core/forms/validators.ts | 28 +++---- .../angular2/test/core/forms/model_spec.ts | 4 +- .../test/core/forms/validators_spec.ts | 75 +++++++++++++++++-- 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/modules/angular2/src/core/forms/validators.ts b/modules/angular2/src/core/forms/validators.ts index 4ef31ce967..aca8932af2 100644 --- a/modules/angular2/src/core/forms/validators.ts +++ b/modules/angular2/src/core/forms/validators.ts @@ -46,7 +46,7 @@ export class Validators { static compose(validators: Function[]): Function { if (isBlank(validators)) return Validators.nullValidator; - return function(control: modelModule.Control) { + return function(control: modelModule.AbstractControl) { var res = ListWrapper.reduce(validators, (res, validator) => { var errors = validator(control); return isPresent(errors) ? StringMapWrapper.merge(res, errors) : res; @@ -55,33 +55,25 @@ export class Validators { }; } - static group(group: modelModule.ControlGroup): {[key: string]: any[]} { + static group(group: modelModule.ControlGroup): {[key: string]: any} { var res: {[key: string]: any[]} = {}; StringMapWrapper.forEach(group.controls, (control, name) => { if (group.contains(name) && isPresent(control.errors)) { - Validators._mergeErrors(control, res); + res[name] = control.errors; } }); - return StringMapWrapper.isEmpty(res) ? null : res; + return StringMapWrapper.isEmpty(res) ? null : {'controls': res}; } - static array(array: modelModule.ControlArray): {[key: string]: any[]} { - var res: {[key: string]: any[]} = {}; + static array(array: modelModule.ControlArray): {[key: string]: any} { + var res: any[] = []; + var anyErrors: boolean = false; array.controls.forEach((control) => { + res.push(control.errors); if (isPresent(control.errors)) { - Validators._mergeErrors(control, res); + anyErrors = true; } }); - return StringMapWrapper.isEmpty(res) ? null : res; - } - - static _mergeErrors(control: modelModule.AbstractControl, res: {[key: string]: any[]}): void { - StringMapWrapper.forEach(control.errors, (value, error) => { - if (!StringMapWrapper.contains(res, error)) { - res[error] = []; - } - var current: any[] = res[error]; - current.push(control); - }); + return anyErrors ? {'controls': res} : null; } } diff --git a/modules/angular2/test/core/forms/model_spec.ts b/modules/angular2/test/core/forms/model_spec.ts index 4dc90e5b07..460a2b8a19 100644 --- a/modules/angular2/test/core/forms/model_spec.ts +++ b/modules/angular2/test/core/forms/model_spec.ts @@ -192,7 +192,7 @@ export function main() { expect(g.valid).toEqual(false); - expect(g.errors).toEqual({"required": [one]}); + expect(g.errors).toEqual({"controls": {"one": {"required": true}}}); }); it("should run the validator with the value changes", () => { @@ -445,7 +445,7 @@ export function main() { ]); expect(a.valid).toBe(false); - expect(a.errors).toEqual({"required": [a.controls[1]]}); + expect(a.errors).toEqual({"controls": [null, {"required": true}, null]}); }); it("should run the validator when the value changes", () => { diff --git a/modules/angular2/test/core/forms/validators_spec.ts b/modules/angular2/test/core/forms/validators_spec.ts index c65d59030a..f81585b9b4 100644 --- a/modules/angular2/test/core/forms/validators_spec.ts +++ b/modules/angular2/test/core/forms/validators_spec.ts @@ -9,11 +9,11 @@ import { afterEach, el } from 'angular2/testing_internal'; -import {ControlGroup, Control, Validators} from 'angular2/core'; +import {ControlGroup, Control, Validators, AbstractControl, ControlArray} from 'angular2/core'; export function main() { function validator(key: string, error: any) { - return function(c: Control) { + return function(c: AbstractControl) { var r = {}; r[key] = error; return r; @@ -87,10 +87,10 @@ export function main() { describe("controlGroupValidator", () => { it("should collect errors from the child controls", () => { var one = new Control("one", validator("a", true)); - var two = new Control("one", validator("b", true)); + var two = new Control("two", validator("b", true)); var g = new ControlGroup({"one": one, "two": two}); - expect(Validators.group(g)).toEqual({"a": [one], "b": [two]}); + expect(Validators.group(g)).toEqual({"controls": {"one": {"a": true}, "two": {"b": true}}}); }); it("should not include controls that have no errors", () => { @@ -98,7 +98,7 @@ export function main() { var two = new Control("two"); var g = new ControlGroup({"one": one, "two": two}); - expect(Validators.group(g)).toEqual({"a": [one]}); + expect(Validators.group(g)).toEqual({"controls": {"one": {"a": true}}}); }); it("should return null when no errors", () => { @@ -106,6 +106,69 @@ export function main() { expect(Validators.group(g)).toEqual(null); }); + + it("should return control errors mixed with group errors", () => { + var one = new Control("one", validator("a", true)); + var g = new ControlGroup({"one": one}, null, + Validators.compose([validator("b", true), Validators.group])); + + expect(g.validator(g)).toEqual({"b": true, "controls": {"one": {"a": true}}}); + }); + + it("should return nested control group errors mixed with group errors", () => { + var one = new Control("one", validator("a", true)); + var g = new ControlGroup({"one": one}, null, + Validators.compose([validator("b", true), Validators.group])); + var two = new Control("two", validator("c", true)); + var gTwo = new ControlGroup({"two": two, "group": g}); + + expect(gTwo.validator(gTwo)) + .toEqual({ + "controls": + {"two": {"c": true}, "group": {"b": true, "controls": {"one": {"a": true}}}} + }); + }); + }); + + describe("controlArrayValidator", () => { + it("should collect errors from the child controls", () => { + var one = new Control("one", validator("a", true)); + var two = new Control("two", validator("b", true)); + var a = new ControlArray([one, two]); + + expect(Validators.array(a)).toEqual({"controls": [{"a": true}, {"b": true}]}); + }); + + it("should not include controls that have no errors", () => { + var one = new Control("one"); + var two = new Control("two", validator("a", true)); + var three = new Control("three"); + var a = new ControlArray([one, two, three]); + + expect(Validators.array(a)).toEqual({"controls": [null, {"a": true}, null]}); + }); + + it("should return null when no errors", () => { + var a = new ControlArray([new Control("one")]); + + expect(Validators.array(a)).toEqual(null); + }); + + it("should return control errors mixed with group errors", () => { + var one = new Control("one", validator("a", true)); + var a = + new ControlArray([one], Validators.compose([validator("b", true), Validators.array])); + + expect(a.validator(a)).toEqual({"b": true, "controls": [{"a": true}]}); + }); + + it("should return nested array errors ", () => { + var one = new Control("one", validator("a", true)); + var a = new ControlArray([one]); + var a2 = new ControlArray([a]); + + expect(Validators.array(a2)).toEqual({"controls": [{"controls": [{"a": true}]}]}); + }); }); }); -} \ No newline at end of file +}