Skip to content

Commit

Permalink
feat(forms): Implement a way to manually set errors on a control
Browse files Browse the repository at this point in the history
Example:

var login = new Control("someLogin");
c.setErrors({"notUnique": true});
expect(c.valid).toEqual(false);
expect(c.errors).toEqual({"notUnique": true});

c.updateValue("newLogin");
expect(c.valid).toEqual(true);

BREAKING CHANGE:

Before:

ControlGroup.errors and ControlArray.errors returned a reduced value of their children controls' errors.

After:

ControlGroup.errors and ControlArray.errors return the errors of the group and array.
And ControlGroup.controlsErrors and ControlArray.controlsErrors return the reduce value of their children controls' errors.

Closes #4917
  • Loading branch information
vsavkin committed Oct 27, 2015
1 parent 689ded5 commit ed4826b
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class AbstractControlDirective {
return isPresent(this.control) ? this.control.errors : null;
}

get controlsErrors(): any { return isPresent(this.control) ? this.control.controlsErrors : null; }

get pristine(): boolean { return isPresent(this.control) ? this.control.pristine : null; }

get dirty(): boolean { return isPresent(this.control) ? this.control.dirty : null; }
Expand Down
8 changes: 4 additions & 4 deletions modules/angular2/src/core/forms/directives/ng_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class NgForm extends ControlContainer implements Form {
var ctrl = new Control();
setUpControl(ctrl, dir);
container.addControl(dir.name, ctrl);
ctrl.updateValidity();
ctrl.updateValueAndValidity({emitEvent: false});
});
}

Expand All @@ -115,7 +115,7 @@ export class NgForm extends ControlContainer implements Form {
var container = this._findContainer(dir.path);
if (isPresent(container)) {
container.removeControl(dir.name);
container.updateValidity();
container.updateValueAndValidity({emitEvent: false});
}
});
}
Expand All @@ -125,7 +125,7 @@ export class NgForm extends ControlContainer implements Form {
var container = this._findContainer(dir.path);
var group = new ControlGroup({});
container.addControl(dir.name, group);
group.updateValidity();
group.updateValueAndValidity({emitEvent: false});
});
}

Expand All @@ -134,7 +134,7 @@ export class NgForm extends ControlContainer implements Form {
var container = this._findContainer(dir.path);
if (isPresent(container)) {
container.removeControl(dir.name);
container.updateValidity();
container.updateValueAndValidity({emitEvent: false});
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class NgFormControl extends NgControl implements OnChanges {
onChanges(changes: {[key: string]: SimpleChange}): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);
this.form.updateValidity();
this.form.updateValueAndValidity({emitEvent: false});
}
if (isPropertyUpdated(changes, this.viewModel)) {
this.form.updateValue(this.model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class NgFormModel extends ControlContainer implements Form,
addControl(dir: NgControl): void {
var ctrl: any = this.form.find(dir.path);
setUpControl(ctrl, dir);
ctrl.updateValidity();
ctrl.updateValueAndValidity({emitEvent: false});
this.directives.push(dir);
}

Expand Down
2 changes: 1 addition & 1 deletion modules/angular2/src/core/forms/directives/ng_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class NgModel extends NgControl implements OnChanges {
onChanges(changes: {[key: string]: SimpleChange}) {
if (!this._added) {
setUpControl(this._control, this);
this._control.updateValidity();
this._control.updateValueAndValidity({emitEvent: false});
this._added = true;
}

Expand Down
121 changes: 90 additions & 31 deletions modules/angular2/src/core/forms/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,20 @@ function _find(control: AbstractControl, path: Array<string | number>| string) {
/**
*
*/
export class AbstractControl {
export abstract class AbstractControl {
/** @internal */
_value: any;
/** @internal */
_status: string;
/** @internal */
_errors: {[key: string]: any};
/** @internal */
_pristine: boolean = true;
/** @internal */
_touched: boolean = false;
/** @internal */
_parent: ControlGroup | ControlArray;

/** @internal */
_valueChanges: EventEmitter;

private _status: string;
private _errors: {[key: string]: any};
private _controlsErrors: any;
private _pristine: boolean = true;
private _touched: boolean = false;
private _parent: ControlGroup | ControlArray;

constructor(public validator: Function) {}

get value(): any { return this._value; }
Expand All @@ -70,8 +68,16 @@ export class AbstractControl {

get valid(): boolean { return this._status === VALID; }

/**
* Returns the errors of this control.
*/
get errors(): {[key: string]: any} { return this._errors; }

/**
* Returns the errors of the child controls.
*/
get controlsErrors(): any { return this._controlsErrors; }

get pristine(): boolean { return this._pristine; }

get dirty(): boolean { return !this.pristine; }
Expand Down Expand Up @@ -105,17 +111,6 @@ export class AbstractControl {

setParent(parent: ControlGroup | ControlArray): void { this._parent = parent; }

updateValidity({onlySelf}: {onlySelf?: boolean} = {}): void {
onlySelf = normalizeBool(onlySelf);

this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID;

if (isPresent(this._parent) && !onlySelf) {
this._parent.updateValidity({onlySelf: onlySelf});
}
}

updateValueAndValidity({onlySelf, emitEvent}: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
onlySelf = normalizeBool(onlySelf);
Expand All @@ -124,7 +119,8 @@ export class AbstractControl {
this._updateValue();

this._errors = this.validator(this);
this._status = isPresent(this._errors) ? INVALID : VALID;
this._controlsErrors = this._calculateControlsErrors();
this._status = this._calculateStatus();

if (emitEvent) {
ObservableWrapper.callNext(this._valueChanges, this._value);
Expand All @@ -135,6 +131,38 @@ export class AbstractControl {
}
}

/**
* Sets errors on a control.
*
* This is used when validations are run not automatically, but manually by the user.
*
* Calling `setErrors` will also update the validity of the parent control.
*
* ## Usage
*
* ```
* var login = new Control("someLogin");
* login.setErrors({
* "notUnique": true
* });
*
* expect(login.valid).toEqual(false);
* expect(login.errors).toEqual({"notUnique": true});
*
* login.updateValue("someOtherLogin");
*
* expect(login.valid).toEqual(true);
* ```
*/
setErrors(errors: {[key: string]: any}): void {
this._errors = errors;
this._status = this._calculateStatus();

if (isPresent(this._parent)) {
this._parent._updateControlsErrors();
}
}

find(path: Array<string | number>| string): AbstractControl { return _find(this, path); }

getError(errorCode: string, path: string[] = null): any {
Expand All @@ -151,7 +179,23 @@ export class AbstractControl {
}

/** @internal */
_updateValue(): void {}
_updateControlsErrors(): void {
this._controlsErrors = this._calculateControlsErrors();
this._status = this._calculateStatus();

if (isPresent(this._parent)) {
this._parent._updateControlsErrors();
}
}

private _calculateStatus(): string {
return isPresent(this._errors) || isPresent(this._controlsErrors) ? INVALID : VALID;
}

/** @internal */
abstract _updateValue(): void;
/** @internal */
abstract _calculateControlsErrors(): any;
}

/**
Expand All @@ -177,7 +221,7 @@ export class Control extends AbstractControl {
constructor(value: any = null, validator: Function = Validators.nullValidator) {
super(validator);
this._value = value;
this.updateValidity({onlySelf: true});
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
this._valueChanges = new EventEmitter();
}

Expand All @@ -203,6 +247,16 @@ export class Control extends AbstractControl {
this.updateValueAndValidity({onlySelf: onlySelf, emitEvent: emitEvent});
}

/**
* @internal
*/
_updateValue() {}

/**
* @internal
*/
_calculateControlsErrors() { return null; }

/**
* Register a listener for change events.
*/
Expand All @@ -226,14 +280,14 @@ export class ControlGroup extends AbstractControl {
private _optionals: {[key: string]: boolean};

constructor(public controls: {[key: string]: AbstractControl},
optionals: {[key: string]: boolean} = null, validator: Function = Validators.group) {
optionals: {[key: string]: boolean} = null,
validator: Function = Validators.nullValidator) {
super(validator);
this._optionals = isPresent(optionals) ? optionals : {};
this._valueChanges = new EventEmitter();

this._setParentForControls();
this._value = this._reduceValue();
this.updateValidity({onlySelf: true});
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}

addControl(name: string, control: AbstractControl): void {
Expand Down Expand Up @@ -266,6 +320,9 @@ export class ControlGroup extends AbstractControl {
/** @internal */
_updateValue() { this._value = this._reduceValue(); }

/** @internal */
_calculateControlsErrors() { return Validators.group(this); }

/** @internal */
_reduceValue() {
return this._reduceChildren({}, (acc, control, name) => {
Expand Down Expand Up @@ -314,14 +371,13 @@ export class ControlGroup extends AbstractControl {
* ### Example ([live demo](http://plnkr.co/edit/23DESOpbNnBpBHZt1BR4?p=preview))
*/
export class ControlArray extends AbstractControl {
constructor(public controls: AbstractControl[], validator: Function = Validators.array) {
constructor(public controls: AbstractControl[], validator: Function = Validators.nullValidator) {
super(validator);

this._valueChanges = new EventEmitter();

this._setParentForControls();
this._updateValue();
this.updateValidity({onlySelf: true});
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
}

/**
Expand Down Expand Up @@ -363,6 +419,9 @@ export class ControlArray extends AbstractControl {
/** @internal */
_updateValue(): void { this._value = this.controls.map((control) => control.value); }

/** @internal */
_calculateControlsErrors() { return Validators.array(this); }

/** @internal */
_setParentForControls(): void {
this.controls.forEach((control) => { control.setParent(this); });
Expand Down
6 changes: 3 additions & 3 deletions modules/angular2/src/core/forms/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ export class Validators {
res[name] = control.errors;
}
});
return StringMapWrapper.isEmpty(res) ? null : {'controls': res};
return StringMapWrapper.isEmpty(res) ? null : res;
}

static array(array: modelModule.ControlArray): {[key: string]: any} {
static array(array: modelModule.ControlArray): any[] {
var res: any[] = [];
var anyErrors: boolean = false;
array.controls.forEach((control) => {
Expand All @@ -74,6 +74,6 @@ export class Validators {
anyErrors = true;
}
});
return anyErrors ? {'controls': res} : null;
return anyErrors ? res : null;
}
}
2 changes: 1 addition & 1 deletion modules/angular2/test/core/forms/form_builder_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function main() {
it("should use default validators when no validators are provided", () => {
var g = b.group({"login": "some value"});
expect(g.controls["login"].validator).toBe(Validators.nullValidator);
expect(g.validator).toBe(Validators.group);
expect(g.validator).toBe(Validators.nullValidator);
});

it("should create control arrays", () => {
Expand Down
Loading

0 comments on commit ed4826b

Please sign in to comment.