Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 60ac964

Browse files
AndrewKushnirthePunderWoman
authored andcommitted
fix(forms): properly handle the change to the FormGroup shape (#40829)
Currently the code in the `FormGroupDirective` assumes that the shape of the underlying `FormGroup` never changes and `FormControl`s are not replaced with other types. In practice this is possible and Forms code should be able to process such changes in FormGroup shape. This commit adds extra check to the `FormGroupDirective` class to avoid applying FormControl-specific to other types. Fixes #13788. PR Close #40829
1 parent b37296a commit 60ac964

File tree

2 files changed

+197
-5
lines changed

2 files changed

+197
-5
lines changed

‎packages/forms/src/directives/reactive_directives/form_group_directive.ts‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,22 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
295295
/** @internal */
296296
_updateDomValue() {
297297
this.directives.forEach(dir => {
298-
const newCtrl: any = this.form.get(dir.path);
299-
if (dir.control !== newCtrl) {
298+
const oldCtrl = dir.control;
299+
const newCtrl = this.form.get(dir.path);
300+
if (oldCtrl !== newCtrl) {
300301
// Note: the value of the `dir.control` may not be defined, for example when it's a first
301302
// `FormControl` that is added to a `FormGroup` instance (via `addControl` call).
302-
cleanUpControl(dir.control || null, dir);
303-
if (newCtrl) setUpControl(newCtrl, dir);
304-
(dir as {control: FormControl}).control = newCtrl;
303+
cleanUpControl(oldCtrl || null, dir);
304+
305+
// Check whether new control at the same location inside the corresponding `FormGroup` is an
306+
// instance of `FormControl` and perform control setup only if that's the case.
307+
// Note: we don't need to clear the list of directives (`this.directives`) here, it would be
308+
// taken care of in the `removeControl` method invoked when corresponding `formControlName`
309+
// directive instance is being removed (invoked from `FormControlName.ngOnDestroy`).
310+
if (newCtrl instanceof FormControl) {
311+
setUpControl(newCtrl, dir);
312+
(dir as {control: FormControl}).control = newCtrl;
313+
}
305314
}
306315
});
307316

‎packages/forms/test/reactive_integration_spec.ts‎

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,189 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
663663
expect(input.nativeElement.getAttribute('disabled')).toBe(null);
664664
});
665665
});
666+
667+
describe('dynamic change of FormGroup and FormArray shapes', () => {
668+
it('should handle FormControl and FormGroup swap', () => {
669+
@Component({
670+
template: `
671+
<form [formGroup]="form">
672+
<input formControlName="name" id="standalone-id" *ngIf="!showAsGroup">
673+
<ng-container formGroupName="name" *ngIf="showAsGroup">
674+
<input formControlName="control" id="inside-group-id">
675+
</ng-container>
676+
</form>
677+
`
678+
})
679+
class App {
680+
showAsGroup = false;
681+
form!: FormGroup;
682+
683+
useStandaloneControl() {
684+
this.showAsGroup = false;
685+
this.form = new FormGroup({
686+
name: new FormControl('standalone'),
687+
});
688+
}
689+
690+
useControlInsideGroup() {
691+
this.showAsGroup = true;
692+
this.form = new FormGroup({
693+
name: new FormGroup({
694+
control: new FormControl('inside-group'),
695+
})
696+
});
697+
}
698+
}
699+
700+
const fixture = initTest(App);
701+
fixture.componentInstance.useStandaloneControl();
702+
fixture.detectChanges();
703+
704+
let input = fixture.nativeElement.querySelector('input');
705+
expect(input.id).toBe('standalone-id');
706+
expect(input.value).toBe('standalone');
707+
708+
// Replace `FormControl` with `FormGroup` at the same location
709+
// in data model and trigger change detection.
710+
fixture.componentInstance.useControlInsideGroup();
711+
fixture.detectChanges();
712+
713+
input = fixture.nativeElement.querySelector('input');
714+
expect(input.id).toBe('inside-group-id');
715+
expect(input.value).toBe('inside-group');
716+
717+
// Swap `FormGroup` with `FormControl` back at the same location
718+
// in data model and trigger change detection.
719+
fixture.componentInstance.useStandaloneControl();
720+
fixture.detectChanges();
721+
722+
input = fixture.nativeElement.querySelector('input');
723+
expect(input.id).toBe('standalone-id');
724+
expect(input.value).toBe('standalone');
725+
});
726+
727+
it('should handle FormControl and FormArray swap', () => {
728+
@Component({
729+
template: `
730+
<form [formGroup]="form">
731+
<input formControlName="name" id="standalone-id" *ngIf="!showAsArray">
732+
<ng-container formArrayName="name" *ngIf="showAsArray">
733+
<input formControlName="0" id="inside-array-id">
734+
</ng-container>
735+
</form>
736+
`
737+
})
738+
class App {
739+
showAsArray = false;
740+
form!: FormGroup;
741+
742+
useStandaloneControl() {
743+
this.showAsArray = false;
744+
this.form = new FormGroup({
745+
name: new FormControl('standalone'),
746+
});
747+
}
748+
749+
useControlInsideArray() {
750+
this.showAsArray = true;
751+
this.form = new FormGroup({
752+
name: new FormArray([
753+
new FormControl('inside-array') //
754+
])
755+
});
756+
}
757+
}
758+
759+
const fixture = initTest(App);
760+
fixture.componentInstance.useStandaloneControl();
761+
fixture.detectChanges();
762+
763+
let input = fixture.nativeElement.querySelector('input');
764+
expect(input.id).toBe('standalone-id');
765+
expect(input.value).toBe('standalone');
766+
767+
// Replace `FormControl` with `FormArray` at the same location
768+
// in data model and trigger change detection.
769+
fixture.componentInstance.useControlInsideArray();
770+
fixture.detectChanges();
771+
772+
input = fixture.nativeElement.querySelector('input');
773+
expect(input.id).toBe('inside-array-id');
774+
expect(input.value).toBe('inside-array');
775+
776+
// Swap `FormArray` with `FormControl` back at the same location
777+
// in data model and trigger change detection.
778+
fixture.componentInstance.useStandaloneControl();
779+
fixture.detectChanges();
780+
781+
input = fixture.nativeElement.querySelector('input');
782+
expect(input.id).toBe('standalone-id');
783+
expect(input.value).toBe('standalone');
784+
});
785+
786+
it('should handle FormGroup and FormArray swap', () => {
787+
@Component({
788+
template: `
789+
<form [formGroup]="form">
790+
<ng-container formGroupName="name" *ngIf="!showAsArray">
791+
<input formControlName="control" id="inside-group-id">
792+
</ng-container>
793+
<ng-container formArrayName="name" *ngIf="showAsArray">
794+
<input formControlName="0" id="inside-array-id">
795+
</ng-container>
796+
</form>
797+
`
798+
})
799+
class App {
800+
showAsArray = false;
801+
form!: FormGroup;
802+
803+
useControlInsideGroup() {
804+
this.showAsArray = false;
805+
this.form = new FormGroup({
806+
name: new FormGroup({
807+
control: new FormControl('inside-group'),
808+
})
809+
});
810+
}
811+
812+
useControlInsideArray() {
813+
this.showAsArray = true;
814+
this.form = new FormGroup({
815+
name: new FormArray([
816+
new FormControl('inside-array') //
817+
])
818+
});
819+
}
820+
}
821+
822+
const fixture = initTest(App);
823+
fixture.componentInstance.useControlInsideGroup();
824+
fixture.detectChanges();
825+
826+
let input = fixture.nativeElement.querySelector('input');
827+
expect(input.id).toBe('inside-group-id');
828+
expect(input.value).toBe('inside-group');
829+
830+
// Replace `FormGroup` with `FormArray` at the same location
831+
// in data model and trigger change detection.
832+
fixture.componentInstance.useControlInsideArray();
833+
fixture.detectChanges();
834+
835+
input = fixture.nativeElement.querySelector('input');
836+
expect(input.id).toBe('inside-array-id');
837+
expect(input.value).toBe('inside-array');
838+
839+
// Swap `FormArray` with `FormGroup` back at the same location
840+
// in data model and trigger change detection.
841+
fixture.componentInstance.useControlInsideGroup();
842+
fixture.detectChanges();
843+
844+
input = fixture.nativeElement.querySelector('input');
845+
expect(input.id).toBe('inside-group-id');
846+
expect(input.value).toBe('inside-group');
847+
});
848+
});
666849
});
667850

668851
describe('user input', () => {

0 commit comments

Comments
 (0)