0

Recently, I asked the following question on StackOverflow and received an amazing response that resulted in building a recursive nested Angular Form: Angular Deeply Nested Reactive Form: Cannot find control with path on nested FormArray . Here is a blog post written by the author about the same question

This recursive nested Angular Form lets me continue to build group FormArrays, all of which have a group object:

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

You can add multiple instances of conditions and further nest the groups as shown in this StackBlitz

Essentially, the Form component has been split up into multiple components: group-control , condition-form, and action-buttons-bar (the buttons to emit actions). This approach does everything I need, however, I am having troubles patching form values via this recursive approach.

So far I have tried sending in a static statement object and patching it both into the entire form itself, as well as specifically targeting the groups object. Here is what this looks like in the

AppComponent:

(I have tried the same and similar approaches within the group-control component as well). Eventually I plan on using data from an API to populate and patch these values, but for now and testing, using this static data

  ngOnInit() {
    const data = {
      statement: {
        groups: [
          {
            conjunctor: "and",
            conditions: [
              {
                variable: "asdf"
              }
            ],
            groups: []
          }
        ]
      }
    };
    this._form.patchValue(data);
    console.log(this._form.value);
  }

Is it possible to patch the form value of the statement FormGroup on a recursive Form?

Community
  • 1
  • 1
Jeremy
  • 1,038
  • 11
  • 34

1 Answers1

3

Stackblitz demo

There's nothing wrong with your data. It's likely a matter of timing. The form must be stable before you can patch any value. As you're doing it in OnInit, I suppose you're building the form in the constructor. If so, the form isn't ready to receive any values yet.

The simplest way to solve that is with an uncomfortable setTimeout. But I suppose that this is just for testing. In real cases, the data will probably get some time to be available anyway.

Your example above will work if you wrap it with the aforementioned setTimeout:

ngOnInit() {
  setTimeout(() => {
    const data = {
      statement: {
        groups: [
          {
            conjunctor: "and",
            conditions: [
              {
                variable: "asdf"
              }
            ],
            groups: []
          }
        ]
      }
    };
    this._form.patchValue(data);
    console.log(this._form.value);
  }, 50);
}

The animated gif below shows a patch in 3 nested groups, the last one being nested on the second one.

enter image description here

Building the form according to given data

One interesting thing you can do is build the form automatically according to the data you have. You can achieve that by analyzing the data that gets into the ControlValueAccessor.writeValue:

if (!value) {
  return;
}
setTimeout(() => {
  if (value && value.conditions && value.conditions.length) {
    this._conditionsFormArray.clear();
    value.conditions.forEach(c => this._addCondition());
  }

  if (value && value.groups && value.groups.length) {
    this._groupsFormArray.clear();
    value.groups.forEach(g => this._addGroup());
  }

  this._form.patchValue(value);
}, 50);

Maybe you've noticed the setTimeout above, it's there because we have another setTimeout in the form creation method (called on OnInit) that add a condition to the form after it's created, and the creation of the condition is, itself, inside a setTimeout. Because of that, we must wait it runs, otherwise this._conditionsFormArray.clear(); wouldn't have any effect => no condition would've been created when it runs.

private _createFormGroup() {
  this._form = this._fb.group({
    conjunctor: null,
    conditions: this._fb.array([]),
    groups: this._fb.array([])
  });

  // add one condition on the next tick, after the form creation
  setTimeout(() => this._addCondition());
}

Then with some adjustments, we can get to the following result (the gif below is based on this other Stackblitz demo):

enter image description here

julianobrasil
  • 8,954
  • 2
  • 33
  • 55
  • This works well, but what about the nested items? If you pass in an object with a nested group, the second group does not seem to patch the value – Jeremy Jun 16 '20 at 01:48
  • It's also working. To test it, in the stackblitz, I increased the delay for 5 seconds just to give me time to create two nested groups in Group 1 and, inside the second nested group, create one more even more nested. I'll put a gif on my answer to show that. – julianobrasil Jun 16 '20 at 02:19
  • Thank you. It seems this isn't possible to add the groups according to the patch, right? As in patching this data and then having the UI show accordingly. Rather than clicking the addGroup buttons, it will get the response (from `data` obj) and then display on the UI – Jeremy Jun 16 '20 at 02:24
  • Let me see if I'm following it right: You want to patch and, based on the data, create dynamically the corresponding form, is that right? – julianobrasil Jun 16 '20 at 02:28
  • That's right. Say the data comes in from an API (or statically in the tested sense). The app loads up, and the object from API/data has a group with a variable "asdf" and nested inside, is another group with the variable "fdsa" (just examples). So it's 1 group and 1 nested group inside. Rather than clicking "add group" and "add nested group", I am interested to see if the UI and form can dynamically be created based on the patched value from the API/data. I would be happy to clarify if that's not making sense still – Jeremy Jun 16 '20 at 02:32
  • It can be done, but Angular doesn't do it out of the box. Without trying to do it, I think we could try something in the `writeValue` method of the `ControlValueAcessor`. I'll give it a try here. – julianobrasil Jun 16 '20 at 02:36
  • Done... Take a look at the stackblitz demo. – julianobrasil Jun 16 '20 at 03:06
  • 1
    This is great. I noticed the add group button no longer adds the condition though, so in the group-control `writeValue` I added `.length` to the if statements within the setTimeout – Jeremy Jun 16 '20 at 15:42
  • Thanks. I had forgotten them. – julianobrasil Jun 16 '20 at 17:19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216095/discussion-between-jeremy-lucas-and-julianobrasil). – Jeremy Jun 16 '20 at 22:17