fix(forms): support rebinding nested controls (#11210)

This commit is contained in:
Kara 2016-09-02 15:57:35 -07:00 committed by Martin Probst
parent d309f7799c
commit 8c09933803
8 changed files with 277 additions and 39 deletions

View File

@ -96,9 +96,11 @@ export const controlNameBinding: any = {
*/ */
@Directive({selector: '[formControlName]', providers: [controlNameBinding]}) @Directive({selector: '[formControlName]', providers: [controlNameBinding]})
export class FormControlName extends NgControl implements OnChanges, OnDestroy { export class FormControlName extends NgControl implements OnChanges, OnDestroy {
private _added = false;
/** @internal */ /** @internal */
viewModel: any; viewModel: any;
private _added = false; /** @internal */
_control: FormControl;
@Input('formControlName') name: string; @Input('formControlName') name: string;
@ -122,12 +124,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (!this._added) { if (!this._added) this._setUpControl();
this._checkParentType();
this.formDirective.addControl(this);
if (this.control.disabled) this.valueAccessor.setDisabledState(true);
this._added = true;
}
if (isPropertyUpdated(changes, this.viewModel)) { if (isPropertyUpdated(changes, this.viewModel)) {
this.viewModel = this.model; this.viewModel = this.model;
this.formDirective.updateModel(this, this.model); this.formDirective.updateModel(this, this.model);
@ -155,7 +152,7 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
return composeAsyncValidators(this._rawAsyncValidators); return composeAsyncValidators(this._rawAsyncValidators);
} }
get control(): FormControl { return this.formDirective.getControl(this); } get control(): FormControl { return this._control; }
private _checkParentType(): void { private _checkParentType(): void {
if (!(this._parent instanceof FormGroupName) && if (!(this._parent instanceof FormGroupName) &&
@ -167,4 +164,11 @@ export class FormControlName extends NgControl implements OnChanges, OnDestroy {
ReactiveErrors.controlParentException(); ReactiveErrors.controlParentException();
} }
} }
private _setUpControl() {
this._checkParentType();
this._control = this.formDirective.addControl(this);
if (this.control.disabled) this.valueAccessor.setDisabledState(true);
this._added = true;
}
} }

View File

@ -15,10 +15,10 @@ import {FormArray, FormControl, FormGroup} from '../../model';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators';
import {ControlContainer} from '../control_container'; import {ControlContainer} from '../control_container';
import {Form} from '../form_interface'; import {Form} from '../form_interface';
import {NgControl} from '../ng_control';
import {ReactiveErrors} from '../reactive_errors'; import {ReactiveErrors} from '../reactive_errors';
import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared'; import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
import {FormControlName} from './form_control_name';
import {FormArrayName, FormGroupName} from './form_group_name'; import {FormArrayName, FormGroupName} from './form_group_name';
export const formDirectiveProvider: any = { export const formDirectiveProvider: any = {
@ -105,7 +105,8 @@ export const formDirectiveProvider: any = {
export class FormGroupDirective extends ControlContainer implements Form, export class FormGroupDirective extends ControlContainer implements Form,
OnChanges { OnChanges {
private _submitted: boolean = false; private _submitted: boolean = false;
directives: NgControl[] = []; private _oldForm: FormGroup;
directives: FormControlName[] = [];
@Input('formGroup') form: FormGroup = null; @Input('formGroup') form: FormGroup = null;
@Output() ngSubmit = new EventEmitter(); @Output() ngSubmit = new EventEmitter();
@ -119,12 +120,9 @@ export class FormGroupDirective extends ControlContainer implements Form,
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this._checkFormPresent(); this._checkFormPresent();
if (StringMapWrapper.contains(changes, 'form')) { if (StringMapWrapper.contains(changes, 'form')) {
var sync = composeValidators(this._validators); this._updateValidators();
this.form.validator = Validators.compose([this.form.validator, sync]); this._updateDomValue();
this._updateRegistrations();
var async = composeAsyncValidators(this._asyncValidators);
this.form.asyncValidator = Validators.composeAsync([this.form.asyncValidator, async]);
this._updateDomValue(changes);
} }
} }
@ -136,16 +134,17 @@ export class FormGroupDirective extends ControlContainer implements Form,
get path(): string[] { return []; } get path(): string[] { return []; }
addControl(dir: NgControl): void { addControl(dir: FormControlName): FormControl {
const ctrl: any = this.form.get(dir.path); const ctrl: any = this.form.get(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.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 { addFormGroup(dir: FormGroupName): void {
var ctrl: any = this.form.get(dir.path); var ctrl: any = this.form.get(dir.path);
@ -167,7 +166,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
getFormArray(dir: FormArrayName): FormArray { return <FormArray>this.form.get(dir.path); } 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); var ctrl  = <FormControl>this.form.get(dir.path);
ctrl.setValue(value); ctrl.setValue(value);
} }
@ -186,21 +185,33 @@ export class FormGroupDirective extends ControlContainer implements Form,
} }
/** @internal */ /** @internal */
_updateDomValue(changes: SimpleChanges) { _updateDomValue() {
const oldForm = changes['form'].previousValue;
this.directives.forEach(dir => { this.directives.forEach(dir => {
const newCtrl: any = this.form.get(dir.path); const newCtrl: any = this.form.get(dir.path);
const oldCtrl = oldForm.get(dir.path); if (dir._control !== newCtrl) {
if (oldCtrl !== newCtrl) { cleanUpControl(dir._control, dir);
cleanUpControl(oldCtrl, dir);
if (newCtrl) setUpControl(newCtrl, dir); if (newCtrl) setUpControl(newCtrl, dir);
dir._control = newCtrl;
} }
}); });
this.form._updateTreeValidity({emitEvent: false}); 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() { private _checkFormPresent() {
if (isBlank(this.form)) { if (isBlank(this.form)) {
ReactiveErrors.missingFormException(); ReactiveErrors.missingFormException();

View File

@ -81,6 +81,8 @@ function coerceToAsyncValidator(asyncValidator: AsyncValidatorFn | AsyncValidato
export abstract class AbstractControl { export abstract class AbstractControl {
/** @internal */ /** @internal */
_value: any; _value: any;
/** @internal */
_onCollectionChange = () => {};
private _valueChanges: EventEmitter<any>; private _valueChanges: EventEmitter<any>;
private _statusChanges: EventEmitter<any>; private _statusChanges: EventEmitter<any>;
@ -420,6 +422,9 @@ export abstract class AbstractControl {
return isStringMap(formState) && Object.keys(formState).length === 2 && 'value' in formState && return isStringMap(formState) && Object.keys(formState).length === 2 && 'value' in formState &&
'disabled' in formState; 'disabled' in formState;
} }
/** @internal */
_registerOnCollectionChange(fn: () => void): void { this._onCollectionChange = fn; }
} }
/** /**
@ -530,6 +535,7 @@ export class FormControl extends AbstractControl {
_clearChangeFns(): void { _clearChangeFns(): void {
this._onChange = []; this._onChange = [];
this._onDisabledChange = null; this._onDisabledChange = null;
this._onCollectionChange = () => {};
} }
/** /**
@ -574,7 +580,7 @@ export class FormGroup extends AbstractControl {
asyncValidator: AsyncValidatorFn = null) { asyncValidator: AsyncValidatorFn = null) {
super(validator, asyncValidator); super(validator, asyncValidator);
this._initObservables(); this._initObservables();
this._setParentForControls(); this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false}); this.updateValueAndValidity({onlySelf: true, emitEvent: false});
} }
@ -585,6 +591,7 @@ export class FormGroup extends AbstractControl {
if (this.controls[name]) return this.controls[name]; if (this.controls[name]) return this.controls[name];
this.controls[name] = control; this.controls[name] = control;
control.setParent(this); control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
return control; return control;
} }
@ -594,14 +601,28 @@ export class FormGroup extends AbstractControl {
addControl(name: string, control: AbstractControl): void { addControl(name: string, control: AbstractControl): void {
this.registerControl(name, control); this.registerControl(name, control);
this.updateValueAndValidity(); this.updateValueAndValidity();
this._onCollectionChange();
} }
/** /**
* Remove a control from this group. * Remove a control from this group.
*/ */
removeControl(name: string): void { removeControl(name: string): void {
if (this.controls[name]) this.controls[name]._registerOnCollectionChange(() => {});
StringMapWrapper.delete(this.controls, name); StringMapWrapper.delete(this.controls, name);
this.updateValueAndValidity(); 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 */ /** @internal */
_setParentForControls() { _setUpControls() {
this._forEachChild((control: AbstractControl, name: string) => { control.setParent(this); }); this._forEachChild((control: AbstractControl) => {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
});
} }
/** @internal */ /** @internal */
@ -750,7 +774,7 @@ export class FormArray extends AbstractControl {
asyncValidator: AsyncValidatorFn = null) { asyncValidator: AsyncValidatorFn = null) {
super(validator, asyncValidator); super(validator, asyncValidator);
this._initObservables(); this._initObservables();
this._setParentForControls(); this._setUpControls();
this.updateValueAndValidity({onlySelf: true, emitEvent: false}); this.updateValueAndValidity({onlySelf: true, emitEvent: false});
} }
@ -764,8 +788,9 @@ export class FormArray extends AbstractControl {
*/ */
push(control: AbstractControl): void { push(control: AbstractControl): void {
this.controls.push(control); this.controls.push(control);
control.setParent(this); this._registerControl(control);
this.updateValueAndValidity(); this.updateValueAndValidity();
this._onCollectionChange();
} }
/** /**
@ -773,16 +798,35 @@ export class FormArray extends AbstractControl {
*/ */
insert(index: number, control: AbstractControl): void { insert(index: number, control: AbstractControl): void {
ListWrapper.insert(this.controls, index, control); ListWrapper.insert(this.controls, index, control);
control.setParent(this); this._registerControl(control);
this.updateValueAndValidity(); this.updateValueAndValidity();
this._onCollectionChange();
} }
/** /**
* Remove the control at the given `index` in the array. * Remove the control at the given `index` in the array.
*/ */
removeAt(index: number): void { removeAt(index: number): void {
if (this.controls[index]) this.controls[index]._registerOnCollectionChange(() => {});
ListWrapper.removeAt(this.controls, index); ListWrapper.removeAt(this.controls, index);
this.updateValueAndValidity(); 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 */ /** @internal */
_setParentForControls(): void { _setUpControls(): void {
this._forEachChild((control: AbstractControl) => { control.setParent(this); }); this._forEachChild((control: AbstractControl) => this._registerControl(control));
} }
/** @internal */ /** @internal */
@ -869,4 +913,9 @@ export class FormArray extends AbstractControl {
} }
return !!this.controls.length; return !!this.controls.length;
} }
private _registerControl(control: AbstractControl) {
control.setParent(this);
control._registerOnCollectionChange(this._onCollectionChange);
}
} }

View File

@ -552,6 +552,7 @@ export function main() {
parent.form = new FormGroup({'name': formModel}); parent.form = new FormGroup({'name': formModel});
controlNameDir = new FormControlName(parent, [], [], [defaultAccessor]); controlNameDir = new FormControlName(parent, [], [], [defaultAccessor]);
controlNameDir.name = 'name'; controlNameDir.name = 'name';
controlNameDir._control = formModel;
}); });
it('should reexport control properties', () => { it('should reexport control properties', () => {

View File

@ -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!']);
});
});
}); });
}); });
} }

View File

@ -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!']);
});
});
}); });
} }

View File

@ -256,6 +256,91 @@ export function main() {
expect(inputs[2]).not.toBeDefined(); 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');
});
});
}); });

View File

@ -167,6 +167,7 @@ export declare class FormArray extends AbstractControl {
reset(value?: any, {onlySelf}?: { reset(value?: any, {onlySelf}?: {
onlySelf?: boolean; onlySelf?: boolean;
}): void; }): void;
setControl(index: number, control: AbstractControl): void;
setValue(value: any[], {onlySelf}?: { setValue(value: any[], {onlySelf}?: {
onlySelf?: boolean; onlySelf?: boolean;
}): void; }): void;
@ -272,6 +273,7 @@ export declare class FormGroup extends AbstractControl {
reset(value?: any, {onlySelf}?: { reset(value?: any, {onlySelf}?: {
onlySelf?: boolean; onlySelf?: boolean;
}): void; }): void;
setControl(name: string, control: AbstractControl): void;
setValue(value: { setValue(value: {
[key: string]: any; [key: string]: any;
}, {onlySelf}?: { }, {onlySelf}?: {
@ -282,27 +284,27 @@ export declare class FormGroup extends AbstractControl {
/** @stable */ /** @stable */
export declare class FormGroupDirective extends ControlContainer implements Form, OnChanges { export declare class FormGroupDirective extends ControlContainer implements Form, OnChanges {
control: FormGroup; control: FormGroup;
directives: NgControl[]; directives: FormControlName[];
form: FormGroup; form: FormGroup;
formDirective: Form; formDirective: Form;
ngSubmit: EventEmitter<{}>; ngSubmit: EventEmitter<{}>;
path: string[]; path: string[];
submitted: boolean; submitted: boolean;
constructor(_validators: any[], _asyncValidators: any[]); constructor(_validators: any[], _asyncValidators: any[]);
addControl(dir: NgControl): void; addControl(dir: FormControlName): FormControl;
addFormArray(dir: FormArrayName): void; addFormArray(dir: FormArrayName): void;
addFormGroup(dir: FormGroupName): void; addFormGroup(dir: FormGroupName): void;
getControl(dir: NgControl): FormControl; getControl(dir: FormControlName): FormControl;
getFormArray(dir: FormArrayName): FormArray; getFormArray(dir: FormArrayName): FormArray;
getFormGroup(dir: FormGroupName): FormGroup; getFormGroup(dir: FormGroupName): FormGroup;
ngOnChanges(changes: SimpleChanges): void; ngOnChanges(changes: SimpleChanges): void;
onReset(): void; onReset(): void;
onSubmit(): boolean; onSubmit(): boolean;
removeControl(dir: NgControl): void; removeControl(dir: FormControlName): void;
removeFormArray(dir: FormArrayName): void; removeFormArray(dir: FormArrayName): void;
removeFormGroup(dir: FormGroupName): void; removeFormGroup(dir: FormGroupName): void;
resetForm(value?: any): void; resetForm(value?: any): void;
updateModel(dir: NgControl, value: any): void; updateModel(dir: FormControlName, value: any): void;
} }
/** @stable */ /** @stable */