diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts index 25ed3e9fa8..184d9c7160 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_control_name.ts @@ -96,9 +96,11 @@ export const controlNameBinding: any = { */ @Directive({selector: '[formControlName]', providers: [controlNameBinding]}) export class FormControlName extends NgControl implements OnChanges, OnDestroy { + private _added = false; /** @internal */ viewModel: any; - private _added = false; + /** @internal */ + _control: FormControl; @Input('formControlName') name: string; @@ -122,12 +124,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { } ngOnChanges(changes: SimpleChanges) { - if (!this._added) { - this._checkParentType(); - this.formDirective.addControl(this); - if (this.control.disabled) this.valueAccessor.setDisabledState(true); - this._added = true; - } + if (!this._added) this._setUpControl(); if (isPropertyUpdated(changes, this.viewModel)) { this.viewModel = this.model; this.formDirective.updateModel(this, this.model); @@ -155,7 +152,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { return composeAsyncValidators(this._rawAsyncValidators); } - get control(): FormControl { return this.formDirective.getControl(this); } + get control(): FormControl { return this._control; } private _checkParentType(): void { if (!(this._parent instanceof FormGroupName) && @@ -167,4 +164,11 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy { ReactiveErrors.controlParentException(); } } + + private _setUpControl() { + this._checkParentType(); + this._control = this.formDirective.addControl(this); + if (this.control.disabled) this.valueAccessor.setDisabledState(true); + this._added = true; + } } diff --git a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts index b99a3536b5..c75b16662f 100644 --- a/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/modules/@angular/forms/src/directives/reactive_directives/form_group_directive.ts @@ -15,10 +15,10 @@ import {FormArray, FormControl, FormGroup} from '../../model'; 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 {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared'; +import {FormControlName} from './form_control_name'; import {FormArrayName, FormGroupName} from './form_group_name'; export const formDirectiveProvider: any = { @@ -105,7 +105,8 @@ export const formDirectiveProvider: any = { export class FormGroupDirective extends ControlContainer implements Form, OnChanges { private _submitted: boolean = false; - directives: NgControl[] = []; + private _oldForm: FormGroup; + directives: FormControlName[] = []; @Input('formGroup') form: FormGroup = null; @Output() ngSubmit = new EventEmitter(); @@ -119,12 +120,9 @@ export class FormGroupDirective extends ControlContainer implements Form, ngOnChanges(changes: SimpleChanges): void { this._checkFormPresent(); if (StringMapWrapper.contains(changes, 'form')) { - var sync = composeValidators(this._validators); - this.form.validator = Validators.compose([this.form.validator, sync]); - - var async = composeAsyncValidators(this._asyncValidators); - this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]); - this._updateDomValue(changes); + this._updateValidators(); + this._updateDomValue(); + this._updateRegistrations(); } } @@ -136,16 +134,17 @@ export class FormGroupDirective extends ControlContainer implements Form, get path(): string[] { return []; } - addControl(dir: NgControl): void { + addControl(dir: FormControlName): FormControl { const ctrl: any = this.form.get(dir.path); setUpControl(ctrl, dir); ctrl.updateValueAndValidity({emitEvent: false}); this.directives.push(dir); + return ctrl; } - getControl(dir: NgControl): FormControl { return this.form.get(dir.path); } + getControl(dir: FormControlName): FormControl { return this.form.get(dir.path); } - removeControl(dir: NgControl): void { ListWrapper.remove(this.directives, dir); } + removeControl(dir: FormControlName): void { ListWrapper.remove(this.directives, dir); } addFormGroup(dir: FormGroupName): void { var ctrl: any = this.form.get(dir.path); @@ -167,7 +166,7 @@ export class FormGroupDirective extends ControlContainer implements Form, getFormArray(dir: FormArrayName): FormArray { return this.form.get(dir.path); } - updateModel(dir: NgControl, value: any): void { + updateModel(dir: FormControlName, value: any): void { var ctrl  = this.form.get(dir.path); ctrl.setValue(value); } @@ -186,21 +185,33 @@ export class FormGroupDirective extends ControlContainer implements Form, } /** @internal */ - _updateDomValue(changes: SimpleChanges) { - const oldForm = changes['form'].previousValue; - + _updateDomValue() { this.directives.forEach(dir => { const newCtrl: any = this.form.get(dir.path); - const oldCtrl = oldForm.get(dir.path); - if (oldCtrl !== newCtrl) { - cleanUpControl(oldCtrl, dir); + if (dir._control !== newCtrl) { + cleanUpControl(dir._control, dir); if (newCtrl) setUpControl(newCtrl, dir); + dir._control = newCtrl; } }); this.form._updateTreeValidity({emitEvent: false}); } + private _updateRegistrations() { + this.form._registerOnCollectionChange(() => this._updateDomValue()); + if (this._oldForm) this._oldForm._registerOnCollectionChange(() => {}); + this._oldForm = this.form; + } + + private _updateValidators() { + const sync = composeValidators(this._validators); + this.form.validator = Validators.compose([this.form.validator, sync]); + + const async = composeAsyncValidators(this._asyncValidators); + this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]); + } + private _checkFormPresent() { if (isBlank(this.form)) { ReactiveErrors.missingFormException(); diff --git a/modules/@angular/forms/src/model.ts b/modules/@angular/forms/src/model.ts index 8ae8b0583e..4b57be84aa 100644 --- a/modules/@angular/forms/src/model.ts +++ b/modules/@angular/forms/src/model.ts @@ -81,6 +81,8 @@ function coerceToAsyncValidator(asyncValidator: AsyncValidatorFn | AsyncValidato export abstract class AbstractControl { /** @internal */ _value: any; + /** @internal */ + _onCollectionChange = () => {}; private _valueChanges: EventEmitter; private _statusChanges: EventEmitter; @@ -420,6 +422,9 @@ export abstract class AbstractControl { return isStringMap(formState) && Object.keys(formState).length === 2 && 'value' in formState && 'disabled' in formState; } + + /** @internal */ + _registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; } } /** @@ -530,6 +535,7 @@ export class FormControl extends AbstractControl { _clearChangeFns(): void { this._onChange = []; this._onDisabledChange = null; + this._onCollectionChange = () => {}; } /** @@ -574,7 +580,7 @@ export class FormGroup extends AbstractControl { asyncValidator: AsyncValidatorFn = null) { super(validator, asyncValidator); this._initObservables(); - this._setParentForControls(); + this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); } @@ -585,6 +591,7 @@ export class FormGroup extends AbstractControl { if (this.controls[name]) return this.controls[name]; this.controls[name] = control; control.setParent(this); + control._registerOnCollectionChange(this._onCollectionChange); return control; } @@ -594,14 +601,28 @@ export class FormGroup extends AbstractControl { addControl(name: string, control: AbstractControl): void { this.registerControl(name, control); this.updateValueAndValidity(); + this._onCollectionChange(); } /** * Remove a control from this group. */ removeControl(name: string): void { + if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {}); StringMapWrapper.delete(this.controls, name); this.updateValueAndValidity(); + this._onCollectionChange(); + } + + /** + * Replace an existing control. + */ + setControl(name: string, control: AbstractControl): void { + if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {}); + StringMapWrapper.delete(this.controls, name); + if (control) this.registerControl(name, control); + this.updateValueAndValidity(); + this._onCollectionChange(); } /** @@ -666,8 +687,11 @@ export class FormGroup extends AbstractControl { } /** @internal */ - _setParentForControls() { - this._forEachChild((control: AbstractControl, name: string) => { control.setParent(this); }); + _setUpControls() { + this._forEachChild((control: AbstractControl) => { + control.setParent(this); + control._registerOnCollectionChange(this._onCollectionChange); + }); } /** @internal */ @@ -750,7 +774,7 @@ export class FormArray extends AbstractControl { asyncValidator: AsyncValidatorFn = null) { super(validator, asyncValidator); this._initObservables(); - this._setParentForControls(); + this._setUpControls(); this.updateValueAndValidity({onlySelf: true, emitEvent: false}); } @@ -764,8 +788,9 @@ export class FormArray extends AbstractControl { */ push(control: AbstractControl): void { this.controls.push(control); - control.setParent(this); + this._registerControl(control); this.updateValueAndValidity(); + this._onCollectionChange(); } /** @@ -773,16 +798,35 @@ export class FormArray extends AbstractControl { */ insert(index: number, control: AbstractControl): void { ListWrapper.insert(this.controls, index, control); - control.setParent(this); + this._registerControl(control); this.updateValueAndValidity(); + this._onCollectionChange(); } /** * Remove the control at the given `index` in the array. */ removeAt(index: number): void { + if (this.controls[index]) this.controls[index]._registerOnCollectionChange(() => {}); ListWrapper.removeAt(this.controls, index); this.updateValueAndValidity(); + this._onCollectionChange(); + } + + /** + * Replace an existing control. + */ + setControl(index: number, control: AbstractControl): void { + if (this.controls[index]) this.controls[index]._registerOnCollectionChange(() => {}); + ListWrapper.removeAt(this.controls, index); + + if (control) { + ListWrapper.insert(this.controls, index, control); + this._registerControl(control); + } + + this.updateValueAndValidity(); + this._onCollectionChange(); } /** @@ -849,8 +893,8 @@ export class FormArray extends AbstractControl { } /** @internal */ - _setParentForControls(): void { - this._forEachChild((control: AbstractControl) => { control.setParent(this); }); + _setUpControls(): void { + this._forEachChild((control: AbstractControl) => this._registerControl(control)); } /** @internal */ @@ -869,4 +913,9 @@ export class FormArray extends AbstractControl { } return !!this.controls.length; } + + private _registerControl(control: AbstractControl) { + control.setParent(this); + control._registerOnCollectionChange(this._onCollectionChange); + } } diff --git a/modules/@angular/forms/test/directives_spec.ts b/modules/@angular/forms/test/directives_spec.ts index e691e77ea7..877d1ec56b 100644 --- a/modules/@angular/forms/test/directives_spec.ts +++ b/modules/@angular/forms/test/directives_spec.ts @@ -552,6 +552,7 @@ export function main() { parent.form = new FormGroup({'name': formModel}); controlNameDir = new FormControlName(parent, [], [], [defaultAccessor]); controlNameDir.name = 'name'; + controlNameDir._control = formModel; }); it('should reexport control properties', () => { diff --git a/modules/@angular/forms/test/form_array_spec.ts b/modules/@angular/forms/test/form_array_spec.ts index b33b870dea..5dff56b18b 100644 --- a/modules/@angular/forms/test/form_array_spec.ts +++ b/modules/@angular/forms/test/form_array_spec.ts @@ -806,6 +806,49 @@ export function main() { }); + describe('setControl()', () => { + let c: FormControl; + let a: FormArray; + + beforeEach(() => { + c = new FormControl('one'); + a = new FormArray([c]); + }); + + it('should replace existing control with new control', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + a.setControl(0, c2); + + expect(a.controls[0]).toEqual(c2); + expect(a.value).toEqual(['new!']); + expect(a.valid).toBe(false); + }); + + it('should add control if control did not exist before', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + a.setControl(1, c2); + + expect(a.controls[1]).toEqual(c2); + expect(a.value).toEqual(['one', 'new!']); + expect(a.valid).toBe(false); + }); + + it('should remove control if new control is null', () => { + a.setControl(0, null); + expect(a.controls[0]).not.toBeDefined(); + expect(a.value).toEqual([]); + }); + + it('should only emit value change event once', () => { + const logger: string[] = []; + const c2 = new FormControl('new!'); + a.valueChanges.subscribe(() => logger.push('change!')); + a.setControl(0, c2); + expect(logger).toEqual(['change!']); + }); + + }); + }); }); } diff --git a/modules/@angular/forms/test/form_group_spec.ts b/modules/@angular/forms/test/form_group_spec.ts index 07aa646ee2..43e795d0d8 100644 --- a/modules/@angular/forms/test/form_group_spec.ts +++ b/modules/@angular/forms/test/form_group_spec.ts @@ -842,6 +842,49 @@ export function main() { }); + describe('setControl()', () => { + let c: FormControl; + let g: FormGroup; + + beforeEach(() => { + c = new FormControl('one'); + g = new FormGroup({one: c}); + }); + + it('should replace existing control with new control', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + g.setControl('one', c2); + + expect(g.controls['one']).toEqual(c2); + expect(g.value).toEqual({one: 'new!'}); + expect(g.valid).toBe(false); + }); + + it('should add control if control did not exist before', () => { + const c2 = new FormControl('new!', Validators.minLength(10)); + g.setControl('two', c2); + + expect(g.controls['two']).toEqual(c2); + expect(g.value).toEqual({one: 'one', two: 'new!'}); + expect(g.valid).toBe(false); + }); + + it('should remove control if new control is null', () => { + g.setControl('one', null); + expect(g.controls['one']).not.toBeDefined(); + expect(g.value).toEqual({}); + }); + + it('should only emit value change event once', () => { + const logger: string[] = []; + const c2 = new FormControl('new!'); + g.valueChanges.subscribe(() => logger.push('change!')); + g.setControl('one', c2); + expect(logger).toEqual(['change!']); + }); + + }); + }); } diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index d7fcef0b10..cc35912ebb 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -256,6 +256,91 @@ export function main() { expect(inputs[2]).not.toBeDefined(); }); + describe('nested control rebinding', () => { + + it('should attach dir to control when leaf control changes', () => { + const form = new FormGroup({'login': new FormControl('oldValue')}); + const fixture = TestBed.createComponent(FormGroupComp); + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + form.removeControl('login'); + form.addControl('login', new FormControl('newValue')); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('newValue'); + + input.nativeElement.value = 'user input'; + dispatchEvent(input.nativeElement, 'input'); + fixture.detectChanges(); + + expect(form.value).toEqual({login: 'user input'}); + + form.setValue({login: 'Carson'}); + fixture.detectChanges(); + expect(input.nativeElement.value).toEqual('Carson'); + }); + + it('should attach dirs to all child controls when group control changes', () => { + const fixture = TestBed.createComponent(NestedFormGroupComp); + const form = new FormGroup({ + signin: new FormGroup( + {login: new FormControl('oldLogin'), password: new FormControl('oldPassword')}) + }); + fixture.debugElement.componentInstance.form = form; + fixture.detectChanges(); + + form.removeControl('signin'); + form.addControl( + 'signin', + new FormGroup( + {login: new FormControl('newLogin'), password: new FormControl('newPassword')})); + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + expect(inputs[0].nativeElement.value).toEqual('newLogin'); + expect(inputs[1].nativeElement.value).toEqual('newPassword'); + + inputs[0].nativeElement.value = 'user input'; + dispatchEvent(inputs[0].nativeElement, 'input'); + fixture.detectChanges(); + + expect(form.value).toEqual({signin: {login: 'user input', password: 'newPassword'}}); + + form.setValue({signin: {login: 'Carson', password: 'Drew'}}); + fixture.detectChanges(); + expect(inputs[0].nativeElement.value).toEqual('Carson'); + expect(inputs[1].nativeElement.value).toEqual('Drew'); + }); + + it('should attach dirs to all present child controls when array control changes', () => { + const fixture = TestBed.createComponent(FormArrayComp); + const cityArray = new FormArray([new FormControl('SF'), new FormControl('NY')]); + const form = new FormGroup({cities: cityArray}); + fixture.debugElement.componentInstance.form = form; + fixture.debugElement.componentInstance.cityArray = cityArray; + fixture.detectChanges(); + + form.removeControl('cities'); + form.addControl('cities', new FormArray([new FormControl('LA')])); + fixture.detectChanges(); + + const input = fixture.debugElement.query(By.css('input')); + expect(input.nativeElement.value).toEqual('LA'); + + input.nativeElement.value = 'MTV'; + dispatchEvent(input.nativeElement, 'input'); + fixture.detectChanges(); + + expect(form.value).toEqual({cities: ['MTV']}); + + form.setValue({cities: ['LA']}); + fixture.detectChanges(); + expect(input.nativeElement.value).toEqual('LA'); + }); + + }); }); diff --git a/tools/public_api_guard/forms/index.d.ts b/tools/public_api_guard/forms/index.d.ts index c27a3a36ed..cbef32528a 100644 --- a/tools/public_api_guard/forms/index.d.ts +++ b/tools/public_api_guard/forms/index.d.ts @@ -167,6 +167,7 @@ export declare class FormArray extends AbstractControl { reset(value?: any, {onlySelf}?: { onlySelf?: boolean; }): void; + setControl(index: number, control: AbstractControl): void; setValue(value: any[], {onlySelf}?: { onlySelf?: boolean; }): void; @@ -272,6 +273,7 @@ export declare class FormGroup extends AbstractControl { reset(value?: any, {onlySelf}?: { onlySelf?: boolean; }): void; + setControl(name: string, control: AbstractControl): void; setValue(value: { [key: string]: any; }, {onlySelf}?: { @@ -282,27 +284,27 @@ export declare class FormGroup extends AbstractControl { /** @stable */ export declare class FormGroupDirective extends ControlContainer implements Form, OnChanges { control: FormGroup; - directives: NgControl[]; + directives: FormControlName[]; form: FormGroup; formDirective: Form; ngSubmit: EventEmitter<{}>; path: string[]; submitted: boolean; constructor(_validators: any[], _asyncValidators: any[]); - addControl(dir: NgControl): void; + addControl(dir: FormControlName): FormControl; addFormArray(dir: FormArrayName): void; addFormGroup(dir: FormGroupName): void; - getControl(dir: NgControl): FormControl; + getControl(dir: FormControlName): FormControl; getFormArray(dir: FormArrayName): FormArray; getFormGroup(dir: FormGroupName): FormGroup; ngOnChanges(changes: SimpleChanges): void; onReset(): void; onSubmit(): boolean; - removeControl(dir: NgControl): void; + removeControl(dir: FormControlName): void; removeFormArray(dir: FormArrayName): void; removeFormGroup(dir: FormGroupName): void; resetForm(value?: any): void; - updateModel(dir: NgControl, value: any): void; + updateModel(dir: FormControlName, value: any): void; } /** @stable */