fix(forms): throw error if wrong control container for reactive forms (#10286)

This commit is contained in:
Kara 2016-07-26 10:08:46 -07:00 committed by GitHub
parent 0d1bf8148b
commit 0aba42ae5b
7 changed files with 238 additions and 40 deletions

View File

@ -118,7 +118,7 @@ export class NgForm extends ControlContainer implements Form {
console.warn(` console.warn(`
*It looks like you're using the old forms module. This will be opt-in in the next RC, and *It looks like you're using the old forms module. This will be opt-in in the next RC, and
will eventually be removed in favor of the new forms module. For more information, see: will eventually be removed in favor of the new forms module. For more information, see:
https://docs.google.com/document/u/1/d/1RIezQqE4aEhBRmArIAS1mRIZtWFf6JxN_7B4meyWK0Y/pub https://docs.google.com/document/d/1RIezQqE4aEhBRmArIAS1mRIZtWFf6JxN_7B4meyWK0Y/preview
`); `);
} }
} }

View File

@ -134,7 +134,7 @@ export class NgFormModel extends ControlContainer implements Form,
console.warn(` console.warn(`
*It looks like you're using the old forms module. This will be opt-in in the next RC, and *It looks like you're using the old forms module. This will be opt-in in the next RC, and
will eventually be removed in favor of the new forms module. For more information, see: will eventually be removed in favor of the new forms module. For more information, see:
https://docs.google.com/document/u/1/d/1RIezQqE4aEhBRmArIAS1mRIZtWFf6JxN_7B4meyWK0Y/pub https://docs.google.com/document/d/1RIezQqE4aEhBRmArIAS1mRIZtWFf6JxN_7B4meyWK0Y/preview
`); `);
} }
} }

View File

@ -29,7 +29,10 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn
/** @internal */ /** @internal */
_asyncValidators: any[]; _asyncValidators: any[];
ngOnInit(): void { this.formDirective.addFormGroup(this); } ngOnInit(): void {
this._checkParentType();
this.formDirective.addFormGroup(this);
}
ngOnDestroy(): void { this.formDirective.removeFormGroup(this); } ngOnDestroy(): void { this.formDirective.removeFormGroup(this); }
@ -51,4 +54,7 @@ export class AbstractFormGroupDirective extends ControlContainer implements OnIn
get validator(): ValidatorFn { return composeValidators(this._validators); } get validator(): ValidatorFn { return composeValidators(this._validators); }
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
/** @internal */
_checkParentType(): void {}
} }

View File

@ -8,6 +8,7 @@
import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core';
import {BaseException} from '../../facade/exceptions';
import {FormArray} from '../../model'; import {FormArray} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {ControlContainer} from '../control_container'; import {ControlContainer} from '../control_container';
@ -15,6 +16,7 @@ import {composeAsyncValidators, composeValidators, controlPath} from '../shared'
import {AsyncValidatorFn, ValidatorFn} from '../validators'; import {AsyncValidatorFn, ValidatorFn} from '../validators';
import {FormGroupDirective} from './form_group_directive'; import {FormGroupDirective} from './form_group_directive';
import {FormGroupName} from './form_group_name';
export const formArrayNameProvider: any = export const formArrayNameProvider: any =
/*@ts2dart_const*/ /* @ts2dart_Provider */ { /*@ts2dart_const*/ /* @ts2dart_Provider */ {
@ -71,7 +73,7 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
@Input('formArrayName') name: string; @Input('formArrayName') name: string;
constructor( constructor(
@Host() @SkipSelf() parent: ControlContainer, @Optional() @Host() @SkipSelf() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super(); super();
@ -80,7 +82,10 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
this._asyncValidators = asyncValidators; this._asyncValidators = asyncValidators;
} }
ngOnInit(): void { this.formDirective.addFormArray(this); } ngOnInit(): void {
this._checkParentType();
this.formDirective.addFormArray(this);
}
ngOnDestroy(): void { this.formDirective.removeFormArray(this); } ngOnDestroy(): void { this.formDirective.removeFormArray(this); }
@ -93,4 +98,31 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
get validator(): ValidatorFn { return composeValidators(this._validators); } get validator(): ValidatorFn { return composeValidators(this._validators); }
get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); } get asyncValidator(): AsyncValidatorFn { return composeAsyncValidators(this._asyncValidators); }
private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective)) {
this._throwParentException();
}
}
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

@ -9,15 +9,19 @@
import {Directive, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf, forwardRef} from '@angular/core'; import {Directive, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, SkipSelf, forwardRef} from '@angular/core';
import {EventEmitter, ObservableWrapper} from '../../facade/async'; import {EventEmitter, ObservableWrapper} from '../../facade/async';
import {BaseException} from '../../facade/exceptions';
import {FormControl} from '../../model'; import {FormControl} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {ControlContainer} from '../control_container'; import {ControlContainer} from '../control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '../control_value_accessor';
import {NgControl} from '../ng_control'; import {NgControl} from '../ng_control';
import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared'; import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor} from '../shared';
import {AsyncValidatorFn, ValidatorFn} from '../validators'; import {AsyncValidatorFn, ValidatorFn} from '../validators';
import {FormArrayName} from './form_array_name';
import {FormGroupDirective} from './form_group_directive';
import {FormGroupName} from './form_group_name';
export const controlNameBinding: any = export const controlNameBinding: any =
/*@ts2dart_const*/ /* @ts2dart_Provider */ { /*@ts2dart_const*/ /* @ts2dart_Provider */ {
@ -105,7 +109,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
@Input('ngModel') model: any; @Input('ngModel') model: any;
@Output('ngModelChange') update = new EventEmitter(); @Output('ngModelChange') update = new EventEmitter();
constructor(@Host() @SkipSelf() private _parent: ControlContainer, constructor(@Optional() @Host() @SkipSelf() private _parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: @Optional() @Self() @Inject(NG_VALIDATORS) private _validators:
/* Array<Validator|Function> */ any[], /* Array<Validator|Function> */ any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators:
@ -118,6 +122,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (!this._added) { if (!this._added) {
this._checkParentType();
this.formDirective.addControl(this); this.formDirective.addControl(this);
this._added = true; this._added = true;
} }
@ -145,4 +150,29 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
} }
get control(): FormControl { return this.formDirective.getControl(this); } get control(): FormControl { return this.formDirective.getControl(this); }
private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) &&
!(this._parent instanceof FormGroupDirective) &&
!(this._parent instanceof FormArrayName)) {
this._throwParentException();
}
}
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

@ -7,10 +7,14 @@
*/ */
import {Directive, Host, Inject, Input, OnDestroy, OnInit, Optional, Self, SkipSelf, forwardRef} from '@angular/core'; 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 {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../../validators';
import {AbstractFormGroupDirective} from '../abstract_form_group_directive'; import {AbstractFormGroupDirective} from '../abstract_form_group_directive';
import {ControlContainer} from '../control_container'; import {ControlContainer} from '../control_container';
import {FormGroupDirective} from './form_group_directive';
export const formGroupNameProvider: any = export const formGroupNameProvider: any =
/*@ts2dart_const*/ /* @ts2dart_Provider */ { /*@ts2dart_const*/ /* @ts2dart_Provider */ {
provide: ControlContainer, provide: ControlContainer,
@ -70,7 +74,7 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
@Input('formGroupName') name: string; @Input('formGroupName') name: string;
constructor( constructor(
@Host() @SkipSelf() parent: ControlContainer, @Optional() @Host() @SkipSelf() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
super(); super();
@ -78,4 +82,29 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit,
this._validators = validators; this._validators = validators;
this._asyncValidators = asyncValidators; this._asyncValidators = asyncValidators;
} }
/** @internal */
_checkParentType(): void {
if (!(this._parent instanceof FormGroupName) && !(this._parent instanceof FormGroupDirective)) {
this._throwParentException();
}
}
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

@ -43,21 +43,6 @@ export function main() {
}); });
})); }));
it('should throw if a form isn\'t passed into formGroup',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<div [formGroup]="form">
<input type="text" formControlName="login">
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`formGroup expects a FormGroup instance`));
async.done();
});
}));
it('should update the form group values on DOM change', it('should update the form group values on DOM change',
inject( inject(
[TestComponentBuilder, AsyncTestCompleter], [TestComponentBuilder, AsyncTestCompleter],
@ -623,23 +608,6 @@ export function main() {
}); });
})); }));
it('should throw if radio button name does not match formControlName attr',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form [formGroup]="form">
<input type="radio" formControlName="food" name="drink" value="chicken">
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.form =
new FormGroup({'food': new FormControl('fish')});
expect(() => fixture.detectChanges())
.toThrowError(new RegExp('If you define both a name and a formControlName'));
async.done();
});
}));
it('should support removing controls from <type=radio>', it('should support removing controls from <type=radio>',
inject( inject(
[TestComponentBuilder, AsyncTestCompleter], [TestComponentBuilder, AsyncTestCompleter],
@ -1162,6 +1130,139 @@ export function main() {
// selection start has not changed because we did not reset the value // selection start has not changed because we did not reset the value
expect(input.selectionStart).toEqual(1); expect(input.selectionStart).toEqual(1);
}))); })));
describe('errors', () => {
it('should throw if a form isn\'t passed into formGroup',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<div [formGroup]="form">
<input type="text" formControlName="login">
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`formGroup expects a FormGroup instance`));
async.done();
});
}));
it('should throw if formControlName is used without a control container',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<input type="text" formControlName="login">`;
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 formGroupName is used without a control container',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<div formGroupName="person">
<input type="text" formControlName="login">
</div>`;
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],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<div formArrayName="cities">
<input type="text" formControlName="login">
</div>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formArrayName must be used with a parent formGroup directive`));
async.done();
});
}));
it('should throw if formControlName is used with the wrong control container',
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 formGroupName is used with the wrong control container',
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 with the wrong control container',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<div formArrayName="person">
<input type="text" formControlName="login">
</div>
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
expect(() => fixture.detectChanges())
.toThrowError(new RegExp(
`formArrayName must be used with a parent formGroup directive.`));
async.done();
});
}));
it('should throw if radio button name does not match formControlName attr',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form [formGroup]="form">
<input type="radio" formControlName="food" name="drink" value="chicken">
</form>`;
tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => {
fixture.debugElement.componentInstance.form =
new FormGroup({'food': new FormControl('fish')});
expect(() => fixture.detectChanges())
.toThrowError(new RegExp('If you define both a name and a formControlName'));
async.done();
});
}));
});
}); });
} }