Search code examples
angularangular2-formsangular-forms

angular 4 passing an object containing other objects into FormBuilder.group function leads to a strange form behavior


Creating a nested form made up of the following "nested object" (Building):

export class Building{
  id: number = 0;
  doorsCount: number = 0;
  description: string = '';
  address: Address = new Address();
  buildingType: BuildingType = new BuildingType();
}

export class Address{
  id: number = 0;
  description: string = '';
}

export class BuildingType{
  id: number = 0;
  description: string = '';
}

As you can see Building class contains other classes like Address and BuildingType which also have other properties such as id and description.

When creating the form I used the following code at the component ts file:

buildingForm: FormGroup;

construct(private fb: FormBuilder){
 this.buildingForm = this.createBuildingFG(new Building);
}

createBuildingFG(building: Building){
  let formGroup : FormGroup;
  formGroup = this.fb.group(building);

  // Because object of type "building" contain properties of non-primitive 
  // types such as object Address and BuildingType I think the following 
  // additional lines are required. 

  formGroup.controls.address = this.fb.group(building.address);
  formGroup.controls.buildingType = this.fb.group(building.buildingType);

  return formGroup;
}

And this is how the form is bound to the HTML template:

<form [formGroup]="buildingForm">
    <label>
        Door count: 
        <input formControlName="doorsCount" >
    </label>
    <label>
        Building description: 
        <input formControlName="description" >
    </label>
    <label formGroupName="address">
        Address: 
        <input formControlName="description"  >
    </label>
    <label formGroupName="buildingType">
         Building type: 
        <input formControlName="description" >
    </label>    
</form>

Now the problem arises when I output the value of the whole form which do not really update according to what is being typed in the nested field controls that are inside a formGroup such as address or buildingType. Otherwise it updates normally.

This is how the value is output

<div>
   {{buildingForm.value | json}}
</div>

However, if createBuildingFG function was done differently and every formControl was created explicitly with out just passing an entire object, the form acts normally. Example:

createBuildingFG(building: Building){
  let formGroup : FormGroup;
  formGroup = this.fb.group({
    doorsCount: '',
    description: '',
    address: this.fb.group({ description: ''}),
    buildingType: this.fb.group({ description: ''})
  });

  return formGroup;
}

Anyone can explain what is going on? Obviously to avoid doing this tedious task of explicitly defining each element of the fromGroup, one would want to just pass an entire object.


Solution

  • As @jonrsharpe mentioned in comments

    because the initial construction does things you're not subsequently overriding.

    So what are the things?

    When angular create new instance of FormGroup it calls _setUpControls method

    export class FormGroup extends AbstractControl {
      constructor(
          public controls: {[key: string]: AbstractControl},
          validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
          asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null) {
        super(
            coerceToValidator(validatorOrOpts),
            coerceToAsyncValidator(asyncValidator, validatorOrOpts));
        this._initObservables();
        this._setUpdateStrategy(validatorOrOpts);
        this._setUpControls();  <--------------
    

    and now let's look at the method:

    /** @internal */
    _setUpControls(): void {
      this._forEachChild((control: AbstractControl) => {
         control.setParent(this);
         control._registerOnCollectionChange(this._onCollectionChange);
      });
    }
    

    As we can see each item of controls sets parent and registers some event but you don't do that.

    The following code should work:

    formGroup = this.fb.group(building);
    formGroup.controls.address = formGroup.controls.buildingType = null;
    formGroup.registerControl('address', this.fb.group(building.address));
    formGroup.registerControl('buildingType', this.fb.group(building.buildingType));
    

    or you can use recursion to do it working:

    constructor(private fb: FormBuilder){
      this.buildingForm = this.createFormGroup(new Building);
    }
    
    createFormGroup(obj: any) {
      let formGroup: { [id: string]: AbstractControl; } = {};
    
      Object.keys(obj).forEach(key => {
        formGroup[key] = obj[key] instanceof Object ? this.createFormGroup(obj[key]) : new FormControl(obj[key]);
      });
    
      return this.fb.group(formGroup);
    }