fix(forms): improve ngModel error messages (#10314)

This commit is contained in:
Kara 2016-07-27 10:59:40 -07:00 committed by GitHub
parent e44e8668ea
commit 43349dd373
10 changed files with 331 additions and 91 deletions

View File

@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const FormErrorExamples = {
formControlName: `
<div [formGroup]="myGroup">
<input formControlName="firstName">
</div>
In your class:
this.myGroup = new FormGroup({
firstName: new FormControl()
});`,
formGroupName: `
<div [formGroup]="myGroup">
<div formGroupName="person">
<input formControlName="firstName">
</div>
</div>
In your class:
this.myGroup = new FormGroup({
person: new FormGroup({ firstName: new FormControl() })
});`,
formArrayName: `
<div [formGroup]="myGroup">
<div formArrayName="cities">
<div *ngFor="let city of cityArray.controls; let i=index">
<input [formControlName]="i">
</div>
</div>
</div>
In your class:
this.cityArray = new FormArray([new FormControl('SF')]);
this.myGroup = new FormGroup({
cities: this.cityArray
});`,
ngModelGroup: `
<form>
<div ngModelGroup="person">
<input [(ngModel)]="person.name" name="firstName">
</div>
</form>`,
ngModelWithFormGroup: `
<div [formGroup]="myGroup">
<input formControlName="firstName">
<input [(ngModel)]="showMoreControls" [ngModelOptions]="{standalone: true}">
</div>
`
};
 

View File

@ -13,10 +13,14 @@ import {BaseException} from '../facade/exceptions';
import {FormControl} from '../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {ControlContainer} from './control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
import {NgControl} from './ng_control';
import {NgForm} from './ng_form';
import {NgModelGroup} from './ng_model_group';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared';
import {TemplateDrivenErrors} from './template_driven_errors';
import {AsyncValidatorFn, ValidatorFn} from './validators';
export const formControlBinding: any =
@ -75,7 +79,7 @@ export class NgModel extends NgControl implements OnChanges,
}
ngOnChanges(changes: SimpleChanges) {
this._checkName();
this._checkForErrors();
if (!this._registered) this._setUpControl();
if (isPropertyUpdated(changes, this.viewModel)) {
@ -120,17 +124,28 @@ export class NgModel extends NgControl implements OnChanges,
this._control.updateValueAndValidity({emitEvent: false});
}
private _checkForErrors(): void {
if (!this._isStandalone()) {
this._checkParentType();
}
this._checkName();
}
private _checkParentType(): void {
if (!(this._parent instanceof NgModelGroup) &&
this._parent instanceof AbstractFormGroupDirective) {
TemplateDrivenErrors.formGroupNameException();
} else if (
!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
TemplateDrivenErrors.modelParentException();
}
}
private _checkName(): void {
if (this.options && this.options.name) this.name = this.options.name;
if (!this._isStandalone() && !this.name) {
throw new BaseException(
`If ngModel is used within a form tag, either the name attribute must be set
or the form control must be defined as 'standalone' in ngModelOptions.
Example 1: <input [(ngModel)]="person.firstName" name="first">
Example 2: <input [(ngModel)]="person.firstName" [ngModelOptions]="{standalone: true}">
`);
TemplateDrivenErrors.missingNameException();
}
}

View File

@ -8,10 +8,13 @@
import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core';
import {BaseException} from '../facade/exceptions';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {ControlContainer} from './control_container';
import {NgForm} from './ng_form';
import {TemplateDrivenErrors} from './template_driven_errors';
export const modelGroupProvider: any =
/*@ts2dart_const*/ /* @ts2dart_Provider */ {
@ -69,4 +72,11 @@ export class NgModelGroup extends AbstractFormGroupDirective implements OnInit,
this._validators = validators;
this._asyncValidators = asyncValidators;
}
/** @internal */
_checkParentType(): void {
if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
TemplateDrivenErrors.modelGroupParentException();
}
}
}

View File

@ -12,6 +12,7 @@ import {BaseException} from '../../facade/exceptions';
import {FormArray} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {ControlContainer} from '../control_container';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, controlPath} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators';
@ -101,28 +102,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective)) {
this._throwParentException();
ReactiveErrors.arrayParentException();
}
}
private _throwParentException(): void {
throw new BaseException(`formArrayName must be used with a parent formGroup directive.
You'll want to add a formGroup directive and pass it an existing FormGroup instance
(you can create one in your class).
Example:
<div [formGroup]="myGroup">
<div formArrayName="cities">
<div *ngFor="let city of cityArray.controls; let i=index">
<input [formControlName]="i">
</div>
</div>
</div>
In your class:
this.cityArray = new FormArray([new FormControl('SF')]);
this.myGroup = new FormGroup({
cities: this.cityArray
});`);
}
}

View File

@ -12,9 +12,11 @@ import {EventEmitter, ObservableWrapper} from '../../facade/async';
import {BaseException} from '../../facade/exceptions';
import {FormControl} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor';
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators';
@ -153,26 +155,13 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) &&
this._parent instanceof AbstractFormGroupDirective) {
ReactiveErrors.ngModelGroupException();
} else if (
!(this._parent instanceof FormGroupName) &&
!(this._parent instanceof FormGroupDirective) &&
!(this._parent instanceof FormArrayName)) {
this._throwParentException();
ReactiveErrors.controlParentException();
}
}
private _throwParentException(): void {
throw new BaseException(
`formControlName must be used with a parent formGroup directive.
You'll want to add a formGroup directive and pass it an existing FormGroup instance
(you can create one in your class).
Example:
<div [formGroup]="myGroup">
<input formControlName="firstName">
</div>
In your class:
this.myGroup = new FormGroup({
firstName: new FormControl()
});`);
}
}

View File

@ -17,6 +17,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators';
import {ControlContainer} from '../control_container';
import {Form} from '../form_interface';
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors';
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
import {FormArrayName} from './form_array_name';
@ -199,9 +200,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
private _checkFormPresent() {
if (isBlank(this.form)) {
throw new BaseException(`formGroup expects a FormGroup instance. Please pass one in.
Example: <form [formGroup]="myFormGroup">
`);
ReactiveErrors.missingFormException();
}
}
}

View File

@ -12,6 +12,7 @@ import {BaseException} from '../../facade/exceptions';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container';
import {ReactiveErrors} from '../reactive_errors';
import {FormGroupDirective} from './form_group_directive';
@ -86,25 +87,7 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
/** @internal */
_checkParentType(): void {
if (!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective)) {
this._throwParentException();
ReactiveErrors.groupParentException();
}
}
private _throwParentException() {
throw new BaseException(`formGroupName must be used with a parent formGroup directive.
You'll want to add a formGroup directive and pass it an existing FormGroup instance
(you can create one in your class).
Example:
<div [formGroup]="myGroup">
<div formGroupName="person">
<input formControlName="firstName">
</div>
</div>
In your class:
this.myGroup = new FormGroup({
person: new FormGroup({ firstName: new FormControl() })
});`);
}
}

View File

@ -0,0 +1,63 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {BaseException} from '../facade/exceptions';
import {FormErrorExamples as Examples} from './error_examples';
export class ReactiveErrors {
static controlParentException(): void {
throw new BaseException(
`formControlName must be used with a parent formGroup directive. You'll want to add a formGroup
directive and pass it an existing FormGroup instance (you can create one in your class).
Example:
${Examples.formControlName}`);
}
static ngModelGroupException(): void {
throw new BaseException(
`formControlName cannot be used with an ngModelGroup parent. It is only compatible with parents
that also have a "form" prefix: formGroupName, formArrayName, or formGroup.
Option 1: Update the parent to be formGroupName (reactive form strategy)
${Examples.formGroupName}
Option 2: Use ngModel instead of formControlName (template-driven strategy)
${Examples.ngModelGroup}`);
}
static missingFormException(): void {
throw new BaseException(`formGroup expects a FormGroup instance. Please pass one in.
Example:
${Examples.formControlName}`);
}
static groupParentException(): void {
throw new BaseException(
`formGroupName must be used with a parent formGroup directive. You'll want to add a formGroup
directive and pass it an existing FormGroup instance (you can create one in your class).
Example:
${Examples.formGroupName}`);
}
static arrayParentException(): void {
throw new BaseException(
`formArrayName must be used with a parent formGroup directive. You'll want to add a formGroup
directive and pass it an existing FormGroup instance (you can create one in your class).
Example:
${Examples.formArrayName}`);
}
}

View File

@ -0,0 +1,61 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {BaseException} from '../facade/exceptions';
import {FormErrorExamples as Examples} from './error_examples';
export class TemplateDrivenErrors {
static modelParentException(): void {
throw new BaseException(`
ngModel cannot be used to register form controls with a parent formGroup directive. Try using
formGroup's partner directive "formControlName" instead. Example:
${Examples.formControlName}
Or, if you'd like to avoid registering this form control, indicate that it's standalone in ngModelOptions:
Example:
${Examples.ngModelWithFormGroup}`);
}
static formGroupNameException(): void {
throw new BaseException(`
ngModel cannot be used to register form controls with a parent formGroupName or formArrayName directive.
Option 1: Use formControlName instead of ngModel (reactive strategy):
${Examples.formGroupName}
Option 2: Update ngModel's parent be ngModelGroup (template-driven strategy):
${Examples.ngModelGroup}`);
}
static missingNameException() {
throw new BaseException(
`If ngModel is used within a form tag, either the name attribute must be set or the form
control must be defined as 'standalone' in ngModelOptions.
Example 1: <input [(ngModel)]="person.firstName" name="first">
Example 2: <input [(ngModel)]="person.firstName" [ngModelOptions]="{standalone: true}">`);
}
static modelGroupParentException() {
throw new BaseException(`
ngModelGroup cannot be used with a parent formGroup directive.
Option 1: Use formGroupName instead of ngModelGroup (reactive strategy):
${Examples.formGroupName}
Option 2: Use a regular form tag instead of the formGroup directive (template-driven strategy):
${Examples.ngModelGroup}`);
}
}

View File

@ -1162,6 +1162,40 @@ export function main() {
});
}));
it('should throw if formControlName is used with NgForm',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<input type="text" formControlName="login">
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formControlName must be used with a parent formGroup directive.`));
async.done();
});
}));
it('should throw if formControlName is used with NgModelGroup',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<div ngModelGroup="parent">
<input type="text" formControlName="login">
</div>
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(
new RegExp(`formControlName cannot be used with an ngModelGroup parent.`));
async.done();
});
}));
it('should throw if formGroupName is used without a control container',
inject(
[TestComponentBuilder, AsyncTestCompleter],
@ -1178,6 +1212,24 @@ export function main() {
});
}));
it('should throw if formGroupName is used with NgForm',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<div formGroupName="person">
<input type="text" formControlName="login">
</div>
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formGroupName must be used with a parent formGroup directive.`));
async.done();
});
}));
it('should throw if formArrayName is used without a control container',
inject(
[TestComponentBuilder, AsyncTestCompleter],
@ -1194,58 +1246,82 @@ export function main() {
});
}));
it('should throw if formControlName is used with the wrong control container',
it('should throw if ngModel is used alone under formGroup',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<input type="text" formControlName="login">
</form>`;
const t = `<div [formGroup]="myGroup">
<input type="text" [(ngModel)]="data">
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.myGroup = new FormGroup({});
;
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formControlName must be used with a parent formGroup directive.`));
`ngModel cannot be used to register form controls with a parent formGroup directive.`));
async.done();
});
}));
it('should throw if formGroupName is used with the wrong control container',
it('should not throw if ngModel is used alone under formGroup with standalone: true',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<div formGroupName="person">
<input type="text" formControlName="login">
</div>
</form>`;
const t = `<div [formGroup]="myGroup">
<input type="text" [(ngModel)]="data" [ngModelOptions]="{standalone: true}">
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formGroupName must be used with a parent formGroup directive.`));
fixture.debugElement.componentInstance.myGroup = new FormGroup({});
expect(() => fixture.detectChanges()).not.toThrowError();
async.done();
});
}));
it('should throw if formArrayName is used with the wrong control container',
it('should throw if ngModel is used alone with formGroupName',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<div formArrayName="person">
<input type="text" formControlName="login">
</div>
</form>`;
const t = `<div [formGroup]="myGroup">
<div formGroupName="person">
<input type="text" [(ngModel)]="data">
</div>
</div>`;
const myGroup = new FormGroup({person: new FormGroup({})});
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.myGroup =
new FormGroup({person: new FormGroup({})});
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formArrayName must be used with a parent formGroup directive.`));
`ngModel cannot be used to register form controls with a parent formGroupName or formArrayName directive.`));
async.done();
});
}));
it('should throw if ngModelGroup is used with formGroup',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<div [formGroup]="myGroup">
<div ngModelGroup="person">
<input type="text" [(ngModel)]="data">
</div>
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.myGroup = new FormGroup({});
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`ngModelGroup cannot be used with a parent formGroup directive`));
async.done();
});
}));
it('should throw if radio button name does not match formControlName attr',
inject(
[TestComponentBuilder, AsyncTestCompleter],