2

I am building a nested, dynamic Form where the User has a group, and then can nest conditions within that group, or new additional group objects within a group FormArray. Here is what the basic UI looks like. Note, not all the pieces are working, but for now I am trying to add a nested group. A nested group will work for the FormBuilder, but it gives an error and does not show correctly on the UI. The error is: ERROR Error: Cannot find control with path: 'statement -> groups -> 0 -> groups -> conditions' . Before going further, the StackBlitz can be found HERE

enter image description here

The form object looks like this:

{
  "statement": {
    "groups": [
      {
        "conjunctor": null,
        "conditions": [
          {
            "variable": ""
          }
        ],
        "groups": []
      }
    ]
  }
}

Within the statement → groups → groups the user is able to push an additional FormGroup that will contain a "groups" object:

 {
 "conjunctor": null,
   "conditions": [
     {
       "variable": ""
     }
    ],
   "groups": []
 }

Long term, I expect to be able to push additional groups and further nest this Form, but for now, I am trying to get it to work on the UI. The HTML is as shown below and in this StackBlitz. I continue to get the error: ERROR Error: Cannot find control with path: 'statement -> groups -> 0 -> groups -> conditions' , and based off several S.O. examples, I recognize that this error is due to the way my HTML is nested and the FormGroups and FormArrays, there must be an issue within it. However, I cannot seem to get it to work in order to nest and display a nested group. Here are some approaches I have tried:

Angular FormArray: Cannot find control with path

Angular: Cannot find control with path: 'variable-> 0 -> id'

Angular 7 and form arrays error of cannot find a control with path

ERROR Error: Cannot find control with path

As a side-note, I'm not sure if this is even the best approach to implementing a nested reusable component, but I expect to research this further once I stop getting errors.

<form [formGroup]="form">
  <div formArrayName="statement">
    <div formArrayName="groups">
      <div *ngFor="let group of form.get('statement.groups')['controls']; let i = index">
        <fieldset>
          <legend>Group {{ i + 1 }}:</legend>
          <div [formGroupName]="i">
            <span style="float: right;">
              <button type="button" style="float: right; cursor: pointer; margin-left: 5px;" (click)="deleteGroup(i)">
                delete group
              </button>
              <button type="button" style="cursor: pointer; margin-left: 5px;" (click)="addNestedGroup(i)">
                add nested group
              </button>
              <button
                type="button"
                style="cursor: pointer; margin-left: 5px;"
                (click)="addNewCondition(group.controls.conditions)"
              >
                add condition
              </button>
            </span>
            <div formArrayName="conditions">
              <div *ngFor="let condition of group.get('conditions')['controls']; let j = index">
                <fieldset>
                  <legend>Condition {{ j + 1 }}</legend>
                  <div [formGroupName]="j">
                    <input style="vertical-align: middle;" type="text" formControlName="variable" />
                    <button
                      style="float: right; margin-bottom: 5px;"
                      (click)="deleteCondition(group.controls.conditions, j)"
                    >
                      delete condition
                    </button>
                  </div>
                </fieldset>
              </div>
            </div>
            <ng-container>
              <div formArrayName="groups">
                <div *ngFor="let num of group.get('groups').value; let idx = index">
                  <fieldset>
                    <legend>Group {{ 2 }}:</legend>
                    <span style="float: right;">
                      <button
                        type="button"
                        style="float: right; cursor: pointer; margin-left: 5px;"
                        (click)="deleteGroup(0)"
                      >
                        delete group
                      </button>
                      <button type="button" style="cursor: pointer; margin-left: 5px;" (click)="addNestedGroup(0)">
                        add nested group
                      </button>
                      <button
                        type="button"
                        style="cursor: pointer; margin-left: 5px;"
                        (click)="addNewCondition(num.conditions)"
                      >
                        add condition
                      </button>
                    </span>
                    <div formArrayName="conditions">
                      <div *ngFor="cond; of: group.controls; let k = index">
                        <fieldset>
                          <legend>Condition {{ k + 1 }}</legend>
                          <div [formGroupName]="k">
                            <input style="vertical-align: middle;" type="text" formControlName="variable" />
                            <button
                              style="float: right; margin-bottom: 5px;"
                              (click)="deleteCondition(group.controls.conditions, k)"
                            >
                              delete condition
                            </button>
                          </div>
                        </fieldset>
                      </div>
                    </div>
                  </fieldset>
                </div>
              </div>
            </ng-container>
          </div>
        </fieldset>
      </div>
    </div>
  </div>
</form>
Jeremy
  • 1,038
  • 11
  • 34
  • How many nested levels do you want to have? I can fix your current issue but I think you will ask for more complex example – yurzui Jun 12 '20 at 21:37
  • Ultimately I'd like to have 3-4 levels deep (compared to 2 as shown right now): groups → groups. Long term I imagine having groups→groups→groups→groups. As far deeply nested as would ever need to be – Jeremy Jun 12 '20 at 22:21

1 Answers1

7

I've written a post at dev.to after writing this answer. Take a look.


You're dealing with a complex form. It's nested and it's recursive (as you have groups in groups). I suggest you split it into more components. By doing that, you'll make it easier to have a big picture of the whole form whenever you revisit it for any reason. And, as a very welcome cherry-on-the-cake, you will avoid the deeply nested object paths you're using to access your controls (this can be overwhelming as you keep nesting dynamic forms the way you're doing).

What I'm trying to say is that the error is likely caused by some silly mistake in the deep object paths you use to get access to the form parts. When dealing with this kind of complex nested form, it usually isn't worth the effort to fix eventual issues related to wrong object paths: refactor it to get a more clean-structured component.

I strongly suggest you do the two things I'll describe below (also, take a look at this Stackblitz demo). What I did in that demo is a complete refactor of your form and I decided not to paste all the code here because it would be excessively long, hard to read, and you wouldn't be able to execute it anyway. So it would be pointless. Just go to the Stackblitz demo and try it there.

Your form has a very peculiar aspect: it's recursive. So everything is gonna be easier if we don't try to row the boat against its recursive nature stream.

I assure you I did nothing special in that code but just these two steps:

1 - Create a wrapper recursive form component:

Let's call it GroupFormComponent. The one thing that's not so common here is that, in the template of this component, you're gonna have... another GroupFormComponent. Yes, you can bury an angular component inside itself recursively.

@Component({
  selector: 'group-form',
  template: `
    ...
    <!-- here you nest another instance of this component -->
    <group-form *ngIf="nestCondition"></group-form>
    ...
  `,
})
export class GroupFormComponent {...}

The above snippet helps to illustrate what I'm suggesting you do (this kind of structure shows how far you can go with angular component-based nature => it's powerful, isn't it?).

2 - Split your form into more controls

You can (and should) group some parts of your form into other components, to make it easier to be understood as a whole. Without any great mental effort, we can identify three components:

  • The main form, containing the overall form

  • A bar of action buttons

  • A condition component

When you assemble all these parts together, you'll get:

<main-form>
  <action-buttons></action-buttons>
  <condition></condition>
  <condition></condition>
  ...
  <condition></condition>

  <!-- The recursive part -->
  <main-form></main-form>
  <main-form></main-form>
  ...
  <main-form></main-form>
</main-form>

To make it even more simple, <condition> and <main-form> components must implement the ControlValueAccessor interface in order to allow them to be used as FormControl's in other forms.

With all these this in place, you'll have a robust, maintainable, and flexible form.

The animated gif below shows it working.

enter image description here

julianobrasil
  • 8,954
  • 2
  • 33
  • 55
  • This is incredible. What a fantastic approach. Thank you very much for the StackBlitz, as well as the explanation of it. Everything is working great. – Jeremy Jun 13 '20 at 12:31
  • My pleasure. I enjoyed doing it. In fact I think I'm gonna write a blog post about it. – julianobrasil Jun 13 '20 at 18:23
  • I'd love to read and share it when and if you write one. Feel free to share in these comments if you happen to remember. Thanks again – Jeremy Jun 15 '20 at 17:52
  • 1
    Already published: https://dev.to/julianobrasil/writing-nested-recursive-angular-reactive-forms-a-clean-approach-57fk – julianobrasil Jun 15 '20 at 18:12
  • You're quick! Thanks. I posted a followup question regarding patch values if you happen to know the answer: https://stackoverflow.com/questions/62397992/how-to-patch-form-value-to-a-recursive-nested-angular-form – Jeremy Jun 15 '20 at 22:41
  • Hi Juliano, I created a new stack overflow issue regarding using the conjunctor on every 2nd instance here: https://stackoverflow.com/questions/62585491/cannot-find-control-with-path-using-ngif-on-recursive-angular-form , would be awesome if you had a chance to check it out. If not, it's no problem. You've been a huge help already – Jeremy Jun 25 '20 at 22:55
  • @julianobrasil, about the second aproach,really you **needn't** components must implement the ControlValueAccessor. Futhermore, create a custom form control to do it is using a sledgehammer to crack a nut. Yes, divide the component in severals components, but pass as `@Input` the formControl or the formArray – Eliseo Sep 24 '20 at 06:49
  • Personally I don't like passing controls around through `@Input()`. I think the control value accessor interface gives us a much more clean/flexible/scalable solution. But it's just an opinion. – julianobrasil Sep 24 '20 at 20:22
  • Great example! I have adapted it to my code and works well. The only issue I am having is i am using complex objects holding arrays of complex objects and although i can see the form value updated correctly i can't seem to be able to configure the validation. So if I pass an object to the formbuilder.control and then inside the child component i set up the formgroup with the validations (e.g name: ite,.name, [val.required]) the validation works on the component but not on the main form. Any idea why or how to fix it by any chance? – Alberto L. Bonfiglio Nov 11 '20 at 21:16
  • 1
    Try to implement the Validator interface in your form component. I've forked that stackblitz in my answer and put together a [simple validator example](https://stackblitz.com/edit/httpsstackoverflowcoma623555906433166-yyiu3t?file=src%2Fapp%2Fgroup-control%2Fgroup-control.component.ts) in one of the components. – julianobrasil Nov 12 '20 at 03:13
  • Yup that was it. I ended up implementing validator and in the validate method i check the validity of the formgroup and return that. Now works like a charm. Thanks! – Alberto L. Bonfiglio Nov 13 '20 at 16:43