angularangular-reactive-formsangular10angular-template

Angular - Template(input filed) does not reflect value in FormGroup Model


I have a dynamically generated FormGroup based on a JSON Shema:

private parse(shema: object, required: boolean = true) {

    if (shema["type"] === "string") {
     return new FormControl(
      shema["value"] || "", 
      (required)? Validators.required : null)
    }

    if (shema["type"] === "boolean") {
      return new FormControl(
       shema["value"] || false, 
       (required)? Validators.required : null)
    }

    if (shema["type"].length && shema["type"][0] === "limit") {
      return new FormControl(
        shema["value"] || "", 
        (required)? Validators.required : null)
    }

    if (
      shema["type"] === "object" &&
      shema["properties"]
    ) {

      let currentLevelControls = new FormGroup({})
      for (let propName in shema["properties"]) {
        const requiredField = (shema["required"][propName])? true : false

        const parsedResult = this.parse(
          shema["properties"][propName], 
          requiredField,
        )

        currentLevelControls.controls[propName] = parsedResult
      }

      return currentLevelControls
    }
  }

It is run on ngOnInit as the component gets the JSON via Input():

  public configurationForm: FormGroup

  ...

  ngOnInit() {
    this.configurationForm = this.parse(this.jsonShema) as FormGroup
    
    this.configurationForm.valueChanges.subscribe((event) => {
      console.log(event)
    })
  }

On the template side, I also dynamically create elements(for simplicity it goes only one level deep and is not recursive like model counterpart):

<div>
  <div class="modal-header">
    <tc-button
      model="action"
      icon="close"
      class="close"
      (click)="modalClose()"></tc-button>
    <h4 class="modal-title"> JSON Editor</h4>
  </div>
  <form [formGroup]="configurationForm" (change)="onCreateConfiguration($event)">
    <div class="modal-body">
        <div *ngFor="let item1 of jsonShema['properties'] | keyvalue">
          <ng-container *ngIf="item1.value.type === 'object'" [formGroupName]="item1.key">
            <h5>{{item1.key}}</h5>
              <div *ngFor="let item2 of item1.value.properties | keyvalue">

                  <ng-container *ngIf="item2.value.type === 'boolean'">
                      <div class="checkbox">
                        <mat-checkbox [formControlName]="item2['key']">{{item2.key}}</mat-checkbox>
                      </div>
                  </ng-container>

                  <ng-container *ngIf="item2.value.type === 'string'">
                    <mat-form-field >
                      <input
                        type="text"
                        [formControlName]="item2['key']"
                        matInput
                        [placeholder]="item2.key"
                        required>
                    </mat-form-field>
                  </ng-container>

                  <ng-container *ngIf="item2.value.type.length && item2.value.type[0] === 'limit'">
                    <mat-form-field>
                      <input
                        type="text"
                        [formControlName]="item2['key']"
                        matInput
                        [placeholder]="item2.key"
                        required>
                    </mat-form-field>
                </ng-container>
              </div>
          </ng-container>
        </div>
    </div>
    <div class="modal-footer">
      <tc-button
        type="button"
        color="neutral">Cancel</tc-button>
      <tc-button
        type="submit" 
        (click)="!configurationForm?.invalid && onCreateConfiguration()">Create</tc-button>
    </div>
  </form>
</div>

The Form renders correctly and any manual adding of new fields would result in form-control issues. So I am guessing the template [formGroup] and the actual FormGroup object are correctly connected.

My issue here is that the above subscribe on valueChange does not trigger. Any input to the form does not reflect the form.value, and all the flags (touched, pristine) are unchanged.

How is It possible that any change to the form(template) does not trigger or reflect its value to the model?

Edit 1: I have noticed that formcontrolname="value" attribute is not rendered in the HTML form after inspecting. If I change my code to e.g.

formControlName="item2['key']"

I will treat it as a value and not a variable, which will throw an error. So, the generated FormGroup is connected correctly to the template.

Edit 2: I have done a sanity check with the FormGroup Parser. Instead of recursion, I have created a 2-level deep parser which adds Form control using {FormGroup}.addControl() functionality.

The issue still persists. I suspect the template has the issue.


Solution

  • I have managed to find the issue. FormGroup object structure was invalid, that's why the template rendered but its values never mirrored to the Reactive Form itself.

    On the above recursive parser, I added the formBuilder.group() on the controls object. This fixed my issue.

        if (
          shema["type"] === "object" &&
          shema["properties"]
        ) {
    
          let currentLevelControls = new FormGroup({})
          for (let propName in shema["properties"]) {
            const requiredField = (shema["required"][propName])? true : false
    
            const parsedResult = this.parseRec(
              shema["properties"][propName], 
              requiredField,
            )
    
    
            currentLevelControls.controls[propName] = parsedResult
    
          }
    
          return this.formBuilder.group(currentLevelControls.controls)
        }
    

    Hope this helps someone if they ever get in the same sittuation.