feat(forms): add support for standalone ngModel dirs inside forms

Closes #9230
This commit is contained in:
Kara Erickson 2016-06-23 09:55:26 -07:00
parent 826f89f862
commit 6edf0474cc
6 changed files with 68 additions and 25 deletions

View File

@ -24,7 +24,7 @@ export interface Form {
/** /**
* Add a control to this form. * Add a control to this form.
*/ */
addControl(dir: NgControl): FormControl; addControl(dir: NgControl): void;
/** /**
* Remove a control from this form. * Remove a control from this form.

View File

@ -116,16 +116,13 @@ export class NgForm extends ControlContainer implements Form {
get controls(): {[key: string]: AbstractControl} { return this.form.controls; } get controls(): {[key: string]: AbstractControl} { return this.form.controls; }
addControl(dir: NgModel): FormControl { addControl(dir: NgModel): void {
const ctrl = new FormControl();
PromiseWrapper.scheduleMicrotask(() => { PromiseWrapper.scheduleMicrotask(() => {
const container = this._findContainer(dir.path); const container = this._findContainer(dir.path);
dir._control = <FormControl>container.registerControl(dir.name, ctrl); dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir); setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false}); dir.control.updateValueAndValidity({emitEvent: false});
}); });
return ctrl;
} }
getControl(dir: NgModel): FormControl { return <FormControl>this.form.find(dir.path); } getControl(dir: NgModel): FormControl { return <FormControl>this.form.find(dir.path); }

View File

@ -55,14 +55,14 @@ export const formControlBinding: any =
export class NgModel extends NgControl implements OnChanges, export class NgModel extends NgControl implements OnChanges,
OnDestroy { OnDestroy {
/** @internal */ /** @internal */
_control: FormControl; _control = new FormControl();
/** @internal */ /** @internal */
_added = false; _registered = false;
viewModel: any; viewModel: any;
@Input('ngModel') model: any; @Input('ngModel') model: any;
@Input() name: string; @Input() name: string;
@Input('ngModelOptions') options: {name?: string}; @Input('ngModelOptions') options: {name?: string, standalone?: boolean};
@Output('ngModelChange') update = new EventEmitter(); @Output('ngModelChange') update = new EventEmitter();
constructor(@Optional() @Host() private _parent: ControlContainer, constructor(@Optional() @Host() private _parent: ControlContainer,
@ -72,12 +72,11 @@ export class NgModel extends NgControl implements OnChanges,
valueAccessors: ControlValueAccessor[]) { valueAccessors: ControlValueAccessor[]) {
super(); super();
this.valueAccessor = selectValueAccessor(this, valueAccessors); this.valueAccessor = selectValueAccessor(this, valueAccessors);
if (!this._parent) this._control = new FormControl();
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
this._checkName(); this._checkName();
if (!this._added) this._addControl(); if (!this._registered) this._setUpControl();
if (isPropertyUpdated(changes, this.viewModel)) { if (isPropertyUpdated(changes, this.viewModel)) {
this._control.updateValue(this.model); this._control.updateValue(this.model);
@ -106,25 +105,32 @@ export class NgModel extends NgControl implements OnChanges,
ObservableWrapper.callEmit(this.update, newValue); ObservableWrapper.callEmit(this.update, newValue);
} }
private _addControl(): void { private _setUpControl(): void {
this._control = this.formDirective ? this.formDirective.addControl(this) : this._isStandalone() ? this._setUpStandalone() :
this._addStandaloneControl(); this.formDirective.addControl(this);
this._added = true; this._registered = true;
} }
private _addStandaloneControl(): FormControl { private _isStandalone(): boolean {
return !this._parent || (this.options && this.options.standalone);
}
private _setUpStandalone(): void {
setUpControl(this._control, this); setUpControl(this._control, this);
this._control.updateValueAndValidity({emitEvent: false}); this._control.updateValueAndValidity({emitEvent: false});
return this._control;
} }
private _checkName() { private _checkName() {
if (this.options && this.options.name) this.name = this.options.name; if (this.options && this.options.name) this.name = this.options.name;
if (this._parent && !this.name) { if (!this._isStandalone() && !this.name) {
throw new BaseException( throw new BaseException(
`Name attribute must be set if ngModel is used within a form. `If ngModel is used within a form tag, either the name attribute must be set
Example: <input [(ngModel)]="person.firstName" name="first">`); 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}">
`);
} }
} }
} }

View File

@ -144,12 +144,11 @@ export class FormGroupDirective extends ControlContainer implements Form,
get path(): string[] { return []; } get path(): string[] { return []; }
addControl(dir: NgControl): FormControl { addControl(dir: NgControl): void {
const ctrl: any = this.form.find(dir.path); const ctrl: any = this.form.find(dir.path);
setUpControl(ctrl, dir); setUpControl(ctrl, dir);
ctrl.updateValueAndValidity({emitEvent: false}); ctrl.updateValueAndValidity({emitEvent: false});
this.directives.push(dir); this.directives.push(dir);
return ctrl;
} }
getControl(dir: NgControl): FormControl { return <FormControl>this.form.find(dir.path); } getControl(dir: NgControl): FormControl { return <FormControl>this.form.find(dir.path); }

View File

@ -281,7 +281,7 @@ export function main() {
personControlGroupDir = new NgModelGroup(form, [], []); personControlGroupDir = new NgModelGroup(form, [], []);
personControlGroupDir.name = 'person'; personControlGroupDir.name = 'person';
loginControlDir = new FormControlName(personControlGroupDir, null, null, [defaultAccessor]); loginControlDir = new NgModel(personControlGroupDir, null, null, [defaultAccessor]);
loginControlDir.name = 'login'; loginControlDir.name = 'login';
loginControlDir.valueAccessor = new DummyControlValueAccessor(); loginControlDir.valueAccessor = new DummyControlValueAccessor();
}); });

View File

@ -1371,7 +1371,7 @@ export function main() {
}))); })));
it('should throw if ngModel has a parent form but no name attr', it('should throw if ngModel has a parent form but no name attr or standalone label',
inject( inject(
[TestComponentBuilder, AsyncTestCompleter], [TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => { (tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
@ -1384,11 +1384,29 @@ export function main() {
.createAsync(MyComp8) .createAsync(MyComp8)
.then((fixture) => { .then((fixture) => {
expect(() => fixture.detectChanges()) expect(() => fixture.detectChanges())
.toThrowError(new RegExp(`Name attribute must be set`)); .toThrowError(new RegExp(`name attribute must be set`));
async.done(); async.done();
}); });
})); }));
it('should not throw if ngModel has a parent form, no name attr, and a standalone label',
inject(
[TestComponentBuilder, AsyncTestCompleter],
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
const t = `<form>
<input [(ngModel)]="name" [ngModelOptions]="{standalone: true}">
</form>`;
tcb.overrideTemplate(MyComp8, t)
.overrideProviders(MyComp8, providerArr)
.createAsync(MyComp8)
.then((fixture) => {
expect(() => fixture.detectChanges()).not.toThrow();
async.done();
});
}));
it('should override name attribute with ngModelOptions name if provided', it('should override name attribute with ngModelOptions name if provided',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = ` const t = `
@ -1407,6 +1425,29 @@ export function main() {
expect(form.value).toEqual({two: 'some data'}); expect(form.value).toEqual({two: 'some data'});
}))); })));
it('should not register standalone ngModels with parent form',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = `
<form>
<input name="one" [(ngModel)]="data">
<input [(ngModel)]="list" [ngModelOptions]="{standalone: true}">
</form>
`;
const fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8);
tick();
fixture.debugElement.componentInstance.data = 'some data';
fixture.debugElement.componentInstance.list = 'should not show';
fixture.detectChanges();
const form = fixture.debugElement.children[0].injector.get(NgForm);
const inputs = fixture.debugElement.queryAll(By.css('input'));
tick();
expect(form.value).toEqual({one: 'some data'});
expect(inputs[1].nativeElement.value).toEqual('should not show');
})));
it('should support <type=radio>', it('should support <type=radio>',
fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
const t = `<form> const t = `<form>