4

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" class="filter-form">
  <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 class="form-group" [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 
 }">. 
2
  • check out this answer Commented Oct 31, 2021 at 22:22
  • it is quite easy to replace that horrible identicon auto-generated by gravatar for you, by uploading another image through the "edit profile" option. in case you were wondering. Commented Nov 3, 2021 at 5:53

1 Answer 1

4
+200

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.