0

I have a data structure which consists of a parent category such as music. its title is defines in the object under hobbies[0].parent.title I want to have a ngFor which outputs this title out (I have no issues here), then under that there will be another nested ngFor dealing with the sub-genres which is under the hobbies[0].children[i] as an array - I would like to output these as checkboxes which is again no problem but the issue arises when I try to make them a reactive form, my basic need is to validate that at least 1 or 2 have been checked and to get the info of the checked item, with ReactiveForms I have a good idea how to achieve this without a nested json struct, its really throwing me off

I have tried a ton of different approaches in these articles but I cant seem to get it to work, any help much appreciated.

https://stackblitz.com/edit/form-array-3-levels

https://medium.com/hashtaagco/3-levels-of-nested-form-arrays-including-reactive-validations-we-decided-to-go-inception-mode-on-4fffe667fb2a

https://www.toptal.com/angular-js/angular-4-forms-validation

https://jenniferwadella.com/blog/managing-dynamic-and-nested-forms-angular

Ive been playing around with this for a few hours by my html has stayed the same

  <form [formGroup]="form"  action="">
    <label *ngFor="let parent of fetchInterests; let i = index">
      <div  *ngFor="let item of parent.children; let i = index">
        <ion-checkbox ></ion-checkbox>  {{  item.title }} {{  item.selected }}
      </div>
    </label>
  </form> 

my json demo data is below

var hobbies = [{
            "parent": {
                "parentId": "dMGkZuB8JV",
                "title": "Music",
                "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg"
            },
            "children": [{
                "title": "Jazz",
                "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
                "about": "",
                "selected": false
            }, {
                "title": "Rock",
                "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
                "about": "",
                "selected": false
            }, {
                "title": "Classical",
                "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
                "about": "",
                "selected": false
            }, {
                "title": "Soul",
                "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
                "about": "",
                "selected": false
            }]
        }, {
            "parent": {
                "parentId": "19h2yOfZaq",
                "title": "computers and systems",
                "hero": "https://s3-eu-west-1.amazonaws.com/it/placeholder.jpg"
            },
            "children": [{
                "title": "data processing",
                "hero": "https://s3-eu-west-1.amazonaws.com/it/placeholder.jpg",
                "about": "",
                "selected": false
            }]
        }, {
            "parent": {
                "parentId": "m2zQkAgOog",
                "title": "African planes",
                "hero": "https://s3-eu-west-1.amazonaws.com/outdoors/placeholder.jpg"
            },
            "children": [{
                "title": "camping",
                "hero": "https://s3-eu-west-1.amazonaws.com/outdoors/placeholder.jpg",
                "about": "",
                "selected": false
            }, {
                "title": "swimming",
                "hero": "https://s3-eu-west-1.amazonaws.com/outdoors/placeholder.jpg",
                "about": "",
                "selected": false
            }, {
                "title": "hunting",
                "hero": "https://s3-eu-west-1.amazonaws.com/outdoors/placeholder.jpg",
                "about": "",
                "selected": false
            }]
        }]

2 Answers 2

1

Kravitz, first think about what do you want to change. You can planning change only selected, or all the object.

I supouse you want to change only the "check", so you need a formArray of formArrays

  hobbiesForm: FormArray
  ngOnInit() {
    this.hobbiesForm = new FormArray(this.hobbies.map(x => new FormArray([], this.customValidator())))

    //this give so formArray as hobbies you get
    this.hobbies.forEach((x, index) => {  //for each hobiee
        const array=this.hobbiesForm.at(index) as FormArray
        x.children.forEach(c=>{
          array.push(new FormControl(c.selected))
        })
      })
  }

It's a few complex manage a formArray of FormArrays, but you only need take account that when we make *ngFor="let control of formArray.controls", "control" is the element in the array. So in hobbiesForm.controls will be a FormArray and inside the formArray will be a control.

Well, better in code

<form [formGroup]="hobbiesForm" (submit)="submit(hobbiesForm)">
  <!--array is the inner FormArray--->
  <div *ngFor="let array of hobbiesForm.controls;let i=index">
    <b>{{hobbies[i].parent.title}}:</b>
    <!--control is the inner control of the array-->
    <div *ngFor="let control of array.controls;let j=index">
    <label>{{hobbies[i].children[j].title}}</label>
    <input  type="checkbox" [formControl]="control">
    </div>
  </div>
  <button>submit</button>
</form>

Well, in submit we perhafs want to change the "hobbies" object, remember that we only has in value some like [[true,false,false,true],[false],[true,false,false]]

submit(form)
  {
    if (form.valid)
    {
      form.value.forEach((x,i)=>
      {
         x.forEach((c,j)=>{
            this.hobbies[i].children[j].selected=c;
         })
      })
      console.log(this.hobbies)
    }
  }

A customValidator can be like

customValidator() {
    return (formArray: FormArray) => {
      return formArray.value.filter(x=>x).length>0?null:{error:"at leat one"}
    }
  }

If is over all the form

customValidatorOverAll(min)
  {
    return (formArray: FormArray) => {
      let count=0;

      formArray.value.forEach(x=>{
         count+=x?x.filter(c=>c).length:0;
      })
      return count>=min?null:{error:"At least select "+min}
    }
  }

you can see it in this stackblitz

Update well, this question is about ion-select. A ion-select multiple return an array with the values, so the things are very different. We are goin to obtain some like [["Jazz","Rock"]["dat aprocessing"],["camping"]]

Thats an array of array but, we need an array of FormControls only. yes a FormControl can store any thing even an array. So

  ngOnInit() {
    this.hobbiesForm = new FormArray(
      this.hobbies.map(x => new FormControl('', this.customValidator())))
  }

And our .html some like

<form [formGroup]="hobbiesForm" (submit)="submit(hobbiesForm)">
    <ion-item *ngFor="let control of hobbiesForm.controls;let i=index">
        <ion-label>{{hobbies[i].parent.title}}:</ion-label>

        <ion-select [formControl]="control" multiple="true" cancelText="cancel" okText="ok">
            <ion-option *ngFor="let hobbiee of hobbies[i].children; let i = index" 
               [value]="hobbiee.title" selected="false">{{hobbiee.title}}</ion-option>
        </ion-select>
    </ion-item>
</form>

Again we iterate over hobbiesForm.controls. But in this case, the select gives us an array.

Well, I put a customValidator. If we want that at least was select one it's only that our customValidator was like

customValidator() {
    return (formControl: FormControl) => {
      return formControl.value && formControl.value.length>0?null:{error:"At least select one"}
    }
  }

If we want a customValidator over All the form, we can use some like

customValidatorOverAll(min)
  {
    return (formArray: FormArray) => {
      let count=0;
      formArray.value.forEach(x=>{
         count+=x.length;
      })
      return count>=min?null:{error:"At least select "+min}
    }
  }

And when we create the formArray

this.hobbiesForm = new FormArray(this.hobbies.map(x => new FormControl(''))
,this.customValidatorOverAll(2))

The ion-select stackblitz

NOTE: See that we iterate over formArray.controls, not over hobbies object, and use two index i,j and hobbies object to show the "labels", but this don't belong to the formArray

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

6 Comments

Wow this is fantastic!
K@Kravitz, no, it's not good, I mkae with heck, and you're using ion-select. I supouse multiple. Let me a time to update my answer with ion-select multiple
Awesome thanks, also how would you go about validating to make sure at least 2/3 are selected
I updated my answer using ion-select. I don't know what do you want to say "at leas 2/3 are selected" form all or at least one on each option? If you want that was select 2/3 of all, you need that the validator was over all the form
Also its not an ion select :) its just ion-input checkboxes but awesome to have that example too... Im trying to use this min validator with the checkboxes but it doesnt work, the form stays invalid
|
0

Rather than putting children as a separate array you can mention inside parent.

var hobbies = [{
        "parent": {
            "parentId": "dMGkZuB8JV",
            "title": "Music",
            "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg"
            "children": [{
                 "title": "Jazz",
                 "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
                 "about": "",
                  "selected": false
                  }, {
            "title": "Rock",
            "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
            "about": "",
            "selected": false
        }, {
            "title": "Classical",
            "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
            "about": "",
            "selected": false
        }, {
            "title": "Soul",
            "hero": "https://s3-eu-west-1.amazonaws.com/music/placeholder.jpg",
            "about": "",
            "selected": false
        }]

        }

    }

2 Comments

How do I create the form controls
Can you please upload your code in stackblitz and share the link

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.