Home > Mobile >  How to create a recursive form with Angular 8?
How to create a recursive form with Angular 8?

Time:11-01

I need to create a dynamic form with multiple nested items. I've found this example

but i'm not sure it's doing deep recursive since once i've tried to add more levels of nested items - the ui brakes down.

Here is the default json structure with my attempts :

 {
    key: "common",
    title: "main fields",
    group: [
      {
        key: "createdAt",
        title: "Create Date",
        type: "date"
      },
             //   group:[{
      //     key: "foo",
      //     title: "Foo",
      //     type: "select",
      //   },
      //   {
      //     key: "goo",
      //     title: "Goo",
      //     type: "input",
      //   },
     
      // ]
    ]
  },

So as you can see under "common" - i've added 2 more levels of groups - the first group works fine - but the nested group with key "foo" and "goo" it's working.

I'm pretty sure the problem is in the template / markup

<form [formGroup]="filterForm" >
  <ng-template #recursiveList let-filterFields let-fromGroup="fromGroup">
    <ng-container *ngFor="let item of filterFields">
      <ng-container *ngIf="item.group; else default;">
          // in this area i'm not sure it's iterate over deeper nesting...
          <p>{{item.key}} </p>
          
          <div  [formGroupName]="item.key">
              <ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: 
          item.group, fromGroup: {name: item.key}, isChild:true }"></ng-container>
          </div>
      </ng-container>
      <ng-template #default>       
        
              <div  [formGroupName]="fromGroup.name">
                  <input [type]="item.type" [formControlName]="item.key" 
               [placeholder]="item.title" [name]="item.key" />
              </div>
         
      </ng-template>
  </ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: filterFields 
 }">. 

CodePudding user response:

From my understanding, there are two issues in the example you provided:

  • The data structure.
  • The template.

Data Structure

These are the interfaces I understand from your example:

interface Base {
  key: string;
  title: string;
}

interface Field extends Base {
  type: 'select' | 'input' | 'date' | ...
}

interface Group extends Base {
  group: Array<Field | Group>
}

So the JSON example you provided should look something like this:

{
  "key": "common",
  "title": "main fields",
  "group": [
    {
      "key": "createdAt",
      "title": "Create Date",
      "type": "date"
    },
    {
      "key": "test",
      "title": "Test"
      "group": [
        {
          "key": "foo",
          "title": "Foo",
          "type": "select"
        },
        {
          "key": "goo",
          "title": "Goo",
          "type": "input"
        }
      ]
    }
  ]
}

Template

Let's look at a very simplified version of the form:

<form [formGroup]="filterForm">
  
  <ng-container formGroupName="common">
    <ng-container *ngTemplateOutlet="control; 
      context:{ controlName: 'foo', group: 'test' }">
    </ng-container>
  </ng-container>

  <ng-template #control let-group="group" let-controlName="controlName">
    <div class="col-md-3">
      <div class="form-group" [formGroupName]="group">
        <input type="input" [formControlName]="controlName" />
      </div>
    </div>
  </ng-template>

</form>

The code won't work, why? Think about the ng-template as a function. If you want it to know about the formGroupName="common" it needs to be declared within that scope. What matters is the declaration context and not the invocation context, just like regular functions.

This is the working version of the above example:

<form [formGroup]="filterForm">

  <ng-container formGroupName="common">
    <ng-container *ngTemplateOutlet="control; 
      context:{ controlName: 'foo', group: 'test' }">
    </ng-container>

    <ng-template #control let-group="group" let-controlName="controlName">
      <div class="col-md-3">
        <div class="form-group" [formGroupName]="group">
          <input type="input" [formControlName]="controlName" />
        </div>
      </div>
    </ng-template>
    
  </ng-container>

</form>

Things get trickier when you have nested and you need to use recursion.
That's why I think that the approach of using the formGroupName and formControlName directives in this scenario makes things more complicated than they are.

I suggest passing the form control directly into the input by providing the right path to it.

Here is a working example of the idea based on your original example.

  • Related