Skip to content

Commit

Permalink
fix(forms): support rebinding nested controls (angular#11210)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored and mprobst committed Sep 2, 2016
1 parent d309f77 commit 8c09933
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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) &&
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
Expand All @@ -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();
}
}

Expand All @@ -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 <FormControl>this.form.get(dir.path); }
getControl(dir: FormControlName): FormControl { return <FormControl>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);
Expand All @@ -167,7 +166,7 @@ export class FormGroupDirective extends ControlContainer implements Form,

getFormArray(dir: FormArrayName): FormArray { return <FormArray>this.form.get(dir.path); }

updateModel(dir: NgControl, value: any): void {
updateModel(dir: FormControlName, value: any): void {
var ctrl  = <FormControl>this.form.get(dir.path);
ctrl.setValue(value);
}
Expand All @@ -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();
Expand Down
65 changes: 57 additions & 8 deletions modules/@angular/forms/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ function coerceToAsyncValidator(asyncValidator: AsyncValidatorFn | AsyncValidato
export abstract class AbstractControl {
/** @internal */
_value: any;
/** @internal */
_onCollectionChange = () => {};

private _valueChanges: EventEmitter<any>;
private _statusChanges: EventEmitter<any>;
Expand Down Expand Up @@ -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; }
}

/**
Expand Down Expand Up @@ -530,6 +535,7 @@ export class FormControl extends AbstractControl {
_clearChangeFns(): void {
this._onChange = [];
this._onDisabledChange = null;
this._onCollectionChange = () => {};
}

/**
Expand Down Expand Up @@ -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});
}

Expand All @@ -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;
}

Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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});
}

Expand All @@ -764,25 +788,45 @@ export class FormArray extends AbstractControl {
*/
push(control: AbstractControl): void {
this.controls.push(control);
control.setParent(this);
this._registerControl(control);
this.updateValueAndValidity();
this._onCollectionChange();
}

/**
* Insert a new {@link AbstractControl} at the given `index` in the array.
*/
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();
}

/**
Expand Down Expand Up @@ -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 */
Expand All @@ -869,4 +913,9 @@ export class FormArray extends AbstractControl {
}
return !!this.controls.length;
}

private _registerControl(control: AbstractControl) {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
}
}
1 change: 1 addition & 0 deletions modules/@angular/forms/test/directives_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
43 changes: 43 additions & 0 deletions modules/@angular/forms/test/form_array_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!']);
});

});

});
});
}
Loading

0 comments on commit 8c09933

Please sign in to comment.