feat(forms): Implement a way to manually set errors on a control

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
This commit is contained in:
vsavkin
2015-10-27 11:20:07 -07:00
committed by Victor Savkin
parent 689ded5c47
commit ed4826b08c
11 changed files with 342 additions and 214 deletions

View File

@ -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; }

View File

@ -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});
});
}
@ -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});
}
});
}
@ -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});
});
}
@ -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});
}
});
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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; }
@ -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; }
@ -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);
@ -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);
@ -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 {
@ -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;
}
/**
@ -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();
}
@ -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.
*/
@ -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 {
@ -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) => {
@ -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});
}
/**
@ -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); });

View File

@ -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) => {
@ -74,6 +74,6 @@ export class Validators {
anyErrors = true;
}
});
return anyErrors ? {'controls': res} : null;
return anyErrors ? res : null;
}
}