feat(forms): support adding validators to ControlGroup via template
Closes #4954
This commit is contained in:
parent
f98faf0702
commit
758062807a
|
@ -21,4 +21,6 @@ export class AbstractControlDirective {
|
|||
get touched(): boolean { return isPresent(this.control) ? this.control.touched : null; }
|
||||
|
||||
get untouched(): boolean { return isPresent(this.control) ? this.control.untouched : null; }
|
||||
|
||||
get path(): string[] { return null; }
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ export class NgControl extends AbstractControlDirective {
|
|||
valueAccessor: ControlValueAccessor = null;
|
||||
|
||||
get validator(): Function { return null; }
|
||||
get path(): string[] { return null; }
|
||||
|
||||
viewToModelUpdate(newValue: any): void {}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {OnInit, OnDestroy} from 'angular2/lifecycle_hooks';
|
||||
import {Directive} from 'angular2/src/core/metadata';
|
||||
import {Inject, Host, SkipSelf, forwardRef, Provider} from 'angular2/src/core/di';
|
||||
import {Optional, Inject, Host, SkipSelf, forwardRef, Provider} from 'angular2/src/core/di';
|
||||
import {ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
|
||||
|
@ -8,6 +8,7 @@ import {ControlContainer} from './control_container';
|
|||
import {controlPath} from './shared';
|
||||
import {ControlGroup} from '../model';
|
||||
import {Form} from './form_interface';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
|
||||
const controlGroupBinding =
|
||||
CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgControlGroup)}));
|
||||
|
@ -60,9 +61,14 @@ export class NgControlGroup extends ControlContainer implements OnInit,
|
|||
OnDestroy {
|
||||
/** @internal */
|
||||
_parent: ControlContainer;
|
||||
constructor(@Host() @SkipSelf() _parent: ControlContainer) {
|
||||
|
||||
private _validators: Function[];
|
||||
|
||||
constructor(@Host() @SkipSelf() parent: ControlContainer,
|
||||
@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
|
||||
super();
|
||||
this._parent = _parent;
|
||||
this._parent = parent;
|
||||
this._validators = validators;
|
||||
}
|
||||
|
||||
onInit(): void { this.formDirective.addControlGroup(this); }
|
||||
|
@ -74,4 +80,6 @@ export class NgControlGroup extends ControlContainer implements OnInit,
|
|||
get path(): string[] { return controlPath(this.name, this._parent); }
|
||||
|
||||
get formDirective(): Form { return this._parent.formDirective; }
|
||||
|
||||
get validator(): Function { return Validators.compose(this._validators); }
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ import {
|
|||
import {StringMapWrapper, ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {isPresent, isBlank, CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {Directive} from 'angular2/src/core/metadata';
|
||||
import {forwardRef, Provider} from 'angular2/src/core/di';
|
||||
import {forwardRef, Provider, Optional, Inject} from 'angular2/src/core/di';
|
||||
import {NgControl} from './ng_control';
|
||||
import {Form} from './form_interface';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {ControlContainer} from './control_container';
|
||||
import {AbstractControl, ControlGroup, Control} from '../model';
|
||||
import {setUpControl} from './shared';
|
||||
import {setUpControl, setUpControlGroup} from './shared';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
|
||||
const formDirectiveProvider =
|
||||
CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgForm)}));
|
||||
|
@ -87,9 +88,14 @@ const formDirectiveProvider =
|
|||
exportAs: 'form'
|
||||
})
|
||||
export class NgForm extends ControlContainer implements Form {
|
||||
form: ControlGroup = new ControlGroup({});
|
||||
form: ControlGroup;
|
||||
ngSubmit = new EventEmitter();
|
||||
|
||||
constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
|
||||
super();
|
||||
this.form = new ControlGroup({}, null, Validators.compose(validators));
|
||||
}
|
||||
|
||||
get formDirective(): Form { return this; }
|
||||
|
||||
get control(): ControlGroup { return this.form; }
|
||||
|
@ -124,6 +130,7 @@ export class NgForm extends ControlContainer implements Form {
|
|||
this._later(_ => {
|
||||
var container = this._findContainer(dir.path);
|
||||
var group = new ControlGroup({});
|
||||
setUpControlGroup(group, dir);
|
||||
container.addControl(dir.name, group);
|
||||
group.updateValueAndValidity({emitEvent: false});
|
||||
});
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import {CONST_EXPR} from 'angular2/src/core/facade/lang';
|
||||
import {ListWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/core/facade/collection';
|
||||
import {ObservableWrapper, EventEmitter} from 'angular2/src/core/facade/async';
|
||||
import {SimpleChange} from 'angular2/src/core/change_detection';
|
||||
|
||||
import {OnChanges} from 'angular2/lifecycle_hooks';
|
||||
import {Directive} from 'angular2/src/core/metadata';
|
||||
import {forwardRef, Provider} from 'angular2/src/core/di';
|
||||
import {forwardRef, Provider, Inject, Optional} from 'angular2/src/core/di';
|
||||
import {NgControl} from './ng_control';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {ControlContainer} from './control_container';
|
||||
import {Form} from './form_interface';
|
||||
import {Control, ControlGroup} from '../model';
|
||||
import {setUpControl} from './shared';
|
||||
import {setUpControl, setUpControlGroup} from './shared';
|
||||
import {Validators, NG_VALIDATORS} from '../validators';
|
||||
|
||||
const formDirectiveProvider =
|
||||
CONST_EXPR(new Provider(ControlContainer, {useExisting: forwardRef(() => NgFormModel)}));
|
||||
|
@ -100,8 +102,21 @@ export class NgFormModel extends ControlContainer implements Form,
|
|||
form: ControlGroup = null;
|
||||
directives: NgControl[] = [];
|
||||
ngSubmit = new EventEmitter();
|
||||
private _validators: Function[];
|
||||
|
||||
onChanges(_): void { this._updateDomValue(); }
|
||||
constructor(@Optional() @Inject(NG_VALIDATORS) validators: Function[]) {
|
||||
super();
|
||||
this._validators = validators;
|
||||
}
|
||||
|
||||
onChanges(changes: {[key: string]: SimpleChange}): void {
|
||||
if (StringMapWrapper.contains(changes, "form")) {
|
||||
var c = Validators.compose(this._validators);
|
||||
this.form.validator = Validators.compose([this.form.validator, c]);
|
||||
}
|
||||
|
||||
this._updateDomValue();
|
||||
}
|
||||
|
||||
get formDirective(): Form { return this; }
|
||||
|
||||
|
@ -120,7 +135,11 @@ export class NgFormModel extends ControlContainer implements Form,
|
|||
|
||||
removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); }
|
||||
|
||||
addControlGroup(dir: NgControlGroup) {}
|
||||
addControlGroup(dir: NgControlGroup) {
|
||||
var ctrl: any = this.form.find(dir.path);
|
||||
setUpControlGroup(ctrl, dir);
|
||||
ctrl.updateValueAndValidity({emitEvent: false});
|
||||
}
|
||||
|
||||
removeControlGroup(dir: NgControlGroup) {}
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptio
|
|||
|
||||
import {ControlContainer} from './control_container';
|
||||
import {NgControl} from './ng_control';
|
||||
import {Control} from '../model';
|
||||
import {AbstractControlDirective} from './abstract_control_directive';
|
||||
import {NgControlGroup} from './ng_control_group';
|
||||
import {Control, ControlGroup} from '../model';
|
||||
import {Validators} from '../validators';
|
||||
import {ControlValueAccessor} from './control_value_accessor';
|
||||
import {ElementRef, QueryList} from 'angular2/src/core/linker';
|
||||
|
@ -42,7 +44,12 @@ export function setUpControl(control: Control, dir: NgControl): void {
|
|||
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
|
||||
}
|
||||
|
||||
function _throwError(dir: NgControl, message: string): void {
|
||||
export function setUpControlGroup(control: ControlGroup, dir: NgControlGroup) {
|
||||
if (isBlank(control)) _throwError(dir, "Cannot find control");
|
||||
control.validator = Validators.compose([control.validator, dir.validator]);
|
||||
}
|
||||
|
||||
function _throwError(dir: AbstractControlDirective, message: string): void {
|
||||
var path = dir.path.join(" -> ");
|
||||
throw new BaseException(`${message} '${path}'`);
|
||||
}
|
||||
|
|
|
@ -105,8 +105,12 @@ export function main() {
|
|||
var loginControlDir;
|
||||
|
||||
beforeEach(() => {
|
||||
form = new NgFormModel();
|
||||
formModel = new ControlGroup({"login": new Control(null)});
|
||||
form = new NgFormModel([]);
|
||||
formModel = new ControlGroup({
|
||||
"login": new Control(),
|
||||
"passwords":
|
||||
new ControlGroup({"password": new Control(), "passwordConfirm": new Control()})
|
||||
});
|
||||
form.form = formModel;
|
||||
|
||||
loginControlDir = new NgControlName(form, [], [defaultAccessor]);
|
||||
|
@ -167,6 +171,26 @@ export function main() {
|
|||
});
|
||||
});
|
||||
|
||||
describe("addControlGroup", () => {
|
||||
var matchingPasswordsValidator = (g) => {
|
||||
if (g.controls["password"].value != g.controls["passwordConfirm"].value) {
|
||||
return {"differentPasswords": true};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
it("should set up validator", () => {
|
||||
var group = new NgControlGroup(form, [matchingPasswordsValidator]);
|
||||
group.name = "passwords";
|
||||
form.addControlGroup(group);
|
||||
|
||||
formModel.find(["passwords", "password"]).updateValue("somePassword");
|
||||
|
||||
expect(formModel.hasError("differentPasswords", ["passwords"])).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeControl", () => {
|
||||
it("should remove the directive to the list of directives included in the form", () => {
|
||||
form.addControl(loginControlDir);
|
||||
|
@ -181,10 +205,22 @@ export function main() {
|
|||
|
||||
formModel.find(["login"]).updateValue("new value");
|
||||
|
||||
form.onChanges(null);
|
||||
form.onChanges({});
|
||||
|
||||
expect((<any>loginControlDir.valueAccessor).writtenValue).toEqual("new value");
|
||||
});
|
||||
|
||||
it("should set up validator", () => {
|
||||
var formValidator = (c) => ({"custom": true});
|
||||
var f = new NgFormModel([formValidator]);
|
||||
f.form = formModel;
|
||||
f.onChanges({"form": formModel});
|
||||
|
||||
// trigger validation
|
||||
formModel.controls["login"].updateValue("");
|
||||
|
||||
expect(formModel.errors).toEqual({"custom": true});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -195,10 +231,10 @@ export function main() {
|
|||
var personControlGroupDir;
|
||||
|
||||
beforeEach(() => {
|
||||
form = new NgForm();
|
||||
form = new NgForm([]);
|
||||
formModel = form.form;
|
||||
|
||||
personControlGroupDir = new NgControlGroup(form);
|
||||
personControlGroupDir = new NgControlGroup(form, []);
|
||||
personControlGroupDir.name = "person";
|
||||
|
||||
loginControlDir = new NgControlName(personControlGroupDir, null, [defaultAccessor]);
|
||||
|
@ -246,6 +282,17 @@ export function main() {
|
|||
|
||||
// should update the form's value and validity
|
||||
});
|
||||
|
||||
it("should set up validator", fakeAsync(() => {
|
||||
var formValidator = (c) => ({"custom": true});
|
||||
var f = new NgForm([formValidator]);
|
||||
f.addControlGroup(personControlGroupDir);
|
||||
f.addControl(loginControlDir);
|
||||
|
||||
flushMicrotasks();
|
||||
|
||||
expect(f.form.errors).toEqual({"custom": true});
|
||||
}));
|
||||
});
|
||||
|
||||
describe("NgControlGroup", () => {
|
||||
|
@ -255,9 +302,9 @@ export function main() {
|
|||
beforeEach(() => {
|
||||
formModel = new ControlGroup({"login": new Control(null)});
|
||||
|
||||
var parent = new NgFormModel();
|
||||
var parent = new NgFormModel([]);
|
||||
parent.form = new ControlGroup({"group": formModel});
|
||||
controlGroupDir = new NgControlGroup(parent);
|
||||
controlGroupDir = new NgControlGroup(parent, []);
|
||||
controlGroupDir.name = "group";
|
||||
});
|
||||
|
||||
|
@ -356,7 +403,7 @@ export function main() {
|
|||
beforeEach(() => {
|
||||
formModel = new Control("name");
|
||||
|
||||
var parent = new NgFormModel();
|
||||
var parent = new NgFormModel([]);
|
||||
parent.form = new ControlGroup({"name": formModel});
|
||||
controlNameDir = new NgControlName(parent, [], [defaultAccessor]);
|
||||
controlNameDir.name = "name";
|
||||
|
|
|
@ -24,6 +24,8 @@ import {
|
|||
ControlGroup,
|
||||
ControlValueAccessor,
|
||||
FORM_DIRECTIVES,
|
||||
NG_VALIDATORS,
|
||||
Provider,
|
||||
NgControl,
|
||||
NgIf,
|
||||
NgFor,
|
||||
|
@ -398,10 +400,10 @@ export function main() {
|
|||
var form = new ControlGroup(
|
||||
{"login": new Control(""), "min": new Control(""), "max": new Control("")});
|
||||
|
||||
var t = `<div [ng-form-model]="form">
|
||||
<input type="text" ng-control="login" required>
|
||||
<input type="text" ng-control="min" minlength="3">
|
||||
<input type="text" ng-control="max" maxlength="3">
|
||||
var t = `<div [ng-form-model]="form" login-is-empty-validator>
|
||||
<input type="text" ng-control="login" required>
|
||||
<input type="text" ng-control="min" minlength="3">
|
||||
<input type="text" ng-control="max" maxlength="3">
|
||||
</div>`;
|
||||
|
||||
tcb.overrideTemplate(MyComp, t).createAsync(MyComp).then((rootTC) => {
|
||||
|
@ -423,6 +425,8 @@ export function main() {
|
|||
expect(form.hasError("minlength", ["min"])).toEqual(true);
|
||||
expect(form.hasError("maxlength", ["max"])).toEqual(true);
|
||||
|
||||
expect(form.hasError("loginIsEmpty")).toEqual(true);
|
||||
|
||||
required.nativeElement.value = "1";
|
||||
minLength.nativeElement.value = "123";
|
||||
maxLength.nativeElement.value = "123";
|
||||
|
@ -914,8 +918,22 @@ class MyInput implements ControlValueAccessor {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: "my-comp"})
|
||||
@View({directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor]})
|
||||
function loginIsEmptyGroupValidator(c: ControlGroup) {
|
||||
return c.controls["login"].value == "" ? {"loginIsEmpty": true} : null;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[login-is-empty-validator]',
|
||||
providers: [new Provider(NG_VALIDATORS, {useValue: loginIsEmptyGroupValidator, multi: true})]
|
||||
})
|
||||
class LoginIsEmptyValidator {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "my-comp",
|
||||
template: '',
|
||||
directives: [FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator]
|
||||
})
|
||||
class MyComp {
|
||||
form: any;
|
||||
name: string;
|
||||
|
|
|
@ -73,6 +73,7 @@ var NG_API = [
|
|||
'AbstractControlDirective.untouched',
|
||||
'AbstractControlDirective.valid',
|
||||
'AbstractControlDirective.value',
|
||||
'AbstractControlDirective.path',
|
||||
'AppRootUrl',
|
||||
'AppRootUrl.value',
|
||||
'AppRootUrl.value=',
|
||||
|
@ -657,6 +658,7 @@ var NG_API = [
|
|||
'NgControlGroup.untouched',
|
||||
'NgControlGroup.valid',
|
||||
'NgControlGroup.value',
|
||||
'NgControlGroup.validator',
|
||||
'NgControlStatus',
|
||||
'NgControlStatus.ngClassDirty',
|
||||
'NgControlStatus.ngClassInvalid',
|
||||
|
@ -1030,9 +1032,7 @@ var NG_API = [
|
|||
'UpperCasePipe.transform()',
|
||||
'UrlResolver',
|
||||
'UrlResolver.resolve()',
|
||||
'Validators#array()',
|
||||
'Validators#compose()',
|
||||
'Validators#group()',
|
||||
'Validators#nullValidator()',
|
||||
'Validators#required()',
|
||||
'Validators#minLength()',
|
||||
|
|
Loading…
Reference in New Issue