diff --git a/modules/@angular/common/src/forms/directives/form_interface.ts b/modules/@angular/common/src/forms/directives/form_interface.ts index fe16d01627..0eeb3913cc 100644 --- a/modules/@angular/common/src/forms/directives/form_interface.ts +++ b/modules/@angular/common/src/forms/directives/form_interface.ts @@ -15,7 +15,7 @@ export interface Form { /** * Add a control to this form. */ - addControl(dir: NgControl): void; + addControl(dir: NgControl): Control; /** * Remove a control from this form. diff --git a/modules/@angular/common/src/forms/directives/ng_form.ts b/modules/@angular/common/src/forms/directives/ng_form.ts index 71f2773f7f..5aa59d0893 100644 --- a/modules/@angular/common/src/forms/directives/ng_form.ts +++ b/modules/@angular/common/src/forms/directives/ng_form.ts @@ -109,14 +109,15 @@ export class NgForm extends ControlContainer implements Form { get controls(): {[key: string]: AbstractControl} { return this.form.controls; } - addControl(dir: NgControl): void { + addControl(dir: NgControl): Control { + const ctrl = new Control(); PromiseWrapper.scheduleMicrotask(() => { - var container = this._findContainer(dir.path); - var ctrl = new Control(); + const container = this._findContainer(dir.path); setUpControl(ctrl, dir); container.registerControl(dir.name, ctrl); ctrl.updateValueAndValidity({emitEvent: false}); }); + return ctrl; } getControl(dir: NgControl): Control { return this.form.find(dir.path); } diff --git a/modules/@angular/common/src/forms/directives/ng_form_model.ts b/modules/@angular/common/src/forms/directives/ng_form_model.ts index 30b0669280..7d29b8d150 100644 --- a/modules/@angular/common/src/forms/directives/ng_form_model.ts +++ b/modules/@angular/common/src/forms/directives/ng_form_model.ts @@ -138,11 +138,12 @@ export class NgFormModel extends ControlContainer implements Form, get path(): string[] { return []; } - addControl(dir: NgControl): void { - var ctrl: any = this.form.find(dir.path); + addControl(dir: NgControl): Control { + const ctrl: any = this.form.find(dir.path); setUpControl(ctrl, dir); ctrl.updateValueAndValidity({emitEvent: false}); this.directives.push(dir); + return ctrl; } getControl(dir: NgControl): Control { return this.form.find(dir.path); } diff --git a/modules/@angular/common/src/forms/directives/ng_model.ts b/modules/@angular/common/src/forms/directives/ng_model.ts index a5205903e4..59bb929d07 100644 --- a/modules/@angular/common/src/forms/directives/ng_model.ts +++ b/modules/@angular/common/src/forms/directives/ng_model.ts @@ -1,12 +1,14 @@ -import {Directive, Inject, OnChanges, Optional, Self, SimpleChanges, forwardRef} from '@angular/core'; +import {Directive, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core'; import {EventEmitter, ObservableWrapper} from '../../facade/async'; +import {BaseException} from '../../facade/exceptions'; import {Control} from '../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; +import {ControlContainer} from './control_container'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; import {NgControl} from './ng_control'; -import {composeAsyncValidators, composeValidators, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared'; +import {composeAsyncValidators, composeValidators, controlPath, isPropertyUpdated, selectValueAccessor, setUpControl} from './shared'; import {AsyncValidatorFn, ValidatorFn} from './validators'; export const formControlBinding: any = @@ -41,33 +43,34 @@ export const formControlBinding: any = @Directive({ selector: '[ngModel]:not([ngControl]):not([ngFormControl])', providers: [formControlBinding], - inputs: ['model: ngModel'], - outputs: ['update: ngModelChange'], exportAs: 'ngForm' }) -export class NgModel extends NgControl implements OnChanges { +export class NgModel extends NgControl implements OnChanges, + OnDestroy { /** @internal */ - _control = new Control(); + _control: Control; /** @internal */ _added = false; - update = new EventEmitter(); - model: any; viewModel: any; - constructor(@Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], + @Input('ngModel') model: any; + @Input() name: string; + + @Output('ngModelChange') update = new EventEmitter(); + + constructor(@Optional() @Host() private _parent: ControlContainer, + @Optional() @Self() @Inject(NG_VALIDATORS) private _validators: any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private _asyncValidators: any[], @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) { super(); this.valueAccessor = selectValueAccessor(this, valueAccessors); + if (!this._parent) this._control = new Control(); } ngOnChanges(changes: SimpleChanges) { - if (!this._added) { - setUpControl(this._control, this); - this._control.updateValueAndValidity({emitEvent: false}); - this._added = true; - } + this._checkName(); + if (!this._added) this._addControl(); if (isPropertyUpdated(changes, this.viewModel)) { this._control.updateValue(this.model); @@ -75,9 +78,15 @@ export class NgModel extends NgControl implements OnChanges { } } + ngOnDestroy(): void { this.formDirective && this.formDirective.removeControl(this); } + get control(): Control { return this._control; } - get path(): string[] { return []; } + get path(): string[] { + return this._parent ? controlPath(this.name, this._parent) : []; + } + + get formDirective(): any { return this._parent ? this._parent.formDirective : null; } get validator(): ValidatorFn { return composeValidators(this._validators); } @@ -89,4 +98,24 @@ export class NgModel extends NgControl implements OnChanges { this.viewModel = newValue; ObservableWrapper.callEmit(this.update, newValue); } + + private _addControl(): void { + this._control = this.formDirective ? this.formDirective.addControl(this) : + this._addStandaloneControl(); + this._added = true; + } + + private _addStandaloneControl(): Control { + setUpControl(this._control, this); + this._control.updateValueAndValidity({emitEvent: false}); + return this._control; + } + + private _checkName() { + if (this._parent && !this.name) { + throw new BaseException( + `Name attribute must be set if ngModel is used within a form. + Example: `); + } + } } diff --git a/modules/@angular/common/test/forms-deprecated/directives_spec.ts b/modules/@angular/common/test/forms-deprecated/directives_spec.ts index 84ad94971d..7d0a3669d2 100644 --- a/modules/@angular/common/test/forms-deprecated/directives_spec.ts +++ b/modules/@angular/common/test/forms-deprecated/directives_spec.ts @@ -4,7 +4,7 @@ import {fakeAsync, flushMicrotasks, Log, tick,} from '@angular/core/testing'; import {SpyNgControl, SpyValueAccessor} from '../spies'; -import {ControlGroup, Control, NgControlName, NgControlGroup, NgFormModel, ControlValueAccessor, Validators, NgForm, NgModel, NgFormControl, NgControl, DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, Validator} from '@angular/common'; +import {ControlGroup, Control, NgControlName, NgControlGroup, NgFormModel, ControlValueAccessor, Validators, NgForm, NgModel, NgFormControl, NgControl, DefaultValueAccessor, CheckboxControlValueAccessor, SelectControlValueAccessor, Validator} from '@angular/common/src/forms-deprecated'; import {selectValueAccessor, composeValidators} from '@angular/common/src/forms-deprecated/directives/shared'; diff --git a/modules/@angular/common/test/forms/directives_spec.ts b/modules/@angular/common/test/forms/directives_spec.ts index cd566c085f..1b44609d5f 100644 --- a/modules/@angular/common/test/forms/directives_spec.ts +++ b/modules/@angular/common/test/forms/directives_spec.ts @@ -409,8 +409,8 @@ export function main() { var ngModel: any /** TODO #9100 */; beforeEach(() => { - ngModel = - new NgModel([Validators.required], [asyncValidator('expected')], [defaultAccessor]); + ngModel = new NgModel( + null, [Validators.required], [asyncValidator('expected')], [defaultAccessor]); ngModel.valueAccessor = new DummyControlValueAccessor(); }); diff --git a/modules/@angular/common/test/forms/integration_spec.ts b/modules/@angular/common/test/forms/integration_spec.ts index 8200721057..17d9f8b3a8 100644 --- a/modules/@angular/common/test/forms/integration_spec.ts +++ b/modules/@angular/common/test/forms/integration_spec.ts @@ -1,4 +1,5 @@ -import {Control, ControlGroup, ControlValueAccessor, FORM_DIRECTIVES, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgFor, NgForm, NgIf, RadioButtonState, Validator, Validators} from '@angular/common'; +import {NgFor, NgIf} from '@angular/common'; +import {Control, ControlGroup, ControlValueAccessor, FORM_DIRECTIVES, FORM_PROVIDERS, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NgControl, NgForm, NgModel, RadioButtonState, Validator, Validators} from '@angular/common/src/forms'; import {TestComponentBuilder} from '@angular/compiler/testing'; import {ComponentFixture} from '@angular/compiler/testing'; import {Component, Directive, EventEmitter, Output} from '@angular/core'; @@ -812,31 +813,32 @@ export function main() { }); })); - it('should support custom value accessors on non builtin input elements that fire a change event without a \'target\' property', - inject( - [TestComponentBuilder, AsyncTestCompleter], - (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { - var t = `
+ // TODO(kara): Revisit when re-writing to ngModelOptions + xit('should support custom value accessors on non builtin input elements that fire a change event without a \'target\' property', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + var t = `
`; - tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { - fixture.debugElement.componentInstance.form = - new ControlGroup({'name': new Control('aa')}); - fixture.detectChanges(); - var input = fixture.debugElement.query(By.css('my-input')); - expect(input.componentInstance.value).toEqual('!aa!'); + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + fixture.debugElement.componentInstance.form = + new ControlGroup({'name': new Control('aa')}); + fixture.detectChanges(); + var input = fixture.debugElement.query(By.css('my-input')); + expect(input.componentInstance.value).toEqual('!aa!'); - input.componentInstance.value = '!bb!'; - ObservableWrapper.subscribe(input.componentInstance.onInput, (value) => { - expect(fixture.debugElement.componentInstance.form.value).toEqual({ - 'name': 'bb' - }); - async.done(); - }); - input.componentInstance.dispatchChangeEvent(); - }); - })); + input.componentInstance.value = '!bb!'; + ObservableWrapper.subscribe(input.componentInstance.onInput, (value) => { + expect(fixture.debugElement.componentInstance.form.value).toEqual({ + 'name': 'bb' + }); + async.done(); + }); + input.componentInstance.dispatchChangeEvent(); + }); + })); }); @@ -1220,6 +1222,42 @@ export function main() { expect(fixture.debugElement.componentInstance.name).toEqual('updatedValue'); }))); + it('should support ngModel registration with a parent form', + fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { + const t = ` +
+ +
+ `; + + let fixture = tcb.overrideTemplate(MyComp8, t).createFakeAsync(MyComp8); + tick(); + fixture.debugElement.componentInstance.name = 'Nancy'; + fixture.detectChanges(); + var form = fixture.debugElement.children[0].inject(NgForm); + + tick(); + expect(form.value).toEqual({first: 'Nancy'}); + expect(form.valid).toBe(false); + + }))); + + + it('should throw if ngModel has a parent form but no name attr', + inject( + [TestComponentBuilder, AsyncTestCompleter], + (tcb: TestComponentBuilder, async: AsyncTestCompleter) => { + const t = `
+ +
`; + + tcb.overrideTemplate(MyComp8, t).createAsync(MyComp8).then((fixture) => { + expect(() => fixture.detectChanges()) + .toThrowError(new RegExp(`Name attribute must be set`)); + async.done(); + }); + })); + it('should support ', fakeAsync(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { @@ -1542,7 +1580,8 @@ class UniqLoginValidator implements Validator { template: '', directives: [ FORM_DIRECTIVES, WrappedValue, MyInput, NgIf, NgFor, LoginIsEmptyValidator, UniqLoginValidator - ] + ], + providers: [FORM_PROVIDERS] }) class MyComp8 { form: any;