diff --git a/packages/forms/src/model.ts b/packages/forms/src/model.ts index d6392dcb94..c21fe81f08 100644 --- a/packages/forms/src/model.ts +++ b/packages/forms/src/model.ts @@ -1569,14 +1569,18 @@ export class FormGroup extends AbstractControl { * * `onlySelf`: When true, each change only affects this control and not its parent. Default is * true. * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and - * `valueChanges` - * observables emit events with the latest status and value when the control value is updated. - * When false, no events are emitted. - * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity - * updateValueAndValidity} method. + * `valueChanges` observables emit events with the latest status and value when the control value + * is updated. When false, no events are emitted. The configuration options are passed to + * the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method. */ patchValue(value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + // Even though the `value` argument type doesn't allow `null` and `undefined` values, the + // `patchValue` can be called recursively and inner data structures might have these values, so + // we just ignore such cases when a field containing FormGroup instance receives `null` or + // `undefined` as a value. + if (value == null /* both `null` and `undefined` */) return; + Object.keys(value).forEach(name => { if (this.controls[name]) { this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent: options.emitEvent}); @@ -2002,13 +2006,17 @@ export class FormArray extends AbstractControl { * * `onlySelf`: When true, each change only affects this control, and not its parent. Default * is false. * * `emitEvent`: When true or not supplied (the default), both the `statusChanges` and - * `valueChanges` - * observables emit events with the latest status and value when the control value is updated. - * When false, no events are emitted. - * The configuration options are passed to the {@link AbstractControl#updateValueAndValidity - * updateValueAndValidity} method. + * `valueChanges` observables emit events with the latest status and value when the control value + * is updated. When false, no events are emitted. The configuration options are passed to + * the {@link AbstractControl#updateValueAndValidity updateValueAndValidity} method. */ patchValue(value: any[], options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void { + // Even though the `value` argument type doesn't allow `null` and `undefined` values, the + // `patchValue` can be called recursively and inner data structures might have these values, so + // we just ignore such cases when a field containing FormArray instance receives `null` or + // `undefined` as a value. + if (value == null /* both `null` and `undefined` */) return; + value.forEach((newValue: any, index: number) => { if (this.at(index)) { this.at(index).patchValue(newValue, {onlySelf: true, emitEvent: options.emitEvent}); diff --git a/packages/forms/test/form_array_spec.ts b/packages/forms/test/form_array_spec.ts index 7159500b47..d1f2737b21 100644 --- a/packages/forms/test/form_array_spec.ts +++ b/packages/forms/test/form_array_spec.ts @@ -266,12 +266,13 @@ describe('FormArray', () => { }); describe('patchValue', () => { - let c: FormControl, c2: FormControl, a: FormArray; + let c: FormControl, c2: FormControl, a: FormArray, a2: FormArray; beforeEach(() => { c = new FormControl(''); c2 = new FormControl(''); a = new FormArray([c, c2]); + a2 = new FormArray([a]); }); it('should set its own value', () => { @@ -329,6 +330,16 @@ describe('FormArray', () => { expect(a.value).toEqual(['', '']); }); + it('should ignore a array if `null` or `undefined` are used as values', () => { + const INITIAL_STATE = [['', '']]; + + a2.patchValue([null]); + expect(a2.value).toEqual(INITIAL_STATE); + + a2.patchValue([undefined]); + expect(a2.value).toEqual(INITIAL_STATE); + }); + describe('patchValue() events', () => { let form: FormGroup; let logger: any[]; @@ -358,6 +369,23 @@ describe('FormArray', () => { expect(logger).toEqual(['control1', 'array', 'form']); }); + it('should not emit valueChange events for skipped controls (represented as `null` or `undefined`)', + () => { + const logEvent = () => logger.push('valueChanges event'); + + const [formArrayControl1, formArrayControl2] = (a2.controls as FormArray[])[0].controls; + + formArrayControl1.valueChanges.subscribe(logEvent); + formArrayControl2.valueChanges.subscribe(logEvent); + + a2.patchValue([null]); + a2.patchValue([undefined]); + + // No events are expected in `valueChanges` since + // all controls were skipped in `patchValue`. + expect(logger).toEqual([]); + }); + it('should not fire an event when explicitly specified', fakeAsync(() => { form.valueChanges.subscribe((value) => { throw 'Should not happen'; diff --git a/packages/forms/test/form_group_spec.ts b/packages/forms/test/form_group_spec.ts index 1660611ed1..f65e88aa9f 100644 --- a/packages/forms/test/form_group_spec.ts +++ b/packages/forms/test/form_group_spec.ts @@ -311,12 +311,16 @@ describe('FormGroup', () => { }); describe('patchValue', () => { - let c: FormControl, c2: FormControl, g: FormGroup; + let c: FormControl, c2: FormControl, g: FormGroup, g2: FormGroup; beforeEach(() => { c = new FormControl(''); c2 = new FormControl(''); g = new FormGroup({'one': c, 'two': c2}); + g2 = new FormGroup({ + 'array': new FormArray([new FormControl(1), new FormControl(2)]), + 'group': new FormGroup({'one': new FormControl(3)}), + }); }); it('should set its own value', () => { @@ -374,6 +378,22 @@ describe('FormGroup', () => { expect(g.value).toEqual({'one': '', 'two': ''}); }); + it('should ignore a control if `null` or `undefined` are used as values', () => { + const INITIAL_STATE = {'array': [1, 2], 'group': {'one': 3}}; + + g2.patchValue({'array': null}); + expect(g2.value).toEqual(INITIAL_STATE); + + g2.patchValue({'array': undefined}); + expect(g2.value).toEqual(INITIAL_STATE); + + g2.patchValue({'group': null}); + expect(g2.value).toEqual(INITIAL_STATE); + + g2.patchValue({'group': undefined}); + expect(g2.value).toEqual(INITIAL_STATE); + }); + describe('patchValue() events', () => { let form: FormGroup; let logger: any[]; @@ -403,6 +423,27 @@ describe('FormGroup', () => { expect(logger).toEqual(['control1', 'group', 'form']); }); + it('should not emit valueChange events for skipped controls (represented as `null` or `undefined`)', + () => { + const logEvent = () => logger.push('valueChanges event'); + + const [formArrayControl1, formArrayControl2] = (g2.controls.array as FormArray).controls; + const formGroupControl = (g2.controls.group as FormGroup).controls.one; + + formArrayControl1.valueChanges.subscribe(logEvent); + formArrayControl2.valueChanges.subscribe(logEvent); + formGroupControl.valueChanges.subscribe(logEvent); + + g2.patchValue({'array': null}); + g2.patchValue({'array': undefined}); + g2.patchValue({'group': null}); + g2.patchValue({'group': undefined}); + + // No events are expected in `valueChanges` since + // all controls were skipped in `patchValue`. + expect(logger).toEqual([]); + }); + it('should not fire an event when explicitly specified', fakeAsync(() => { form.valueChanges.subscribe((value) => { throw 'Should not happen';